Skip to content

Commit 4c4c62a

Browse files
committed
refactor: unify service scripts and harden publisher units [no ci]
1 parent ae4d325 commit 4c4c62a

9 files changed

Lines changed: 211 additions & 102 deletions

install.sh

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@
33
set -euo pipefail
44

55
INSTALL_DIR="/opt/relaysms/relaysms-publisher"
6-
SERVICE_NAME="relaysms-publisher"
76
REPO_URL="https://github.com/smswithoutborders/RelaySMS-Publisher.git"
87
BRANCH="${BRANCH:-main}"
98
CARGO_BIN="$HOME/.cargo/bin"
109
DEPS_MARKER="/var/lib/relaysms-publisher-deps-installed"
1110

12-
# Single source of truth for the unit set. Add/rename a service here and it
13-
# propagates to install, enable/start, and restart — no need to touch it
14-
# in more than one place.
1511
TARGET_UNIT="relaysms-publisher.target"
1612
SERVICE_UNITS=(
1713
relaysms-publisher-rest.service
@@ -21,12 +17,8 @@ SERVICE_UNITS=(
2117
)
2218
ALL_UNITS=("$TARGET_UNIT" "${SERVICE_UNITS[@]}")
2319

24-
# Prefer the invoking user (SUDO_USER) as the service owner so no extra
25-
# system account is needed; fall back to a dedicated 'relaysms' user when
26-
# run directly as root. Note this only affects ownership of the app's
27-
# runtime files/services — the build itself (Rust, venv, `make build-setup`)
28-
# always runs as root/root's $HOME, since the script isn't re-exec'd with
29-
# `sudo -E`.
20+
# Runtime files are owned by the invoking user if run via sudo, otherwise
21+
# a dedicated 'relaysms' user. The build itself still runs as root.
3022
if [ -n "${SUDO_USER:-}" ] && id "$SUDO_USER" &>/dev/null; then
3123
SERVICE_USER="$SUDO_USER"
3224
else
@@ -38,18 +30,14 @@ error() {
3830
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
3931
exit 1
4032
}
41-
# Safety net for failures not already wrapped in `|| error "..."` (e.g. a
42-
# bare apt-get/git call). Explicit exit calls from error() don't re-trigger
43-
# this trap, so messages are never duplicated.
33+
# Catches failures not already wrapped in error(), with line context.
4434
on_err() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: aborted at line $1 (last command: $2)" >&2; }
4535
trap 'on_err "$LINENO" "$BASH_COMMAND"' ERR
4636

4737
check_root() { [ "$EUID" -eq 0 ] || error "Run with sudo"; }
4838

49-
# Reads a single KEY=value from an env file without sourcing/evaluating it
50-
# (avoids executing arbitrary shell in a file we don't fully control).
51-
# Tolerates an `export ` prefix and quoted values. The `|| true` on the
52-
# grep keeps a no-match result from tripping `pipefail`.
39+
# Reads KEY=value from a file without sourcing it. `|| true` on the grep
40+
# stops a no-match from tripping pipefail.
5341
read_env_var() {
5442
local key="$1" file="$2" val
5543
val=$( (grep -E "^(export[[:space:]]+)?${key}[[:space:]]*=" "$file" 2>/dev/null || true) |
@@ -61,10 +49,7 @@ read_env_var() {
6149
echo "$val"
6250
}
6351

64-
# Appends a directory to the global RW_DIRS array (resolving it relative to
65-
# INSTALL_DIR if not absolute), de-duplicating as it goes. Defined once at
66-
# top level — resolve_app_directories is called twice per run (directory
67-
# creation, then systemd ReadWritePaths), and both must see the same set.
52+
# Adds a dir to the global RW_DIRS array, de-duped, relative to INSTALL_DIR.
6853
_resolve_dir() {
6954
local dir="$1"
7055
[ -z "$dir" ] && return
@@ -120,9 +105,7 @@ setup_service_user() {
120105

121106
clone_repository() {
122107
log "Cloning repository"
123-
# `-c url....insteadOf` is scoped to this git invocation only, so we
124-
# don't permanently rewrite the invoking user's global git config just
125-
# to fetch this one repo over HTTPS instead of SSH.
108+
# -c scopes the SSH-to-HTTPS rewrite to this command, not global git config.
126109
if [ -d "$INSTALL_DIR/.git" ]; then
127110
log "Repository exists, updating"
128111
cd "$INSTALL_DIR"
@@ -184,9 +167,8 @@ setup_env() {
184167
log "Edit $INSTALL_DIR/.env before starting services"
185168
}
186169

187-
# Shared by create_app_directories and install_services so the directories
188-
# that get created and the paths granted to systemd's ReadWritePaths can
189-
# never drift apart from what .env actually configures.
170+
# Shared by create_app_directories and install_services so both stay in
171+
# sync with .env.
190172
resolve_app_directories() {
191173
local envfile="$INSTALL_DIR/.env"
192174
[ -f "$envfile" ] || error ".env not found"
@@ -256,7 +238,7 @@ install_services() {
256238
local svc
257239
for svc in "${ALL_UNITS[@]}"; do
258240
[ -f "$svc" ] || error "Service file not found: $svc"
259-
# '#' delimiter (not '|') since a resolved path could contain a pipe.
241+
# # as delimiter, a resolved path could contain a pipe.
260242
sed \
261243
-e "s/User=relaysms/User=$SERVICE_USER/" \
262244
-e "s#__RW_PATHS__#$rw_paths#" \

manage.sh

Lines changed: 122 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,144 @@
11
#!/bin/bash
22

3-
SERVICE_NAME="relaysms-publisher.target"
3+
set -euo pipefail
4+
45
INSTALL_DIR="/opt/relaysms/relaysms-publisher"
6+
CARGO_BIN="$HOME/.cargo/bin"
7+
8+
TARGET_UNIT="relaysms-publisher.target"
9+
SERVICE_UNITS=(
10+
relaysms-publisher-rest.service
11+
relaysms-publisher-grpc.service
12+
relaysms-publisher-worker.service
13+
relaysms-publisher-beat.service
14+
)
15+
ALL_UNITS=("$TARGET_UNIT" "${SERVICE_UNITS[@]}")
516

6-
check_sudo() {
7-
[ "$EUID" -eq 0 ] || {
8-
echo "Requires sudo"
9-
exit 1
10-
}
17+
log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"; }
18+
error() {
19+
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
20+
exit 1
1121
}
1222

13-
case "$1" in
14-
start)
23+
check_sudo() { [ "$EUID" -eq 0 ] || error "Run with sudo"; }
24+
25+
cmd_start() {
1526
check_sudo
16-
systemctl start $SERVICE_NAME
17-
echo "Services started"
18-
;;
19-
stop)
27+
systemctl start "$TARGET_UNIT"
28+
log "Services started"
29+
}
30+
31+
cmd_stop() {
2032
check_sudo
21-
systemctl stop $SERVICE_NAME
22-
echo "Services stopped"
23-
;;
24-
restart)
33+
local svc
34+
for svc in "${SERVICE_UNITS[@]}"; do
35+
systemctl stop "$svc"
36+
done
37+
systemctl stop "$TARGET_UNIT"
38+
log "Services stopped"
39+
}
40+
41+
cmd_restart() {
2542
check_sudo
26-
systemctl restart $SERVICE_NAME
27-
echo "Services restarted"
28-
;;
29-
status)
30-
systemctl status relaysms-publisher-rest relaysms-publisher-grpc relaysms-publisher-worker relaysms-publisher-beat
31-
;;
32-
logs)
33-
journalctl -u relaysms-publisher-rest -u relaysms-publisher-grpc -u relaysms-publisher-worker -u relaysms-publisher-beat -f
34-
;;
35-
enable)
43+
local svc
44+
for svc in "${SERVICE_UNITS[@]}"; do
45+
systemctl restart "$svc"
46+
done
47+
log "Services restarted"
48+
}
49+
50+
cmd_status() {
51+
systemctl status "${SERVICE_UNITS[@]}" || true
52+
}
53+
54+
cmd_logs() {
55+
local args=() svc
56+
for svc in "${SERVICE_UNITS[@]}"; do
57+
args+=(-u "$svc")
58+
done
59+
journalctl "${args[@]}" -f || true
60+
}
61+
62+
cmd_enable() {
3663
check_sudo
37-
systemctl enable $SERVICE_NAME
38-
echo "Services enabled on boot"
39-
;;
40-
disable)
64+
systemctl enable "$TARGET_UNIT"
65+
log "Services enabled on boot"
66+
}
67+
68+
cmd_disable() {
4169
check_sudo
42-
systemctl disable $SERVICE_NAME
43-
echo "Services disabled on boot"
44-
;;
45-
update)
70+
systemctl disable "$TARGET_UNIT"
71+
log "Services disabled on boot"
72+
}
73+
74+
cmd_update() {
4675
check_sudo
47-
export PATH="$HOME/.cargo/bin:$PATH"
48-
systemctl stop $SERVICE_NAME
76+
local svc
77+
for svc in "${SERVICE_UNITS[@]}"; do
78+
systemctl stop "$svc"
79+
done
80+
4981
cd "$INSTALL_DIR"
5082
git pull
5183
git submodule update --init --recursive --remote --merge
52-
source venv/bin/activate
53-
pip install --quiet --upgrade pip
54-
pip install --quiet -r requirements.txt
84+
85+
venv/bin/pip install --quiet --upgrade pip
86+
venv/bin/pip install --quiet -r requirements.txt
87+
88+
export PATH="$CARGO_BIN:$INSTALL_DIR/venv/bin:$PATH"
5589
make build-setup
90+
5691
systemctl daemon-reload
57-
systemctl start $SERVICE_NAME
58-
echo "Update complete"
59-
;;
60-
uninstall)
92+
for svc in "${SERVICE_UNITS[@]}"; do
93+
systemctl restart "$svc"
94+
done
95+
systemctl start "$TARGET_UNIT"
96+
log "Update complete"
97+
}
98+
99+
cmd_uninstall() {
61100
check_sudo
62-
read -p "Remove all services and data? (yes/no): " confirm
63-
[ "$confirm" != "yes" ] && echo "Cancelled" && exit 0
64-
systemctl stop $SERVICE_NAME 2>/dev/null || true
65-
systemctl disable $SERVICE_NAME 2>/dev/null || true
66-
rm -f /etc/systemd/system/relaysms-publisher*.{service,target}
101+
local confirm
102+
read -r -p "Remove all services and data? (yes/no): " confirm || confirm="no"
103+
if [ "$confirm" != "yes" ]; then
104+
log "Cancelled"
105+
return 0
106+
fi
107+
108+
local unit
109+
for unit in "${SERVICE_UNITS[@]}"; do
110+
systemctl stop "$unit" 2>/dev/null || true
111+
done
112+
systemctl stop "$TARGET_UNIT" 2>/dev/null || true
113+
systemctl disable "$TARGET_UNIT" 2>/dev/null || true
114+
115+
for unit in "${ALL_UNITS[@]}"; do
116+
rm -f "/etc/systemd/system/$unit"
117+
done
67118
systemctl daemon-reload
119+
68120
rm -rf "$INSTALL_DIR"
69-
echo "Uninstall complete"
70-
;;
71-
*)
121+
log "Uninstall complete"
122+
}
123+
124+
usage() {
72125
echo "Usage: $0 {start|stop|restart|status|logs|enable|disable|update|uninstall}"
73126
exit 1
74-
;;
75-
esac
127+
}
128+
129+
main() {
130+
case "${1:-}" in
131+
start) cmd_start ;;
132+
stop) cmd_stop ;;
133+
restart) cmd_restart ;;
134+
status) cmd_status ;;
135+
logs) cmd_logs ;;
136+
enable) cmd_enable ;;
137+
disable) cmd_disable ;;
138+
update) cmd_update ;;
139+
uninstall) cmd_uninstall ;;
140+
*) usage ;;
141+
esac
142+
}
143+
144+
main "$@"

