├── .dockerignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── docker └── start.sh ├── example-service ├── services.sh └── test.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | README.md 3 | LICENSE 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | RUN apt-get update && apt-get install -y curl 4 | 5 | WORKDIR /app 6 | 7 | COPY docker/start.sh /app/start.sh 8 | COPY example-service /app/example-service 9 | COPY services.sh /app/services.sh 10 | COPY test.sh /app/test.sh 11 | 12 | RUN bash services.sh || true 13 | 14 | CMD ./start.sh 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Eduardo Cuomo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bash Service Manager 2 | 3 | Bash Script's for Service Manager 4 | 5 | Create your custom service for development. 6 | 7 | ## Test 8 | 9 | ```bash 10 | docker compose run --rm test 11 | ``` 12 | 13 | ## Usage 14 | 15 | 1. Create your _service script_. 16 | 2. Define the configuration environment variables: 17 | * `PID_FILE_PATH` 18 | * `LOG_FILE_PATH` 19 | 3. Define the configuration variables: 20 | * Mandatory configuration variables: 21 | * `$SERVICE_NAME` 22 | * `$SERVICE_CMD` _(array)_ 23 | * Optional configuration variables: 24 | * `$SERVICE_WORK_DIR` 25 | * `$SERVICE_ON_START` _(array)_ 26 | * `$SERVICE_ON_FINISH` _(array)_ 27 | 4. Copy `services.sh` content or import it. 28 | 5. Call `serviceMenu` function and pass the **action** as first parameter (ex: `serviceMenu "$1"`). 29 | 6. Make your new _service script_ executable: `chmod a+x my-service-script`. 30 | 7. Use it! 31 | 32 | ## Configuration Environment Variables 33 | 34 | ### PID_FILE_PATH 35 | 36 | Configure the `PID_FILE_PATH` variable before import `service.sh` script, and define the **PID** file path. 37 | 38 | ### LOG_FILE_PATH 39 | 40 | Configure the `LOG_FILE_PATH` variable before import `service.sh` script, and define the **LOG** file path. 41 | 42 | ## Configuration Variables 43 | 44 | ### Mandatory Configuration Variables 45 | 46 | #### SERVICE_NAME 47 | 48 | This is the user friendly _Service Name_. 49 | 50 | #### SERVICE_CMD (array variable) 51 | 52 | This is the _commands_ that you must execute to start _your service_. 53 | 54 | ### Optional Configuration Variables 55 | 56 | #### SERVICE_WORK_DIR 57 | 58 | The working directory is set, where it must be located to execute the _command_. 59 | 60 | #### SERVICE_ON_START (array variable) 61 | 62 | Commands to execute before _Service_ start. 63 | 64 | If function exit code is not `0` (zero), the service will not started. 65 | 66 | #### SERVICE_ON_FINISH (array variable) 67 | 68 | Commands to execute after _Service_ finish/exit. 69 | 70 | ## servicesMenu function 71 | 72 | Just call only this function to make everything work! 73 | 74 | ## Actions 75 | 76 | If it is an invalid action or empty action, you can see the _help_. 77 | 78 | * `start`: Start the _service_. 79 | * `stop`: Stop the _service_. 80 | * `restart`: Restart the _service_. If the _service_ is running, first call _stop_ then call _start_. 81 | * `status`: Get _service_ status. 82 | * `tail`: See all _service_ output. 83 | * `run`: Execute _service command_ and exit (this action **does not** stop the _service_). 84 | * `debug`: Stop _service_ (if running) and run _service command_ and exit. 85 | 86 | ## Examples 87 | 88 | ### Telegraf Service 89 | 90 | **telegraf.sh** file: 91 | 92 | ```bash 93 | #!/usr/bin/env bash 94 | 95 | appDir=$(cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd) 96 | 97 | . "$appDir/services.sh" 98 | 99 | # Friendly service name (mandatory) 100 | SERVICE_NAME="telegraf" 101 | # Command to run (mandatory, array variable) 102 | SERVICE_CMD=(./telegraf --config telegraf.conf) 103 | # Working Directory (optional) 104 | SERVICE_WORK_DIR="$appDir" 105 | # On start (optional, array variable) 106 | #SERVICE_ON_START=() 107 | # On finish (optional, array variable) 108 | #SERVICE_ON_FINISH=() 109 | 110 | export LOG_FILE_PATH="$SERVICE_NAME.log" 111 | export PID_FILE_PATH="$SERVICE_NAME.pid" 112 | 113 | serviceMenu "$1" 114 | ``` 115 | 116 | In console: 117 | 118 | ```bash 119 | $ telegraf.sh status 120 | $ telegraf.sh restart 121 | ``` 122 | 123 | ### Custom Service 124 | 125 | **my-service** file: 126 | 127 | ```bash 128 | #!/usr/bin/env bash 129 | 130 | export PID_FILE_PATH="my-service.pid" 131 | export LOG_FILE_PATH="my-service.log" 132 | 133 | . ./services.sh 134 | 135 | # Friendly service name (mandatory) 136 | SERVICE_NAME="Example Service" 137 | # Command to run (mandatory, array variable) 138 | SERVICE_CMD=(ping 1.1.1.1) 139 | # Working Directory (optional) 140 | #SERVICE_WORK_DIR= 141 | # On start (optional, array variable) 142 | #SERVICE_ON_START=() 143 | # On finish (optional, array variable) 144 | #SERVICE_ON_FINISH=() 145 | 146 | serviceMenu "$1" 147 | ``` 148 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | test: 5 | build: 6 | context: . 7 | image: bash-service-manager-test 8 | volumes: 9 | - ./docker/start.sh:/app/start.sh:ro 10 | - ./example-service:/app/example-service:ro 11 | - ./services.sh:/app/services.sh:ro 12 | - ./test.sh:/app/test.sh:ro 13 | -------------------------------------------------------------------------------- /docker/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Only as example for docker container 4 | 5 | set -ex 6 | 7 | ./example-service start 8 | echo "Ready!" 9 | 10 | ( 11 | sleep 3 12 | ./example-service status 13 | sleep 1 14 | ./example-service tail 15 | ) & 16 | TEST_PID="$!" 17 | 18 | sleep 30 19 | 20 | kill -9 $TEST_PID 21 | ./example-service stop 22 | 23 | echo "Finish!" 24 | 25 | # vim: filetype=sh tabstop=2 softtabstop=0 expandtab shiftwidth=2 smarttab -------------------------------------------------------------------------------- /example-service: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Service definition 4 | 5 | # Bootstrap 6 | . ./services.sh 7 | 8 | # Friendly service name (mandatoty) 9 | export SERVICE_NAME="Example Service" 10 | # Command to run (mandatoty, array variable) 11 | export SERVICE_CMD=("$(pwd)/test.sh" 'Hello world!') 12 | # Working Directory (optional) 13 | #SERVICE_WORK_DIR= 14 | # On start (optional, array variable) 15 | #SERVICE_ON_START=() 16 | # On finish (optional, array variable) 17 | #SERVICE_ON_FINISH=() 18 | 19 | export PID_FILE_PATH="${SERVICE_NAME}.pid" 20 | export LOG_FILE_PATH="${SERVICE_NAME}.log" 21 | 22 | # Menu 23 | serviceMenu "$1" 24 | 25 | # vim: filetype=sh tabstop=2 softtabstop=0 expandtab shiftwidth=2 smarttab -------------------------------------------------------------------------------- /services.sh: -------------------------------------------------------------------------------- 1 | # Bash Service Manager 2 | # Project: https://github.com/reduardo7/bash-service-manager 3 | 4 | ############################################################################### 5 | # BashX | https://github.com/reduardo7/bashx 6 | set +ex;export BASHX_VERSION="v3.1.2" 7 | (export LC_CTYPE=C;export LC_ALL=C;export LANG=C;set -e;x() { s="$*";echo "# Error: ${s:-Installation fail}" >&2;exit 1;};d=/dev/null;[ ! -z "$BASHX_VERSION" ] || x BASHX_VERSION is required;export BASHX_DIR="${BASHX_DIR:-${HOME:-/tmp}/.bashx/$BASHX_VERSION}";if [ ! -d "$BASHX_DIR" ];then u="https://raw.githubusercontent.com/reduardo7/bashx/$BASHX_VERSION/src/setup.sh";if type wget >$d 2>&1 ;then sh -c "$(wget -q $u -O -)" || x;elif type curl >$d 2>&1 ;then sh -c "$(curl -fsSL $u)" || x;else x wget or curl are required. Install wget or curl to continue;fi;fi) || exit $? 8 | . "${HOME:-/tmp}/.bashx/${BASHX_VERSION}/src/init.sh" 9 | ############################################################################### 10 | 11 | # export PID_FILE_PATH="/tmp/my-service.pid" 12 | # export LOG_FILE_PATH="/tmp/my-service.log" 13 | 14 | # Friendly service name (mandatoty) 15 | #SERVICE_NAME= 16 | # Command to run (mandatoty, array variable) 17 | #SERVICE_CMD=() 18 | # Working Directory (optional) 19 | #SERVICE_WORK_DIR= 20 | # On start (optional, array variable) 21 | #SERVICE_ON_START=() 22 | # On finish (optional, array variable) 23 | #SERVICE_ON_FINISH=() 24 | 25 | @execService() { 26 | [ -z "$SERVICE_WORK_DIR" ] || cd "$SERVICE_WORK_DIR" 27 | 28 | if [ ! -z "$SERVICE_ON_START" ] ; then 29 | ( "${SERVICE_ON_START[@]}" ) 30 | exit_code=$? 31 | 32 | if [ $exit_code -gt 0 ] ; then 33 | @log.warn "Start service fail" 34 | exit $exit_code 35 | fi 36 | fi 37 | 38 | if [ ! -z "$onFinish" ] ; then 39 | onServiceFinish() { 40 | local exit_code=$? 41 | ( "${onFinish[@]}" ) 42 | return $exit_code 43 | } 44 | 45 | trap onServiceFinish EXIT 46 | fi 47 | 48 | nohup "${SERVICE_CMD[@]}" \ 49 | >>"$LOG_FILE_PATH" \ 50 | 2>>"$LOG_FILE_PATH" \ 51 | & echo $! >"$PID_FILE_PATH" 52 | 53 | return $? 54 | } 55 | 56 | @serviceStatus() { 57 | if [ -f "$PID_FILE_PATH" ] && [ ! -z "$(cat "$PID_FILE_PATH")" ] ; then 58 | local PID=$(cat "$PID_FILE_PATH") 59 | local kill_result_message=$(kill -0 $PID 2>&1) 60 | local kill_result_code=$? 61 | 62 | if (( $kill_result_code == 0 )) ; then 63 | @log "Service $SERVICE_NAME is runnig with PID $PID" 64 | return 0 65 | elif [[ $kill_result_message == *"kill: ($PID) - No such process" ]] ; then 66 | @log.warn "Service $SERVICE_NAME is not running (process PID $PID not exists)" 67 | return 2 68 | elif [[ $kill_result_message == *"kill: ($PID) - Operation not permitted" ]] ; then 69 | @log.warn "Status of $SERVICE_NAME service could not be obtained (operation not permitted for process PID $PID)" 70 | return 1 71 | else 72 | @log.warn "Status of $SERVICE_NAME service could not be obtained (process PID $PID)" 73 | return 3 74 | fi 75 | else 76 | @log.warn "Service $SERVICE_NAME is not running" 77 | return 4 78 | fi 79 | } 80 | 81 | @serviceStart() { 82 | if @serviceStatus ; then 83 | @log "Service ${SERVICE_NAME} already running with PID $(cat "$PID_FILE_PATH")" 84 | return 0 85 | fi 86 | 87 | @log "Starting ${SERVICE_NAME} service..." 88 | touch "$LOG_FILE_PATH" >/dev/null || @app.error "Can not create $LOG_FILE_PATH file" 89 | touch "$PID_FILE_PATH" >/dev/null || @app.error "Can not create $PID_FILE_PATH file" 90 | 91 | @execService 92 | @log "Service ${SERVICE_NAME} started with PID $(cat "$PID_FILE_PATH")" 93 | sleep 2 94 | 95 | @serviceStatus 96 | local exit_code=$? 97 | 98 | if (( ${exit_code} == 0 )); then 99 | @log "Service ${SERVICE_NAME} started successfully" 100 | else 101 | @log.warn "Service ${SERVICE_NAME} could not be started (exit code: ${exit_code})" 102 | cat "$LOG_FILE_PATH" 103 | fi 104 | 105 | return ${exit_code} 106 | } 107 | 108 | @serviceStop() { 109 | if [ -f "$PID_FILE_PATH" ] && [ ! -z "$(cat "$PID_FILE_PATH")" ]; then 110 | touch "$PID_FILE_PATH" >/dev/null || @app.error "Can not touch $PID_FILE_PATH file" 111 | 112 | @log "Stopping ${SERVICE_NAME}..." 113 | 114 | for p in $(cat "$PID_FILE_PATH"); do 115 | if kill -0 $p >/dev/null 2>&1 ; then 116 | kill $p 117 | sleep 2 118 | 119 | if kill -0 $p >/dev/null 2>&1 ; then 120 | kill -9 $p 121 | sleep 2 122 | 123 | if kill -0 $p >/dev/null 2>&1 ; then 124 | @log "Exec: sudo kill -9 $p" 125 | sudo kill -9 $p 126 | sleep 2 127 | fi 128 | fi 129 | fi 130 | done 131 | 132 | if @serviceStatus ; then 133 | @app.error "Error stopping Service ${SERVICE_NAME}! Service is still running with PID $(cat "$PID_FILE_PATH")" 134 | fi 135 | 136 | rm -f "$PID_FILE_PATH" || @app.error "Can not delete $PID_FILE_PATH file" 137 | return 0 138 | else 139 | @log.warn "Service $SERVICE_NAME is not running" 140 | fi 141 | } 142 | 143 | @serviceRestart() { 144 | @serviceStop 145 | sleep 2 146 | @serviceStart 147 | } 148 | 149 | @serviceDebug() { 150 | @serviceStop 151 | @log "Debugging ${SERVICE_NAME}..." 152 | @execService 153 | exitCode=$? 154 | @log "Finish debugging ${SERVICE_NAME} (exit code ${exitCode})" 155 | return $exitCode 156 | } 157 | 158 | # Service menu 159 | serviceMenu() { 160 | case "$1" in 161 | start) 162 | @serviceStart 163 | ;; 164 | stop) 165 | @serviceStop 166 | ;; 167 | restart) 168 | @serviceRestart 169 | ;; 170 | status) 171 | @serviceStatus 172 | ;; 173 | run) 174 | ( 175 | [ -z "$SERVICE_WORK_DIR" ] || cd "$SERVICE_WORK_DIR" 176 | "${SERVICE_CMD[@]}" 177 | ) 178 | ;; 179 | debug) 180 | @serviceDebug 181 | ;; 182 | tail) 183 | set -ex 184 | tail -f -n 10 "${LOG_FILE_PATH}" 185 | ;; 186 | logs) 187 | set -ex 188 | cat "${LOG_FILE_PATH}" 189 | ;; 190 | *) 191 | @log "Usage: {start|stop|restart|status|run|debug|tail}" 192 | exit 1 193 | ;; 194 | esac 195 | } 196 | 197 | # vim: filetype=sh tabstop=2 softtabstop=0 expandtab shiftwidth=2 smarttab -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Script to run as a service 4 | 5 | x=0 6 | 7 | while true; do 8 | echo "[$x] $@" 9 | echo "[$x] ERR" >&2 10 | sleep 1 11 | x=$((x+1)) 12 | done 13 | 14 | # vim: filetype=sh tabstop=2 softtabstop=0 expandtab shiftwidth=2 smarttab --------------------------------------------------------------------------------