service#
Start, stop, restart, status, logs and readiness for a repository's own services, through
docker compose or as native processes. A repository sets plain variables (normally in
scripts/project.sh) and calls the svc_* functions from its ./dev verbs.
source scripts/script-helpers/helpers.sh
shlib_import logging service
SVC_BACKEND=proc
SVC_PROCS=("api:python3 -m http.server 8000" "worker:./bin/worker --queue default")
SVC_PORTS=(8000)
SVC_HEALTH_URL="http://127.0.0.1:8000/"
SVC_URLS=("API http://localhost:8000/")
svc_start # or svc_stop, svc_restart, svc_status, svc_logs api
Configuration#
| Variable | Backend | Meaning |
|---|---|---|
SVC_BACKEND |
both | compose or proc. Unset or anything else: exit 2. |
SVC_COMPOSE_FILES |
compose | Array of compose files, passed as -f each. |
SVC_COMPOSE_PROJECT |
compose | Project name, passed as -p. Optional. |
SVC_PROCS |
proc | Array of name:command. Names: letters, digits, _ . -, no leading dot, unique. The command runs under bash -c, so quotes, && and & work. |
SVC_PORTS |
both | TCP ports the services listen on, each 8000 or NAME:8000, where NAME is the setting that holds it (BACKEND_PORT:8000) and the commands read it (--port "$BACKEND_PORT"). A taken port: on a terminal, a NAME:port entry is asked about (port_choose, Enter takes a free port); the answer is exported as NAME for this start, SVC_HEALTH_URL and SVC_URLS follow it, and you are asked whether to save it in the repository's .env (Enter saves, through env_set_value), so the next status, stop and start use it too. Without a terminal, or for a bare port, the start is refused with the owner, a free port and what to change. NAME may not be a variable the shell or loader reads (PATH, HOME, LD_*, SVC_* ...). |
SVC_HEALTH_URL |
both | URL that answers with a status below 400 once ready (curl -f). Unset: no readiness wait. |
SVC_URLS |
both | Array of "Label url", printed as Label: url after a start. |
SVC_STOP_MODE |
compose | stop keeps containers (compose stop). Default: compose down. |
SVC_STOP_TIMEOUT |
both | Seconds between TERM and KILL. Default 10. |
SVC_READY_TIMEOUT |
both | Seconds svc_wait_ready waits. Default 180. |
SVC_ROOT |
both | Repository root. Default: git rev-parse --show-toplevel, else $PWD. |
SVC_LOGS_FOLLOW |
both | 0 makes svc_logs print the last 100 lines instead of following. |
Run directory: .run/ at the repository root holds <name>.pid, <name>.started (a
start-time fingerprint of the pid) and <name>.log. Add .run/ to .gitignore.
Exit codes: 0 ok or nothing to do, 1 failure (not ready, port taken, docker missing, a
process that would not stop), 2 usage (bad SVC_* configuration or argument).
Functions#
- svc_start [--build]
- Purpose: start the services, wait for
SVC_HEALTH_URL, printSVC_URLS. - compose:
check_docker, thencompose up -d [--build]fromSVC_ROOT. Ports are checked only when the stack is down (compose ps -qis empty). - proc: each process not already running is detached into its own session and process group, stdout and stderr appended to
.run/<name>.log. One already running is reported and left alone; all running exits 0. Ports are checked only when none of the processes is running. Without a health URL, a process that exits within a second fails the start with its log tail. -
Returns: 0 started or already running; 1 port taken (the message names the owner and the setting to change), docker unavailable,
curlmissing withSVC_HEALTH_URLset (checked before anything starts), not ready; 2 usage. -
svc_stop
- Purpose: stop the services.
- compose:
compose down --timeout SVC_STOP_TIMEOUT, orcompose stop --timeout ...withSVC_STOP_MODE=stop. - proc: TERM to the whole process group (children included), wait
SVC_STOP_TIMEOUT, then KILL; remove the pidfile. -
Returns: 0 stopped or nothing running; 1 a group survived KILL; 2 usage.
-
svc_restart [--build]
-
Purpose:
svc_stop, thensvc_startwith the same arguments. -
svc_status
- Purpose: what runs, which ports listen (and who owns them), whether health answers.
- compose:
compose ps. proc: one line per process,name: running (pid N)orname: stopped. -
Returns: 0 when everything is up and healthy; 1 otherwise; 2 usage.
-
svc_logs [name]
- Purpose: follow the logs of one service, or of all.
- compose:
compose logs -f [name]. proc:tail -fon.run/<name>.log(all logs without a name). -
Returns: 2 for a name not in
SVC_PROCS(proc backend). -
svc_wait_ready
- Purpose: poll
SVC_HEALTH_URLonce a second until it answers orSVC_READY_TIMEOUTruns out. - proc: stops early, with the same log tail, when every process has exited.
- Returns: 0 ready, or no health URL set; 1 on timeout, after printing the last 40 log lines (
compose logs --tail 40, or the tail of each.run/*.log).
Pidfiles#
A pidfile is trusted only while its process group is alive and, when the leader is alive,
its start time matches <name>.started. Anything else is a stale pidfile: it is removed,
reported, and nothing is signalled. So a pid reused by an unrelated process after a reboot
is never killed. Where neither /proc nor ps -o lstart works the fingerprint is empty and
only the liveness check applies.
Detaching (macOS and Linux)#
macOS has no setsid binary. Processes are detached with perl -MPOSIX (POSIX::setsid),
which stock macOS ships; perl is preferred on Linux too, because it also restores SIGINT and
SIGQUIT, which bash leaves ignored in a background job. Without perl, setsid (util-linux
or BusyBox) is used. With neither, start fails with exit 1. The leader writes its own pid,
so this works whether or not the caller has job control.
Dependencies#
- Modules:
logging,docker(get_docker_compose_cmd,check_docker),ports(port_in_use_by). perlorsetsid(proc),curl(health checks),docker composeordocker-compose(compose).