reachable-watcher 1.0.124 → 1.0.126-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reachable-watcher",
3
- "version": "1.0.124",
3
+ "version": "1.0.126-1",
4
4
  "description": "Check status codes of web servers and sends notification e-mails",
5
5
  "keywords": [
6
6
  "reachable",
@@ -15,36 +15,35 @@
15
15
  "license": "CC-BY-3.0",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "https://github.com/thaibault/reachable-watcher"
18
+ "url": "git+https://github.com/thaibault/reachable-watcher.git"
19
19
  },
20
20
  "main": "reachable-watcher.sh",
21
- "bin": "reachable-watcher.sh",
21
+ "bin": {
22
+ "reachable-watcher": "reachable-watcher.sh"
23
+ },
22
24
  "files": [
23
25
  "reachable-watcher.service"
24
26
  ],
25
27
  "scripts": {
26
28
  "clear": "rimraf apiDocumentation",
27
- "document": "mkdir --parents apiDocumentation/reachable-watcher/latest && bashlink-document reachable-watcher 1>apiDocumentation/reachable-watcher/latest/index.html",
28
- "document:show": "bashlink-document reachable-watcher",
29
- "document:show:error": "bashlink-document reachable-watcher 1>/dev/null",
30
- "lint": "hash shellcheck && shellcheck -x *.sh || echo Please install \"shellcheck\". || true",
29
+ "document": "mkdir --parents apiDocumentation/reachable-watcher/latest && bash ./node_modules/.bin/bashlink-document reachable-watcher 1>apiDocumentation/reachable-watcher/latest/index.html",
30
+ "document:show": "bash ./node_modules/.bin/bashlink-document reachable-watcher",
31
+ "document:show:error": "bash ./node_modules/.bin/bashlink-document reachable-watcher 1>/dev/null",
32
+ "lint": "shellcheck -x *.sh",
31
33
  "lint:given": "shellcheck -x",
32
- "test": "./node_modules/.bin/bashlink-doctest --verbose ../../reachable-watcher",
34
+ "test": "bash ./node_modules/.bin/bashlink-doctest --verbose ../../reachable-watcher",
33
35
  "update:documentation": "web-documentation"
34
36
  },
35
37
  "devDependencies": {
36
- "bashlink": "^1.0.282",
37
- "rimraf": "^6.1.3",
38
+ "bashlink": "^1.0.292",
39
+ "rimraf": "^6.1.4",
38
40
  "shellcheck": "^4.1.0",
39
- "web-documentation": "^1.0.65"
41
+ "web-documentation": "^1.0.72"
40
42
  },
41
43
  "engines": {
42
44
  "node": ">=26",
43
45
  "npm": ">=12",
44
46
  "yarn": ">=4"
45
47
  },
46
- "webDocumentation": {
47
- "trackingCode": "UA-40192634-15"
48
- },
49
48
  "packageManager": "yarn@4.18.1+sha512.b2e1e7524f654f2749d32b4ebcb4622473cb5bcbc485df2007e12a154e50162a4d795526768bc5f5b8f81717bfd79deb2472813d86fb5ae2eb551fa9c872b08f"
50
49
  }