publications.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,25 @@ def _publish_online_content(
231231
)
232232
raise AdapterIntegrationError("Failed to send message via adapter.")
233233

234-
mark_token_hash_used(token_hash_obj, self.session)
235-
logger.info("Published message for token %d via %r.", token_id, token.platform)
234+
result = pipe.get("result") or {}
236235

236+
# Refresh may have happened even if the send itself failed downstream
237+
# (e.g. token refreshed, then the HTTP call or attachment step failed).
238+
# Persist it regardless of outcome so the next attempt isn't stale.
237239
if proto_id == rrs.V1PayloadsSupportedProtocols.O_AUTH20:
238-
self._maybe_refresh_token(token, pipe.get("result", {}))
240+
self._maybe_refresh_token(token, result)
241+
242+
if not result.get("success", True):
243+
logger.error(
244+
"Adapter %r failed for token %d: %s",
245+
token.platform,
246+
token_id,
247+
result.get("message"),
248+
)
249+
raise AdapterIntegrationError("Failed to send message via adapter.")
250+
251+
mark_token_hash_used(token_hash_obj, self.session)
252+
logger.info("Published message for token %d via %r.", token_id, token.platform)
239253

240254
def _maybe_refresh_token(self, token, result: dict) -> None:
241255
refreshed_token = result.get("refreshed_token") or {}

