aidevops 3.12.0 → 3.13.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/README.md +17 -0
- package/VERSION +1 -1
- package/aidevops.sh +185 -5
- package/package.json +1 -1
- package/setup-modules/schedulers-linux.sh +386 -0
- package/setup-modules/schedulers-platform.sh +1024 -0
- package/setup-modules/schedulers-pulse.sh +978 -0
- package/setup-modules/schedulers.sh +345 -2330
- package/setup-modules/tool-install.sh +89 -0
- package/setup.sh +139 -48
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Schedulers Linux Sub-Library -- systemd/cron scheduler installation and
|
|
6
|
+
# uninstall functions for Linux (and macOS uninstall path).
|
|
7
|
+
# =============================================================================
|
|
8
|
+
# This sub-library is sourced by setup-modules/schedulers.sh (the orchestrator).
|
|
9
|
+
# It covers:
|
|
10
|
+
# - systemd user service availability check
|
|
11
|
+
# - systemd value escaping
|
|
12
|
+
# - Building systemd Environment= and cron env prefix lines
|
|
13
|
+
# - Generic systemd timer installation
|
|
14
|
+
# - Generic cron entry installation
|
|
15
|
+
# - Linux dispatcher (systemd preferred, cron fallback)
|
|
16
|
+
# - Generic scheduler uninstall (launchd/systemd/cron)
|
|
17
|
+
# - Supervisor pulse uninstall
|
|
18
|
+
#
|
|
19
|
+
# Usage: source "${SCRIPT_DIR}/schedulers-linux.sh"
|
|
20
|
+
#
|
|
21
|
+
# Dependencies:
|
|
22
|
+
# - shared-constants.sh (print_info, print_warning)
|
|
23
|
+
#
|
|
24
|
+
# Part of aidevops framework: https://aidevops.sh
|
|
25
|
+
|
|
26
|
+
# Apply strict mode only when executed directly (not when sourced)
|
|
27
|
+
[[ "${BASH_SOURCE[0]}" == "${0}" ]] && set -euo pipefail
|
|
28
|
+
|
|
29
|
+
# Include guard
|
|
30
|
+
[[ -n "${_SCHEDULERS_LINUX_LIB_LOADED:-}" ]] && return 0
|
|
31
|
+
_SCHEDULERS_LINUX_LIB_LOADED=1
|
|
32
|
+
|
|
33
|
+
# SCRIPT_DIR fallback — needed when sourced from test harnesses that don't set it.
|
|
34
|
+
if [[ -z "${SCRIPT_DIR:-}" ]]; then
|
|
35
|
+
_sched_linux_lib_path="${BASH_SOURCE[0]%/*}"
|
|
36
|
+
[[ "$_sched_linux_lib_path" == "${BASH_SOURCE[0]}" ]] && _sched_linux_lib_path="."
|
|
37
|
+
SCRIPT_DIR="$(cd "$_sched_linux_lib_path" && pwd)"
|
|
38
|
+
unset _sched_linux_lib_path
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# --- Functions ---
|
|
42
|
+
|
|
43
|
+
# Check if systemd user services are available on this Linux system.
|
|
44
|
+
# Returns 0 if systemd --user is functional, 1 otherwise.
|
|
45
|
+
_systemd_user_available() {
|
|
46
|
+
command -v systemctl >/dev/null 2>&1 || return 1
|
|
47
|
+
systemctl --user status >/dev/null 2>&1 || return 1
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Escape a value for safe embedding in a systemd unit Environment= or ExecStart=
|
|
52
|
+
# directive. systemd interprets % as specifiers (%h, %n, %t, etc.) and spaces
|
|
53
|
+
# as key-value separators. This helper:
|
|
54
|
+
# 1. Escapes \ → \\ (must be first to avoid double-escaping)
|
|
55
|
+
# 2. Doubles % → %% (escape specifiers)
|
|
56
|
+
# 3. Escapes embedded " → \"
|
|
57
|
+
# 4. Wraps the result in "..." (handles spaces and other shell metacharacters)
|
|
58
|
+
# Usage: escaped=$(_systemd_escape "$value")
|
|
59
|
+
#
|
|
60
|
+
# WARNING: Do NOT use for StandardOutput= or StandardError= directives.
|
|
61
|
+
# systemd does not strip outer quotes from those values — "append:/path" is
|
|
62
|
+
# treated as a literal filename with quote characters, failing silently.
|
|
63
|
+
# Use bare values for StandardOutput=/StandardError=:
|
|
64
|
+
# StandardOutput=append:${log_file} ← correct
|
|
65
|
+
# StandardOutput=$(_systemd_escape "append:${log_file}") ← WRONG
|
|
66
|
+
_systemd_escape() {
|
|
67
|
+
local _val="$1"
|
|
68
|
+
# Step 1: escape backslashes
|
|
69
|
+
_val="${_val//\\/\\\\}"
|
|
70
|
+
# Step 2: escape % specifiers
|
|
71
|
+
_val="${_val//%/%%}"
|
|
72
|
+
# Step 3: escape embedded double-quotes
|
|
73
|
+
_val="${_val//\"/\\\"}"
|
|
74
|
+
# Step 4: wrap in double-quotes
|
|
75
|
+
printf '"%s"' "$_val"
|
|
76
|
+
return 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Build systemd Environment= lines from newline-separated KEY=VALUE pairs.
|
|
80
|
+
# Always appends HOME and PATH for parity with launchd and cron execution.
|
|
81
|
+
_scheduler_systemd_env_lines() {
|
|
82
|
+
local env_vars="$1"
|
|
83
|
+
local _env_lines=""
|
|
84
|
+
|
|
85
|
+
if [[ -n "$env_vars" ]]; then
|
|
86
|
+
while IFS= read -r _kv; do
|
|
87
|
+
[[ -z "$_kv" ]] && continue
|
|
88
|
+
local _key="${_kv%%=*}"
|
|
89
|
+
local _raw_val="${_kv#*=}"
|
|
90
|
+
local _escaped_val
|
|
91
|
+
_escaped_val=$(_systemd_escape "$_raw_val")
|
|
92
|
+
_env_lines+="Environment=${_key}=${_escaped_val}"$'\n'
|
|
93
|
+
done <<<"$env_vars"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
_env_lines+="Environment=HOME=$(_systemd_escape "$HOME")"$'\n'
|
|
97
|
+
_env_lines+="Environment=PATH=$(_systemd_escape "$PATH")"$'\n'
|
|
98
|
+
printf '%s' "$_env_lines"
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Build inline cron environment assignments from newline-separated KEY=VALUE pairs.
|
|
103
|
+
_scheduler_cron_env_prefix() {
|
|
104
|
+
local env_vars="$1"
|
|
105
|
+
local _env_prefix=""
|
|
106
|
+
|
|
107
|
+
if [[ -n "$env_vars" ]]; then
|
|
108
|
+
while IFS= read -r _kv; do
|
|
109
|
+
[[ -z "$_kv" ]] && continue
|
|
110
|
+
local _key="${_kv%%=*}"
|
|
111
|
+
local _raw_val="${_kv#*=}"
|
|
112
|
+
local _escaped_val
|
|
113
|
+
_escaped_val=$(_cron_escape "$_raw_val")
|
|
114
|
+
_env_prefix+="${_key}=${_escaped_val} "
|
|
115
|
+
done <<<"$env_vars"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
printf '%s' "$_env_prefix"
|
|
119
|
+
return 0
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Install a generic scheduler via systemd user timer (Linux with systemd).
|
|
123
|
+
# Args:
|
|
124
|
+
# $1 = service_name (e.g. "aidevops-stats-wrapper")
|
|
125
|
+
# $2 = exec_command (shell command run via /bin/bash -lc)
|
|
126
|
+
# $3 = interval_sec (OnUnitActiveSec interval in seconds; may be empty for calendar-only)
|
|
127
|
+
# $4 = log_file (absolute path to log file)
|
|
128
|
+
# $5 = env_vars (newline-separated KEY=VALUE pairs, may be empty)
|
|
129
|
+
# $6 = run_at_load ("true" or "false")
|
|
130
|
+
# $7 = low_priority ("true" or "false")
|
|
131
|
+
# $8 = on_calendar (optional systemd OnCalendar spec)
|
|
132
|
+
# $9 = timeout_sec (optional TimeoutStartSec; defaults to interval_sec)
|
|
133
|
+
# Returns 0 on success, 1 if systemd enable fails (caller should fall back to cron).
|
|
134
|
+
_install_scheduler_systemd() {
|
|
135
|
+
local service_name="$1"
|
|
136
|
+
local exec_command="$2"
|
|
137
|
+
local interval_sec="$3"
|
|
138
|
+
local log_file="$4"
|
|
139
|
+
local env_vars="$5"
|
|
140
|
+
local run_at_load="$6"
|
|
141
|
+
local low_priority="$7"
|
|
142
|
+
local on_calendar="$8"
|
|
143
|
+
local timeout_sec="$9"
|
|
144
|
+
local service_dir="$HOME/.config/systemd/user"
|
|
145
|
+
local service_file="${service_dir}/${service_name}.service"
|
|
146
|
+
local timer_file="${service_dir}/${service_name}.timer"
|
|
147
|
+
|
|
148
|
+
mkdir -p "$service_dir"
|
|
149
|
+
|
|
150
|
+
# GH#18439 Bug 1: command substitution strips trailing newlines, which
|
|
151
|
+
# would run the final Environment=PATH=... into the following
|
|
152
|
+
# StandardOutput=... directive on the same line. Use a sentinel ('x')
|
|
153
|
+
# to preserve the trailing newline that _scheduler_systemd_env_lines
|
|
154
|
+
# always emits.
|
|
155
|
+
local _env_lines
|
|
156
|
+
_env_lines=$(
|
|
157
|
+
_scheduler_systemd_env_lines "$env_vars"
|
|
158
|
+
printf 'x'
|
|
159
|
+
)
|
|
160
|
+
_env_lines="${_env_lines%x}"
|
|
161
|
+
|
|
162
|
+
if [[ -z "$timeout_sec" ]]; then
|
|
163
|
+
timeout_sec="$interval_sec"
|
|
164
|
+
fi
|
|
165
|
+
if [[ -z "$timeout_sec" ]]; then
|
|
166
|
+
timeout_sec="3600"
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
local _service_extra=""
|
|
170
|
+
if [[ "$low_priority" == "true" ]]; then
|
|
171
|
+
_service_extra+="Nice=10"$'\n'
|
|
172
|
+
_service_extra+="IOSchedulingClass=idle"$'\n'
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
printf '%s' "[Unit]
|
|
176
|
+
Description=aidevops ${service_name}
|
|
177
|
+
After=network.target
|
|
178
|
+
|
|
179
|
+
[Service]
|
|
180
|
+
Type=oneshot
|
|
181
|
+
KillMode=process
|
|
182
|
+
ExecStart=/bin/bash -lc $(_systemd_escape "$exec_command")
|
|
183
|
+
TimeoutStartSec=${timeout_sec}
|
|
184
|
+
${_service_extra}${_env_lines}StandardOutput=append:${log_file}
|
|
185
|
+
StandardError=append:${log_file}
|
|
186
|
+
" >"$service_file"
|
|
187
|
+
|
|
188
|
+
local _timer_lines=""
|
|
189
|
+
if [[ "$run_at_load" == "true" ]]; then
|
|
190
|
+
_timer_lines+="OnActiveSec=10s"$'\n'
|
|
191
|
+
fi
|
|
192
|
+
if [[ -n "$interval_sec" ]]; then
|
|
193
|
+
_timer_lines+="OnBootSec=${interval_sec}"$'\n'
|
|
194
|
+
_timer_lines+="OnUnitActiveSec=${interval_sec}"$'\n'
|
|
195
|
+
fi
|
|
196
|
+
if [[ -n "$on_calendar" ]]; then
|
|
197
|
+
_timer_lines+="OnCalendar=${on_calendar}"$'\n'
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
printf '%s' "[Unit]
|
|
201
|
+
Description=aidevops ${service_name} Timer
|
|
202
|
+
|
|
203
|
+
[Timer]
|
|
204
|
+
${_timer_lines}Persistent=true
|
|
205
|
+
|
|
206
|
+
[Install]
|
|
207
|
+
WantedBy=timers.target
|
|
208
|
+
" >"$timer_file"
|
|
209
|
+
|
|
210
|
+
systemctl --user daemon-reload 2>/dev/null || true
|
|
211
|
+
if systemctl --user enable --now "${service_name}.timer" 2>/dev/null; then
|
|
212
|
+
return 0
|
|
213
|
+
fi
|
|
214
|
+
return 1
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# Install a generic cron entry.
|
|
218
|
+
# Args: $1=cron_tag, $2=cron_schedule, $3=exec_command, $4=log_file, $5=env_vars
|
|
219
|
+
_install_scheduler_cron() {
|
|
220
|
+
local cron_tag="$1"
|
|
221
|
+
local cron_schedule="$2"
|
|
222
|
+
local exec_command="$3"
|
|
223
|
+
local log_file="$4"
|
|
224
|
+
local env_vars="$5"
|
|
225
|
+
local _cron_exec
|
|
226
|
+
local _cron_log
|
|
227
|
+
local _env_prefix
|
|
228
|
+
|
|
229
|
+
_env_prefix=$(_scheduler_cron_env_prefix "$env_vars")
|
|
230
|
+
_cron_exec=$(_cron_escape "$exec_command")
|
|
231
|
+
_cron_log=$(_cron_escape "$log_file")
|
|
232
|
+
|
|
233
|
+
(
|
|
234
|
+
crontab -l 2>/dev/null | grep -vF "${cron_tag}" || true
|
|
235
|
+
echo "${cron_schedule} ${_env_prefix}/bin/bash -lc ${_cron_exec} >> ${_cron_log} 2>&1 # ${cron_tag}"
|
|
236
|
+
) | crontab - 2>/dev/null || true
|
|
237
|
+
return 0
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Dispatcher: install a scheduler on Linux, preferring systemd over cron.
|
|
241
|
+
# Args:
|
|
242
|
+
# $1 = service_name (systemd service name, e.g. "aidevops-stats-wrapper")
|
|
243
|
+
# $2 = cron_tag (comment tag for cron line, e.g. "aidevops: stats-wrapper")
|
|
244
|
+
# $3 = cron_schedule (cron schedule expression, e.g. "*/15 * * * *")
|
|
245
|
+
# $4 = exec_command (shell command run via /bin/bash -lc)
|
|
246
|
+
# $5 = interval_sec (systemd OnUnitActiveSec in seconds; may be empty for calendar-only)
|
|
247
|
+
# $6 = log_file (absolute path to log file)
|
|
248
|
+
# $7 = env_vars (newline-separated KEY=VALUE pairs for systemd/cron, may be empty)
|
|
249
|
+
# $8 = success_msg (message to print on success)
|
|
250
|
+
# $9 = fail_msg (message to print on failure)
|
|
251
|
+
# $10 = run_at_load ("true" or "false")
|
|
252
|
+
# $11 = low_priority ("true" or "false")
|
|
253
|
+
# $12 = on_calendar (optional systemd OnCalendar spec)
|
|
254
|
+
# $13 = timeout_sec (optional TimeoutStartSec)
|
|
255
|
+
# Returns 0 always (failures are warnings, not fatal).
|
|
256
|
+
_install_scheduler_linux() {
|
|
257
|
+
local service_name="$1"
|
|
258
|
+
local cron_tag="$2"
|
|
259
|
+
local cron_schedule="$3"
|
|
260
|
+
local exec_command="$4"
|
|
261
|
+
local interval_sec="$5"
|
|
262
|
+
local log_file="$6"
|
|
263
|
+
local env_vars="$7"
|
|
264
|
+
local success_msg="$8"
|
|
265
|
+
local fail_msg="$9"
|
|
266
|
+
local run_at_load="${10}"
|
|
267
|
+
local low_priority="${11}"
|
|
268
|
+
local on_calendar="${12:-}"
|
|
269
|
+
local timeout_sec="${13:-}"
|
|
270
|
+
|
|
271
|
+
if _systemd_user_available; then
|
|
272
|
+
if _install_scheduler_systemd \
|
|
273
|
+
"$service_name" \
|
|
274
|
+
"$exec_command" \
|
|
275
|
+
"$interval_sec" \
|
|
276
|
+
"$log_file" \
|
|
277
|
+
"$env_vars" \
|
|
278
|
+
"$run_at_load" \
|
|
279
|
+
"$low_priority" \
|
|
280
|
+
"$on_calendar" \
|
|
281
|
+
"$timeout_sec"; then
|
|
282
|
+
print_info "${success_msg} (systemd user timer)"
|
|
283
|
+
# After systemd install succeeds, remove any pre-existing cron entry
|
|
284
|
+
# to prevent dual-execution (GH#17695 Finding A)
|
|
285
|
+
if command -v crontab >/dev/null 2>&1; then
|
|
286
|
+
local current_cron
|
|
287
|
+
current_cron=$(crontab -l 2>/dev/null) || current_cron=""
|
|
288
|
+
if [[ -n "$current_cron" ]] && echo "$current_cron" | grep -qF "$cron_tag"; then
|
|
289
|
+
echo "$current_cron" | grep -vF "$cron_tag" | crontab -
|
|
290
|
+
echo "[schedulers] Removed pre-existing cron entry for $cron_tag (migrated to systemd)"
|
|
291
|
+
fi
|
|
292
|
+
fi
|
|
293
|
+
else
|
|
294
|
+
print_warning "systemd enable failed for ${service_name} — falling back to cron"
|
|
295
|
+
_install_scheduler_cron "$cron_tag" "$cron_schedule" "$exec_command" "$log_file" "$env_vars"
|
|
296
|
+
if crontab -l 2>/dev/null | grep -qF "${cron_tag}" 2>/dev/null; then
|
|
297
|
+
print_info "${success_msg} (cron fallback)"
|
|
298
|
+
else
|
|
299
|
+
print_warning "${fail_msg}"
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
else
|
|
303
|
+
_install_scheduler_cron "$cron_tag" "$cron_schedule" "$exec_command" "$log_file" "$env_vars"
|
|
304
|
+
if crontab -l 2>/dev/null | grep -qF "${cron_tag}" 2>/dev/null; then
|
|
305
|
+
print_info "${success_msg} (cron)"
|
|
306
|
+
else
|
|
307
|
+
print_warning "${fail_msg}"
|
|
308
|
+
fi
|
|
309
|
+
fi
|
|
310
|
+
return 0
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# Uninstall a scheduler across all backends (launchd/systemd/cron).
|
|
314
|
+
# Args:
|
|
315
|
+
# $1 = os (output of uname -s)
|
|
316
|
+
# $2 = launchd_label (e.g. "sh.aidevops.stats-wrapper")
|
|
317
|
+
# $3 = systemd_name (e.g. "aidevops-stats-wrapper")
|
|
318
|
+
# $4 = cron_tag (grep pattern for cron line, e.g. "aidevops: stats-wrapper")
|
|
319
|
+
# $5 = success_msg (message to print on removal)
|
|
320
|
+
# Returns 0 always.
|
|
321
|
+
_uninstall_scheduler() {
|
|
322
|
+
local _os="$1"
|
|
323
|
+
local launchd_label="$2"
|
|
324
|
+
local systemd_name="$3"
|
|
325
|
+
local cron_tag="$4"
|
|
326
|
+
local success_msg="$5"
|
|
327
|
+
|
|
328
|
+
if [[ "$_os" == "Darwin" ]]; then
|
|
329
|
+
local _plist="$HOME/Library/LaunchAgents/${launchd_label}.plist"
|
|
330
|
+
if _launchd_has_agent "$launchd_label"; then
|
|
331
|
+
launchctl unload "$_plist" 2>/dev/null || true
|
|
332
|
+
rm -f "$_plist"
|
|
333
|
+
print_info "${success_msg} (launchd agent removed)"
|
|
334
|
+
fi
|
|
335
|
+
else
|
|
336
|
+
# Check and remove from ALL backends sequentially, not just the first
|
|
337
|
+
# match. Prevents orphan entries when migrating between systemd and cron
|
|
338
|
+
# (GH#17695 Finding A).
|
|
339
|
+
if _systemd_user_available && systemctl --user is-enabled "${systemd_name}.timer" >/dev/null 2>&1; then
|
|
340
|
+
systemctl --user disable --now "${systemd_name}.timer" 2>/dev/null || true
|
|
341
|
+
rm -f "$HOME/.config/systemd/user/${systemd_name}.service"
|
|
342
|
+
rm -f "$HOME/.config/systemd/user/${systemd_name}.timer"
|
|
343
|
+
systemctl --user daemon-reload 2>/dev/null || true
|
|
344
|
+
print_info "${success_msg} (systemd timer removed)"
|
|
345
|
+
fi
|
|
346
|
+
if command -v crontab >/dev/null 2>&1; then
|
|
347
|
+
local current_cron
|
|
348
|
+
current_cron=$(crontab -l 2>/dev/null) || current_cron=""
|
|
349
|
+
if [[ -n "$current_cron" ]] && echo "$current_cron" | grep -qF "${cron_tag}"; then
|
|
350
|
+
echo "$current_cron" | grep -vF "${cron_tag}" | crontab - 2>/dev/null || true
|
|
351
|
+
print_info "${success_msg} (cron entry removed)"
|
|
352
|
+
fi
|
|
353
|
+
fi
|
|
354
|
+
fi
|
|
355
|
+
return 0
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# Uninstall supervisor pulse (user explicitly disabled)
|
|
359
|
+
_uninstall_pulse() {
|
|
360
|
+
local _os="$1"
|
|
361
|
+
local pulse_label="$2"
|
|
362
|
+
if [[ "$_os" == "Darwin" ]]; then
|
|
363
|
+
local pulse_plist="$HOME/Library/LaunchAgents/${pulse_label}.plist"
|
|
364
|
+
if _launchd_has_agent "$pulse_label"; then
|
|
365
|
+
launchctl unload "$pulse_plist" || true
|
|
366
|
+
rm -f "$pulse_plist"
|
|
367
|
+
pkill -f 'Supervisor Pulse' 2>/dev/null || true
|
|
368
|
+
print_info "Supervisor pulse disabled (launchd agent removed per config)"
|
|
369
|
+
fi
|
|
370
|
+
elif _systemd_user_available; then
|
|
371
|
+
local service_name="aidevops-supervisor-pulse"
|
|
372
|
+
if systemctl --user is-enabled "${service_name}.timer" >/dev/null 2>&1; then
|
|
373
|
+
systemctl --user disable --now "${service_name}.timer" 2>/dev/null || true
|
|
374
|
+
rm -f "$HOME/.config/systemd/user/${service_name}.service"
|
|
375
|
+
rm -f "$HOME/.config/systemd/user/${service_name}.timer"
|
|
376
|
+
systemctl --user daemon-reload 2>/dev/null || true
|
|
377
|
+
print_info "Supervisor pulse disabled (systemd timer removed per config)"
|
|
378
|
+
fi
|
|
379
|
+
else
|
|
380
|
+
if crontab -l 2>/dev/null | grep -qF "pulse-wrapper"; then
|
|
381
|
+
crontab -l 2>/dev/null | grep -v 'aidevops: supervisor-pulse' | crontab - || true
|
|
382
|
+
print_info "Supervisor pulse disabled (cron entry removed per config)"
|
|
383
|
+
fi
|
|
384
|
+
fi
|
|
385
|
+
return 0
|
|
386
|
+
}
|