@@ -19,7 +19,7 @@ rw_download() {
19
19
  local -r __documentation__='
20
20
  Simply downloads missing modules.
21
21
 
22
- >>> br.download --silent https://domain.tld/path/to/file.ext; echo $?
22
+ >>> rw.download --silent https://domain.tld/path/to/file.ext; echo $?
23
23
  6
24
24
  '
25
25
  command curl --insecure "$@"
@@ -53,8 +53,10 @@ else
53
53
  # shellcheck disable=SC1091
54
54
  source "${BL_MODULE_REMOTE_MODULE_CACHE_PATH}/module.sh"
55
55
  fi
56
+ # NOTE: Shortens this module's scope to "rw_*" and "RW_*" names.
57
+ BL_MODULE_FUNCTION_SCOPE_REWRITES+=('^reachable\.watcher$/rw/')
58
+ BL_MODULE_GLOBAL_SCOPE_REWRITES+=('^REACHABLE_WATCHER$/RW/')
56
59
  bl.module.import bashlink.array
57
- bl.module.import bashlink.dictionary
58
60
  bl.module.import bashlink.exception
59
61
  bl.module.import bashlink.logging
60
62
  bl.module.import bashlink.tools
@@ -106,30 +108,45 @@ declare -agr RW__DEPENDENCIES__=(
106
108
  bash
107
109
  curl
108
110
  date
109
- grep
110
111
  msmtp
111
112
  sleep
112
113
  )
113
- BL_MODULE_FUNCTION_SCOPE_REWRITES+=('^reachableWatcher([._][a-zA-Z_-]+)?$/rw\1/')
114
- BL_MODULE_GLOBAL_SCOPE_REWRITES+=('^REACHABLE_WATCHER(_[a-zA-Z_-]+)?$/RW\1/')
114
+ declare -gr RW__DOCTEST_SETUP__='
115
+ # Mocks the status code request (configurable via
116
+ # "RW_DOCTEST_STATUS_CODE") and prints only the receiver and subject of
117
+ # each sent e-mail.
118
+ curl() {
119
+ echo -n "${RW_DOCTEST_STATUS_CODE:-200}"
120
+ }
121
+ msmtp() {
122
+ command grep --extended-regexp "^(To|Subject):"
123
+ }
124
+ '
115
125
  ## region default options
116
- declare -ag RW_URLS_TO_CHECK=()
126
+ # Maps each url to its expected status code followed by its recipients.
127
+ declare -Ag RW_URLS_TO_CHECK=()
117
128
  # Wait for 5 minutes (60 * 5 = 300).
118
129
  declare -ig RW_DELAY_BETWEEN_TWO_CONSEQUTIVE_REQUESTS_IN_SECONDS=300
119
130
  declare -g RW_DATE_TIME_FORMAT='%T:%N at %d.%m.%Y'
120
131
  declare -g RW_SENDER_E_MAIL_ADDRESS=''
121
- declare -g RW_REPLIER_E_MAIL_ADDRESS="$RW_SENDER_E_MAIL_ADDRESS"
132
+ # Falls back to the sender address when empty.
133
+ declare -g RW_REPLIER_E_MAIL_ADDRESS=''
122
134
  declare -g RW_VERBOSE=false
123
135
  declare -g RW_NAME=NODE_NAME
124
136
  ## endregion
125
137
  ## region load options if present
126
- if [ -f /etc/reachableWatcher ]; then
138
+ if \
139
+ [ "${BL_DOCTEST_RUNNING:-false}" != true ] && \
140
+ [ -f /etc/reachableWatcher ]
141
+ then
127
142
  # shellcheck disable=SC1091
128
143
  source /etc/reachableWatcher
129
144
  fi
130
145
  ## endregion
146
+ # Last determined state ("valid" or "invalid") of each checked url.
147
+ declare -Ag RW_STATES=()
131
148
  # endregion
132
- # region functions
149
+ # region helper
133
150
  alias rw.is_status_valid=rw_is_status_valid
134
151
  rw_is_status_valid() {
135
152
  local -r __documentation__='
@@ -139,10 +156,10 @@ rw_is_status_valid() {
139
156
  0
140
157
 
141
158
  >>> rw.is_status_valid 200 "000"; echo $?
142
- 0
159
+ 1
143
160
 
144
161
  >>> rw.is_status_valid 200 0; echo $?
145
- 0
162
+ 1
146
163
 
147
164
  >>> rw.is_status_valid "000" 200; echo $?
148
165
  0
@@ -151,6 +168,9 @@ rw_is_status_valid() {
151
168
  0
152
169
 
153
170
  >>> rw.is_status_valid 206 "000"; echo $?
171
+ 1
172
+
173
+ >>> rw.is_status_valid "000" "000"; echo $?
154
174
  0
155
175
 
156
176
  >>> rw.is_status_valid 206 200; echo $?
@@ -172,12 +192,13 @@ rw_is_status_valid() {
172
192
  local -ir expected_status_code=$1
173
193
  local -ir given_status_code=$2
174
194
  local -ar valid_ok_codes=(0 200 206)
175
- if \
176
- (( expected_status_code == given_status_code )) || \
177
- (( given_status_code == 0 ))
178
- then
195
+ if (( expected_status_code == given_status_code )); then
179
196
  return 0
180
197
  fi
198
+ # NOTE: Status code "000" indicates that the url is not reachable.
199
+ if (( given_status_code == 0 )); then
200
+ return 1
201
+ fi
181
202
  if \
182
203
  bl.array.contains "$expected_status_code" "${valid_ok_codes[@]}" &&
183
204
  bl.array.contains "$given_status_code" "${valid_ok_codes[@]}"
@@ -186,95 +207,214 @@ rw_is_status_valid() {
186
207
  fi
187
208
  return 1
188
209
  }
189
- ## region controller
190
- alias rw.main=rw_main
191
- rw_main() {
210
+ alias rw.get_expected_status_code=rw_get_expected_status_code
211
+ rw_get_expected_status_code() {
192
212
  local -r __documentation__='
193
- Main entry point.
213
+ Determines the expected status code of given url configuration.
214
+
215
+ >>> rw.get_expected_status_code "200 a@domain.tld b@domain.tld"
216
+ 200
217
+
218
+ >>> rw.get_expected_status_code " 206 a@domain.tld"
219
+ 206
220
+
221
+ >>> rw.get_expected_status_code 404
222
+ 404
194
223
  '
195
- $RW_VERBOSE && \
196
- bl.logging.set_level info
197
- while true; do
198
- local url_to_check
199
- for url_to_check in "${!RW_URLS_TO_CHECK[@]}"; do
200
- local -i expected_status_code="$(
201
- echo "${RW_URLS_TO_CHECK[$url_to_check]}" | \
202
- grep '^[^ ]+' --only-matching --extended-regexp)"
203
- bl.logging.debug "Check url \"$url_to_check\" for status code $expected_status_code."
204
- local -i given_status_code="$(
205
- curl \
206
- --head \
207
- --insecure \
208
- --output /dev/null \
209
- --silent \
210
- --write-out '%{http_code}' \
211
- "$url_to_check"
212
- )"
213
- local normalized_url_to_check="$(
214
- echo "$url_to_check" | sed 's/[:/.]/_/g')"
215
- if rw.is_status_valid \
216
- "$expected_status_code" \
217
- "$given_status_code"
218
- then
219
- local message="Requested URL \"$url_to_check\" returns valid status code $given_status_code on $(date +"$RW_DATE_TIME_FORMAT")."
220
- bl.logging.debug "$message"
221
- if [ "$(bl.dictionary.get state "$normalized_url_to_check")" = invalid ]; then
222
- bl.logging.info "Status has changed to \"valid\" for \"$url_to_check\". Do notification."
223
- local e_main_address
224
- for e_mail_address in $(
225
- echo "${RW_URLS_TO_CHECK[$url_to_check]}" | \
226
- grep ' .+$' --only-matching --extended-regexp)
227
- do
228
- msmtp -t <<EOF
229
- From: $RW_SENDER_E_MAIL_ADDRESS
230
- To: $e_mail_address
231
- Reply-To: $RW_REPLIER_E_MAIL_ADDRESS
232
- Date: $(date)
233
- Subject: $RW_NAME registers: "$url_to_check" responses with valid status code $given_status_code.
224
+ local -a parts
225
+ read -r -a parts <<<"$1"
226
+ echo "${parts[0]}"
227
+ }
228
+ alias rw.get_recipients=rw_get_recipients
229
+ rw_get_recipients() {
230
+ local -r __documentation__='
231
+ Determines all recipients of given url configuration (one per line).
234
232
 
235
- $message
233
+ >>> rw.get_recipients "200 a@domain.tld b@domain.tld"
234
+ a@domain.tld
235
+ b@domain.tld
236
236
 
237
- EOF
238
- done
239
- bl.dictionary.set state "$normalized_url_to_check" valid
240
- else
241
- bl.logging.debug "Status unchanged."
242
- fi
243
- else
244
- local message="Requested URL \"$url_to_check\" returns status code $given_status_code (instead of \"$expected_status_code\") on $(date +"$RW_DATE_TIME_FORMAT")."
245
- bl.logging.debug "$message"
246
- if \
247
- [ "$(bl.dictionary.get state "$normalized_url_to_check")" = valid ] ||
248
- [ "$(bl.dictionary.get state "$normalized_url_to_check")" = '' ]
249
- then
250
- bl.logging.info "Status has changed to \"invalid for \"$url_to_check\". Do notification."
251
- local e_main_address
252
- for e_mail_address in $(
253
- echo "${RW_URLS_TO_CHECK[$url_to_check]}" | \
254
- grep ' .+$' --only-matching --extended-regexp)
255
- do
256
- msmtp -t <<EOF
237
+ >>> rw.get_recipients 200 | wc --lines
238
+ 0
239
+ '
240
+ local -a parts
241
+ read -r -a parts <<<"$1"
242
+ if (( ${#parts[@]} > 1 )); then
243
+ printf '%s\n' "${parts[@]:1}"
244
+ fi
245
+ }
246
+ alias rw.request_status_code=rw_request_status_code
247
+ rw_request_status_code() {
248
+ local -r __documentation__='
249
+ Requests given url and prints the resulting status code ("000" if no
250
+ response could be retrieved).
251
+
252
+ >>> rw.request_status_code https://domain.tld
253
+ 200
254
+
255
+ >>> RW_DOCTEST_STATUS_CODE=503
256
+ >>> rw.request_status_code https://domain.tld
257
+ 503
258
+
259
+ >>> unset -f curl
260
+ >>> rw.request_status_code http://127.0.0.1:1
261
+ 000
262
+ '
263
+ # NOTE: Declaring and assigning at once ignores the exit code of failing
264
+ # requests so that they do not trigger an exception.
265
+ local -r status_code="$(
266
+ curl \
267
+ --head \
268
+ --insecure \
269
+ --output /dev/null \
270
+ --silent \
271
+ --write-out '%{http_code}' \
272
+ "$1"
273
+ )"
274
+ echo "$status_code"
275
+ }
276
+ alias rw.notify=rw_notify
277
+ rw_notify() {
278
+ local -r __documentation__='
279
+ Sends an e-mail with given subject and message to given recipient.
280
+
281
+ >>> msmtp() { cat; }
282
+ >>> RW_SENDER_E_MAIL_ADDRESS=sender@domain.tld
283
+ >>> rw.notify a@domain.tld "A subject" "A message." | \
284
+ >>> grep --invert-match --extended-regexp "^(Date:.*)?$"
285
+ From: sender@domain.tld
286
+ To: a@domain.tld
287
+ Reply-To: sender@domain.tld
288
+ Subject: A subject
289
+ A message.
290
+
291
+ >>> msmtp() { grep "^Reply-To:"; }
292
+ >>> RW_SENDER_E_MAIL_ADDRESS=sender@domain.tld
293
+ >>> RW_REPLIER_E_MAIL_ADDRESS=replier@domain.tld
294
+ >>> rw.notify a@domain.tld "A subject" "A message."
295
+ Reply-To: replier@domain.tld
296
+ '
297
+ local -r recipient="$1"
298
+ local -r subject="$2"
299
+ local -r message="$3"
300
+ msmtp -t <<EOF
257
301
  From: $RW_SENDER_E_MAIL_ADDRESS
258
- To: $e_mail_address
259
- Reply-To: $RW_REPLIER_E_MAIL_ADDRESS
302
+ To: $recipient
303
+ Reply-To: ${RW_REPLIER_E_MAIL_ADDRESS:-$RW_SENDER_E_MAIL_ADDRESS}
260
304
  Date: $(date)
261
- Subject: $RW_NAME registers: "$url_to_check" responses with invalid status code $given_status_code!
305
+ Subject: $subject
262
306
 
263
307
  $message
264
308
 
265
309
  EOF
266
- done
267
- bl.dictionary.set state "$normalized_url_to_check" invalid
268
- else
269
- bl.logging.debug "Status unchanged."
270
- fi
271
- fi
310
+ }
311
+ # endregion
312
+ # region controller
313
+ alias rw.check_url=rw_check_url
314
+ rw_check_url() {
315
+ local -r __documentation__='
316
+ Checks given url once and notifies all its recipients if its state has
317
+ changed. An initially valid state is not notified.
318
+
319
+ >>> RW_URLS_TO_CHECK=(["https://domain.tld"]="200 a@domain.tld")
320
+ >>> rw.check_url https://domain.tld
321
+ >>> echo "${RW_STATES[https://domain.tld]}"
322
+ valid
323
+
324
+ >>> RW_URLS_TO_CHECK=(["https://domain.tld"]="200 a@domain.tld")
325
+ >>> RW_DOCTEST_STATUS_CODE=503
326
+ >>> rw.check_url https://domain.tld
327
+ >>> echo "${RW_STATES[https://domain.tld]}"
328
+ To: a@domain.tld
329
+ Subject: NODE_NAME registers: "https://domain.tld" responses with invalid status code 503!
330
+ invalid
331
+
332
+ >>> RW_URLS_TO_CHECK=(["https://domain.tld"]="200 a@domain.tld")
333
+ >>> RW_DOCTEST_STATUS_CODE=000
334
+ >>> rw.check_url https://domain.tld
335
+ >>> echo "${RW_STATES[https://domain.tld]}"
336
+ To: a@domain.tld
337
+ Subject: NODE_NAME registers: "https://domain.tld" is not reachable!
338
+ invalid
339
+
340
+ >>> RW_URLS_TO_CHECK=(
341
+ >>> ["https://domain.tld/?a=b&c"]="200 a@domain.tld b@domain.tld"
342
+ >>> )
343
+ >>> rw.check_url "https://domain.tld/?a=b&c"
344
+ >>> RW_DOCTEST_STATUS_CODE=500
345
+ >>> rw.check_url "https://domain.tld/?a=b&c"
346
+ >>> rw.check_url "https://domain.tld/?a=b&c"
347
+ >>> RW_DOCTEST_STATUS_CODE=206
348
+ >>> rw.check_url "https://domain.tld/?a=b&c"
349
+ >>> rw.check_url "https://domain.tld/?a=b&c"
350
+ To: a@domain.tld
351
+ Subject: NODE_NAME registers: "https://domain.tld/?a=b&c" responses with invalid status code 500!
352
+ To: b@domain.tld
353
+ Subject: NODE_NAME registers: "https://domain.tld/?a=b&c" responses with invalid status code 500!
354
+ To: a@domain.tld
355
+ Subject: NODE_NAME registers: "https://domain.tld/?a=b&c" responses with valid status code 206.
356
+ To: b@domain.tld
357
+ Subject: NODE_NAME registers: "https://domain.tld/?a=b&c" responses with valid status code 206.
358
+ '
359
+ local -r url="$1"
360
+ local -r configuration="${RW_URLS_TO_CHECK[$url]}"
361
+ local -ir expected_status_code="$(
362
+ rw.get_expected_status_code "$configuration")"
363
+ bl.logging.debug "Check url \"$url\" for status code $expected_status_code."
364
+ local -ir given_status_code="$(rw.request_status_code "$url")"
365
+ local -r now="$(date +"$RW_DATE_TIME_FORMAT")"
366
+ local state message subject
367
+ if rw.is_status_valid "$expected_status_code" "$given_status_code"; then
368
+ state=valid
369
+ message="Requested URL \"$url\" returns valid status code $given_status_code on ${now}."
370
+ subject="$RW_NAME registers: \"$url\" responses with valid status code ${given_status_code}."
371
+ elif (( given_status_code == 0 )); then
372
+ state=invalid
373
+ message="Requested URL \"$url\" is not reachable (instead of returning status code \"$expected_status_code\") on ${now}."
374
+ subject="$RW_NAME registers: \"$url\" is not reachable!"
375
+ else
376
+ state=invalid
377
+ message="Requested URL \"$url\" returns status code $given_status_code (instead of \"$expected_status_code\") on ${now}."
378
+ subject="$RW_NAME registers: \"$url\" responses with invalid status code ${given_status_code}!"
379
+ fi
380
+ bl.logging.debug "$message"
381
+ local -r last_state="${RW_STATES[$url]:-valid}"
382
+ RW_STATES[$url]="$state"
383
+ if [ "$state" = "$last_state" ]; then
384
+ bl.logging.debug "Status unchanged."
385
+ return 0
386
+ fi
387
+ bl.logging.info "Status has changed to \"$state\" for \"$url\". Do notification."
388
+ local recipient
389
+ for recipient in $(rw.get_recipients "$configuration"); do
390
+ rw.notify "$recipient" "$subject" "$message"
391
+ done
392
+ }
393
+ alias rw.main=rw_main
394
+ rw_main() {
395
+ local -r __documentation__='
396
+ Main entry point: Checks all configured urls periodically.
397
+
398
+ >>> RW_URLS_TO_CHECK=(
399
+ >>> ["https://a.tld"]="200 a@domain.tld"
400
+ >>> ["https://b.tld"]="404 b@domain.tld"
401
+ >>> )
402
+ >>> sleep() { exit; }
403
+ >>> rw.main
404
+ To: b@domain.tld
405
+ Subject: NODE_NAME registers: "https://b.tld" responses with invalid status code 200!
406
+ '
407
+ $RW_VERBOSE && \
408
+ bl.logging.set_level info
409
+ while true; do
410
+ local url
411
+ for url in "${!RW_URLS_TO_CHECK[@]}"; do
412
+ rw.check_url "$url"
272
413
  done
273
414
  bl.logging.debug "Wait for $RW_DELAY_BETWEEN_TWO_CONSEQUTIVE_REQUESTS_IN_SECONDS seconds until next check."
274
415
  sleep "$RW_DELAY_BETWEEN_TWO_CONSEQUTIVE_REQUESTS_IN_SECONDS"
275
416
  done
276
417
  }
277
- ## endregion
278
418
  # endregion
279
419
  if bl.tools.is_main; then
280
420
  bl.exception.activate
package/readme.md CHANGED
@@ -19,6 +19,9 @@ Project Status
19
19
 
20
20
  [![build push package](https://img.shields.io/github/actions/workflow/status/thaibault/reachable-watcher/build-package-and-push.yaml?label=build%20push%20package&style=for-the-badge)](https://github.com/thaibault/reachable-watcher/actions/workflows/build-package-and-push.yaml)
21
21
 
22
+ [![lint](https://img.shields.io/github/actions/workflow/status/thaibault/reachable-watcher/lint.yaml?label=lint&style=for-the-badge)](https://github.com/thaibault/reachable-watcher/actions/workflows/lint.yaml)
23
+ [![test](https://img.shields.io/github/actions/workflow/status/thaibault/reachable-watcher/test.yaml?label=test&style=for-the-badge)](https://github.com/thaibault/reachable-watcher/actions/workflows/test.yaml)
24
+
22
25
  [![deploy web documentation](https://img.shields.io/github/actions/workflow/status/thaibault/reachable-watcher/deploy-web-documentation.yaml?label=deploy%20web%20documentation&style=for-the-badge)](https://github.com/thaibault/reachable-watcher/actions/workflows/deploy-web-documentation.yaml)
23
26
  [![web documentation](https://img.shields.io/website-up-down-green-red/https/tsickert.com/reachable-watcher.svg?label=web-documentation&style=for-the-badge)](https://tsickert.com/reachable-watcher)
24
27