relaysms-publisher-beat.service

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
[Unit]
22
Description=RelaySMS Publisher Celery Beat
33
PartOf=relaysms-publisher.target
4-
After=network.target
4+
After=network-online.target
5+
Wants=network-online.target
6+
StartLimitIntervalSec=60
7+
StartLimitBurst=5
58

69
[Service]
10+
Type=exec
711
User=relaysms
812
WorkingDirectory=/opt/relaysms/relaysms-publisher
913
EnvironmentFile=/opt/relaysms/relaysms-publisher/.env
1014
ExecStart=/opt/relaysms/relaysms-publisher/venv/bin/celery \
1115
-A tasks.celery_app:celery_app beat \
1216
--loglevel=info
1317
Restart=on-failure
18+
RestartSec=5
19+
TimeoutStopSec=10
1420
SyslogIdentifier=relaysms-publisher-beat
15-
1621
PrivateTmp=true
1722
ProtectSystem=strict
1823
ProtectHome=true
24+
ProtectKernelTunables=true
25+
ProtectKernelModules=true
26+
ProtectControlGroups=true
27+
RestrictSUIDSGID=true
28+
RestrictNamespaces=true
1929
NoNewPrivileges=true
2030
ReadWritePaths=__RW_PATHS__
2131

relaysms-publisher-grpc.service

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
[Unit]
22
Description=RelaySMS Publisher gRPC Server
33
PartOf=relaysms-publisher.target
4-
After=network.target
4+
After=network-online.target
5+
Wants=network-online.target
6+
StartLimitIntervalSec=60
7+
StartLimitBurst=5
58

69
[Service]
10+
Type=exec
711
User=relaysms
812
WorkingDirectory=/opt/relaysms/relaysms-publisher
913
EnvironmentFile=/opt/relaysms/relaysms-publisher/.env
1014
ExecStart=/opt/relaysms/relaysms-publisher/venv/bin/python3 -u grpc_server.py
1115
Restart=on-failure
16+
RestartSec=5
1217
SyslogIdentifier=relaysms-publisher-grpc
13-
1418
PrivateTmp=true
1519
ProtectSystem=strict
1620
ProtectHome=true
21+
ProtectKernelTunables=true
22+
ProtectKernelModules=true
23+
ProtectControlGroups=true
24+
RestrictSUIDSGID=true
25+
RestrictNamespaces=true
1726
NoNewPrivileges=true
1827
ReadWritePaths=__RW_PATHS__
1928

0 commit comments

Comments
 (0)