build(client): use locked uv environment in launcher and Docker

This commit is contained in:
2026-10-06 18:27:09 -03:00
parent f8d5b7857a
commit fa00b36e49
16 changed files with 230 additions and 53 deletions
+5
View File
@@ -17,3 +17,8 @@
**/uploads/
**/sessions/
debug_files/
tmp/
**/.mypy_cache/
**/.pytest_cache/
**/.ruff_cache/
**/*.egg-info/
+4 -3
View File
@@ -14,7 +14,7 @@ documentation, source identifiers, comments, and internal instructions use Engli
Start with [client setup and usage](docs/client/usage.md), then read the
[architecture](docs/client/architecture.md) and [documentation index](docs/README.md).
The speech backend needs its own installed model and runtime dependencies;
`s2s-client/requirements.txt` installs only the UI server dependencies.
The uv project in `s2s-client/` installs only the UI server dependencies.
## Run the browser client
@@ -25,13 +25,13 @@ S2S_PROXY_TARGET=ws://localhost:8765/v1/realtime ./start_ui.sh
```
Open <http://localhost:8420/> and allow microphone access. The launcher creates
`.venv`, installs missing Python requirements and locked frontend dependencies,
`s2s-client/.venv`, syncs locked Python and frontend dependencies,
and loads the root `.env` if present. Vite listens on `UI_PORT` (default `8420`)
and forwards HTTP/WebSocket backend routes to FastAPI on `127.0.0.1:8421`.
Both servers reload during development; stopping the launcher stops both.
Leave `SPEECH_TO_SPEECH_URL` unset to use the same-origin speech proxy.
The launcher requires Python 3 with venv support, Node.js 22.18+ on the 22.x
The launcher requires uv 0.12.5 (which provisions Python 3.12), Node.js 22.18+ on the 22.x
line or Node.js 24+, npm, Bash 4.4+, `setsid`, and `curl`. For staging hostnames,
set `S2S_DEV_ALLOWED_HOSTS` and forward the Vite HMR WebSocket through the existing
HTTPS reverse proxy. See [development setup](s2s-frontend/README.md).
@@ -52,6 +52,7 @@ proxy deployment, supply a backend address reachable from the container instead.
## Project references
- [Change history](s2s-client/CHANGELOG.md)
- [Backend development and compatibility](docs/client/development.md)
- [Realtime protocol and pipeline](docs/speech/realtime-api.md)
- [Context injection](docs/client/context-injection.md)
- [Tool calling](docs/client/tool-calling.md)
+1
View File
@@ -8,6 +8,7 @@ do not define the current implementation.
- [Usage and configuration](client/usage.md)
- [Architecture and source map](client/architecture.md)
- [Backend development and compatibility](client/development.md)
- [Reasoning modes](client/reasoning.md)
- [Context injection](client/context-injection.md)
- [Tool calling](client/tool-calling.md)
+12 -1
View File
@@ -42,7 +42,14 @@ audio. It is not an unconditional byte-for-byte tunnel.
| Source | Responsibility |
|---|---|
| [server.py](../../s2s-client/server.py) | Configuration, documentation serving, WebSocket relay, context loading, ApiPilot, reasoning, search and queue routes |
| [server.py](../../s2s-client/server.py) | Compatibility entry point for existing `server:app` commands |
| [main.py](../../s2s-client/s2s_client/main.py) | FastAPI assembly, documentation serving and redirects |
| [api](../../s2s-client/s2s_client/api) | HTTP route registration, handlers and compatible response validation |
| [models](../../s2s-client/s2s_client/models) | Pydantic request and response contracts |
| [core](../../s2s-client/s2s_client/core) | Existing environment parsing, logging, URL helpers and diagnostics |
| [providers](../../s2s-client/s2s_client/providers) | Caller context and ApiPilot HTTP integrations |
| [services](../../s2s-client/s2s_client/services) | Context construction, reasoning and tool-context rendering |
| [realtime](../../s2s-client/s2s_client/realtime) | WebSocket relay, events, markers and audio handling |
| [start_ui.sh](../../start_ui.sh) | Dependency setup, ports, readiness, reload processes, and shutdown |
| [vite.config.ts](../../s2s-frontend/vite.config.ts) | Public development endpoint, HMR, allowed hosts, and backend routing |
| [App.tsx](../../s2s-frontend/src/App.tsx) | React layout, voice controls, dialogs, profile and reports sidebar |
@@ -75,6 +82,10 @@ source text, and directories without `index.html` are not browsable pages.
FastAPI serves backend and documentation routes only. The frontend source and
assets live in `s2s-frontend` and are served by Vite.
The uv project and its Python tools are scoped to `s2s-client/`. Response models
describe service-owned structure and retain loose upstream values. See
[development and compatibility](development.md) before tightening a contract.
## Audio path
1. The browser captures microphone audio with echo cancellation, noise
+2 -1
View File
@@ -2,7 +2,8 @@
Context reaches the assistant through browser-built instructions, server-patched
realtime session instructions, and HTTP reasoning context. The implementation is
in [server.py](../../s2s-client/server.py),
in the server's [context services](../../s2s-client/s2s_client/services/context.py),
[realtime proxy](../../s2s-client/s2s_client/realtime/proxy.py),
[context.ts](../../s2s-frontend/src/lib/context.ts), and the
[WebSocket client](../../s2s-frontend/src/realtime/client.ts).
+77
View File
@@ -0,0 +1,77 @@
# Backend development and compatibility
The FastAPI project lives entirely in `s2s-client/`. It uses uv 0.12.5, Python
3.12, and a committed `uv.lock`. The frontend and speech pipeline keep their
own tooling. Runtime dependency versions match the environment used to capture
the pre-migration server; framework upgrades require a separate compatibility
review.
## Commands
From the repository root:
```bash
make -C s2s-client install # uv sync --locked, including development tools
make -C s2s-client format # Ruff safe fixes and formatting
make -C s2s-client check # lock freshness, format, lint, mypy, pytest
make -C s2s-client test-launcher # Bash supervisor integration fixtures
make -C s2s-client test-image # Docker build and isolated runtime smoke test
```
Run `uv lock` inside `s2s-client/` when intentionally changing dependencies.
Do not install server packages into a shared root environment. The launcher
syncs `s2s-client/.venv` and retains root `.env` loading, port defaults,
readiness checks, and process-group shutdown. Offline starts require the
locked packages and Python runtime to have been installed or cached already.
Optional hooks use `make -C s2s-client hooks-install`; the configuration stays
in `s2s-client/.pre-commit-config.yaml`. `make -C s2s-client hooks-run` runs the
same hooks without installing them. Hooks and Python quality checks apply
only to the client service. The server entry points are `s2s_client.main:app`
and the retained `server:app` shim; use `--app-dir s2s-client` from the root.
## Schema boundaries
`models/requests.py` retains the existing search, reasoning, and ApiPilot
request models, including their defaults, coercion, and ignored extra fields.
The queue beacon still accepts malformed JSON and non-object bodies as before;
its Pydantic view is applied only after the original object check, and
`queueId` remains unconstrained.
`models/responses.py` validates structure constructed by this server. Fields
owned by external services remain `Any` where the old parser imposed no type
constraint. Object models allow extra fields. Caller profiles can be arbitrary
JSON; load-balancer fields remain optional; execution results and error details
remain open. Existing provider parsing, authentication precedence, argument
filtering, environment parsing, and WebSocket frames are unchanged.
HTTP handlers call `validated_json_response`: Pydantic validates the payload,
then `JSONResponse` serializes the original object. Returning a dumped model
would insert defaults or change values/key order, so do not replace this helper
with model serialization without a contract review. Route `response_model`
declarations publish the new response schemas in OpenAPI. Existing request
schemas and validation errors remain unchanged. HTTP exceptions retain their
existing error shapes; there is no new common error envelope.
## Compatibility evidence
`tests/fixtures/legacy-contracts.json` was captured from the original monolithic
server at commit `3d42df5`, before refactoring. Tests compare 86 HTTP scenarios,
including exact response text, statuses, selected headers, and outgoing mock
HTTP requests. They also compare request OpenAPI definitions and representative
realtime frames. Additional tests cover response validation and permissive
upstream values; the original local HTTP/WebSocket tests remain in the suite.
The captures deliberately preserve unknown fields, omitted fields, unusual JSON
values, queue beacon edge cases, permissive tool arguments, and known failures.
Examples include reasoning's existing uninitialized local variables in certain
configuration combinations, API-key-only tool precheck behavior, the catalog
cache's existing key, and relay termination after a suppressed invalid
`session.update` error. Fix those in separately reviewed behavior changes;
do not regenerate golden captures to make a refactor pass.
These checks use local fixtures and do not claim exhaustive coverage of all
provider responses, live speech models, or production configuration. No deployed
services or real credentials are needed. The Docker check verifies the actual
runtime image, API configuration, documentation, OpenAPI, and the backend's
existing root 404; it removes its temporary container and image afterward.
+1 -1
View File
@@ -99,7 +99,7 @@ realtime backend instead.
|---|---|
| Change the base agent prompt | `S2S_PRIMARY_PROMPT` or browser settings when unpinned |
| Change browser context assembly | `buildInstructions()` in `s2s-frontend/src/lib/context.ts` |
| Change HTTP provider messages | `reasoning()` in `server.py` |
| Change HTTP provider messages | `reasoning()` in `s2s-client/s2s_client/services/reasoning.py` |
| Change external-answer rendering | `VoiceController.reason()` and `RealtimeClient.renderExternalAnswer()` |
| Change Node-RED processing | `s2s-reasoning-engine/flows.json` |
| Change native model/tool generation | `speech_to_speech/LLM` and realtime handlers |
+5 -4
View File
@@ -35,9 +35,10 @@ launcher. If the page loads but edits do not appear, check the browser's
`/__vite_hmr` WebSocket and the reverse proxy's upgrade handling. Speech uses
the separate `/v1/realtime` WebSocket on the same public origin.
For a startup dependency error, check Node/npm on the process manager's PATH,
Python venv support, and `setsid`/`curl`. The first start installs dependencies;
later starts reuse them until frontend manifests change. Changes to root
For a startup dependency error, check uv 0.12.5 and Node/npm on the process
manager's PATH, plus `setsid`/`curl`. uv manages Python 3.12 in
`s2s-client/.venv` and checks the committed lock on every start. Frontend
dependencies are reused until their manifests change. Changes to root
`.env` or launcher settings require a restart.
## The assistant promises a tool but nothing happens
@@ -126,7 +127,7 @@ From the repository root, run the frontend checks and local backend fixtures:
```bash
npm --prefix s2s-frontend run check
npm --prefix s2s-frontend run build
.venv/bin/python -m unittest discover -s s2s-client/tests -v
make -C s2s-client check
```
These checks cover frontend contracts and local session/queue/WebSocket
+9 -6
View File
@@ -12,8 +12,8 @@ From the repository root:
S2S_PROXY_TARGET=ws://localhost:8765/v1/realtime ./start_ui.sh
```
The launcher loads the root `.env`, creates `.venv` if needed, installs the
client requirements when the dependency check fails, and installs the locked
The launcher loads the root `.env`, syncs `s2s-client/.venv` from the committed
`s2s-client/uv.lock`, and installs the locked
frontend dependencies when needed. Vite serves the React frontend on port
8420 and proxies the backend routes to Uvicorn on `127.0.0.1:8421`.
`UI_PORT` overrides the public port; `S2S_BACKEND_PORT` overrides the internal
@@ -21,7 +21,7 @@ port. Both servers reload source edits, and stopping the launcher stops both.
The speech pipeline remains a separate service. Because `.env` is sourced by
the script, values in that file can replace variables supplied on the command line.
Use Node.js 22.18+ on the 22.x line or Node.js 24+, npm, Python 3 with venv,
Use Node.js 22.18+ on the 22.x line or Node.js 24+, npm, uv 0.12.5,
Bash 4.4+, `setsid`, and `curl`. Remote staging access also needs the exact
hostname in `S2S_DEV_ALLOWED_HOSTS`, HTTPS, and WebSocket forwarding for speech
and Vite HMR. See [frontend development setup](../../s2s-frontend/README.md).
@@ -29,15 +29,18 @@ and Vite HMR. See [frontend development setup](../../s2s-frontend/README.md).
To install and start the backend manually:
```bash
python3 -m venv .venv
.venv/bin/python -m pip install -r s2s-client/requirements.txt
uv sync --project s2s-client --locked
S2S_PROXY_TARGET=ws://localhost:8765/v1/realtime \
.venv/bin/python -m uvicorn --app-dir s2s-client server:app \
uv run --project s2s-client --locked uvicorn --app-dir s2s-client s2s_client.main:app \
--host 127.0.0.1 --port 8421 --reload --reload-dir s2s-client
```
Then, in a second terminal, run `cd s2s-frontend`, `npm ci`, and `npm run dev`.
uv provisions Python 3.12 when needed. The Python environment belongs only to
`s2s-client`; an existing root `.venv` for speech dependencies is left alone.
See [development and compatibility](development.md) for checks and schema rules.
Open <http://localhost:8420/>, tap the orb/microphone control, grant microphone
access, and speak. Microphone access requires HTTPS or a local browser origin;
use HTTPS when accessing the UI from another machine.
+5 -1
View File
@@ -10,8 +10,12 @@ the implementation at the recorded date; current operating instructions live in
### Changed
- Added a uv-managed Python 3.12 environment and Ruff, mypy, and pytest quality
checks. Split the FastAPI service into configuration, route, provider,
checks, all contained in `s2s-client/`. Startup and Docker use the committed
lockfile. Split the FastAPI service into configuration, route, provider,
context/reasoning, and realtime modules while retaining `server:app` startup.
- Added Pydantic response contracts with permissive upstream fields and original
payload serialization. Captured HTTP, request-schema, and realtime regression
fixtures preserve existing API behavior, including known failures.
- Removed the legacy browser site in favor of the React application in
`s2s-frontend`. FastAPI now serves only backend and documentation routes.
- Retired daily talk-time accounting and the associated Hugging Face account
+13 -5
View File
@@ -2,16 +2,24 @@
# Serve backend APIs, the WebSocket proxy, and shared project documentation.
# Run the React/Vite frontend separately; this image contains no browser UI.
# HF Spaces (sdk: docker) routes traffic to $PORT, default 7860.
FROM python:3.11-slim
FROM python:3.12.14-slim
COPY --from=ghcr.io/astral-sh/uv:0.12.5 /uv /uvx /bin/
ENV UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=never \
PATH="/app/s2s-client/.venv/bin:$PATH"
WORKDIR /app/s2s-client
COPY s2s-client/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY s2s-client/pyproject.toml s2s-client/uv.lock ./
RUN uv sync --locked --no-dev --no-install-project
COPY s2s-client/ .
COPY s2s-client/README.md s2s-client/server.py ./
COPY s2s-client/s2s_client/ ./s2s_client/
COPY docs/ /app/docs/
RUN uv sync --locked --no-dev
EXPOSE 7860
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7860"]
CMD ["uvicorn", "s2s_client.main:app", "--host", "0.0.0.0", "--port", "7860"]
+10 -3
View File
@@ -31,17 +31,24 @@ Build from the repository root so the container includes the documentation:
docker build -f s2s-client/Dockerfile -t s2s-client .
```
Run the backend regression tests from the repository root after installing
`requirements.txt` in `.venv`:
The Python project is contained in this directory: `pyproject.toml`, `uv.lock`,
`.python-version`, `.venv`, quality tools, and tests. Use uv 0.12.5 and Python
3.12 (uv can provision Python). From the repository root:
```bash
.venv/bin/python -m unittest discover -s s2s-client/tests -v
make -C s2s-client install
make -C s2s-client check
```
Tests isolate environment configuration, use HTTP fixtures for allocation and
caller authentication, and start a loopback WebSocket service to verify audio
frames, keepalive, and teardown. No deployed services or database are needed.
See [backend development and compatibility](../docs/client/development.md) for
formatting, typing, schema policy, optional Git hooks, and deployment checks.
`uvicorn server:app --app-dir s2s-client` remains a supported entry point;
the implementation now lives in `s2s_client/`.
FastAPI's API explorer remains at `/docs`. Project documents are served as files below
`/docs/client/`, `/docs/speech/`, `/docs/adr/`, `/docs/examples/`, and
`/docs/proposals/`. The historical `/project/` and `/public/docs.html` URLs
-4
View File
@@ -1,4 +0,0 @@
fastapi==0.115.6
uvicorn[standard]==0.34.0
httpx==0.28.1
python-dotenv==1.2.2
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Build the actual runtime and check HTTP behavior without external services.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
IMAGE="s2s-client-smoke:$(date +%s)-$$"
container_id=""
cleanup() {
if [ -n "$container_id" ]; then
docker rm --force "$container_id" >/dev/null
fi
docker image rm "$IMAGE" >/dev/null 2>&1 || true
}
trap cleanup EXIT
docker build --file "$ROOT_DIR/s2s-client/Dockerfile" --tag "$IMAGE" "$ROOT_DIR"
container_id="$(docker run --detach --publish 127.0.0.1::7860 "$IMAGE")"
port="$(docker port "$container_id" 7860/tcp)"
ready=false
for ((attempt = 0; attempt < 60; attempt++)); do
if curl --noproxy '*' --silent --fail --max-time 1 "http://$port/api/config" >/dev/null; then
ready=true
break
fi
sleep 0.5
done
if [ "$ready" != true ]; then
docker logs "$container_id"
exit 1
fi
docker exec -i "$container_id" python - <<'PY'
import json
import urllib.error
import urllib.request
base = "http://127.0.0.1:7860"
with urllib.request.urlopen(base + "/api/config") as response:
assert json.load(response) == {
"search": False, "lb": False, "allowDirect": True, "s2sUrl": "",
"primaryPrompt": "", "reasoningType": "PROXY", "injectProfile": False,
"debugPrompt": False, "startSessionMode": "", "theme": "", "apipilot": False,
}
for path in ["/docs", "/docs/README.md", "/openapi.json"]:
with urllib.request.urlopen(base + path) as response:
assert response.status == 200
try:
urllib.request.urlopen(base + "/")
except urllib.error.HTTPError as error:
assert error.code == 404
else:
raise AssertionError("The backend root must remain 404")
print("Docker runtime smoke checks passed")
PY
+23 -5
View File
@@ -66,14 +66,19 @@ async function launcher(context: TestContext, env: Record<string, string> = {})
const root = await mkdtemp(join(tmpdir(), "s2s-launcher-"));
const frontend = join(root, "s2s-frontend");
const modules = join(frontend, "node_modules");
await mkdir(join(root, ".venv/bin"), { recursive: true });
await mkdir(join(root, "s2s-client"));
await mkdir(join(root, "s2s-client/.venv/bin"), { recursive: true });
await mkdir(join(root, "bin"));
await mkdir(join(modules, "vite/bin"), { recursive: true });
await copyFile(new URL("../../start_ui.sh", import.meta.url), join(root, "start_ui.sh"));
await writeFile(join(root, "s2s-client/requirements.txt"), "");
await writeFile(join(root, ".venv/bin/activate"), 'export PATH="$S2S_TEST_ROOT/.venv/bin:$PATH"\n');
await writeFile(join(root, "s2s-client/pyproject.toml"), "");
await writeFile(join(root, "s2s-client/uv.lock"), "");
await writeFile(
join(root, ".venv/bin/python"),
join(root, "bin/uv"),
'#!/bin/bash\nprintf "%s\\n" "$@" > "$S2S_TEST_ROOT/uv-args"\nprintf "%s" "$UV_PROJECT_ENVIRONMENT" > "$S2S_TEST_ROOT/uv-environment"\nexit "${S2S_TEST_UV_EXIT:-0}"\n',
{ mode: 0o755 },
);
await writeFile(
join(root, "s2s-client/.venv/bin/python"),
'#!/bin/bash\nif [ "$1" = "-c" ]; then exit 0; fi\nexec node "$S2S_TEST_ROOT/child.mjs" backend "$@"\n',
{ mode: 0o755 },
);
@@ -94,6 +99,7 @@ async function launcher(context: TestContext, env: Record<string, string> = {})
cwd: tmpdir(),
env: {
...process.env,
PATH: `${join(root, "bin")}:${process.env.PATH ?? ""}`,
UI_PORT: String(publicPort),
UI_HOST: "0.0.0.0",
NODE_ENV: "production",
@@ -138,6 +144,11 @@ for (const signal of ["SIGINT", "SIGTERM"] as const) {
nodeEnv: "development",
});
assert.equal(run.output().includes("Installing"), false);
assert.equal(
await readFile(join(run.root, "uv-args"), "utf8"),
`sync\n--project\n${run.root}/s2s-client\n--locked\n`,
);
assert.equal(await readFile(join(run.root, "uv-environment"), "utf8"), `${run.root}/s2s-client/.venv`);
run.child.kill(signal);
assert.equal(await run.exited, signal === "SIGINT" ? 130 : 143);
for (const role of ["backend", "frontend"]) {
@@ -168,6 +179,13 @@ await test("does not start Vite when the backend fails readiness", { timeout: 10
assert.match(run.output(), /FastAPI exited before it was ready/);
});
await test("does not start either server when uv sync fails", { timeout: 10_000 }, async (context) => {
const run = await launcher(context, { S2S_TEST_UV_EXIT: "23" });
assert.equal(await run.exited, 23, run.output());
await assert.rejects(readFile(join(run.root, "backend.json")));
await assert.rejects(readFile(join(run.root, "frontend.json")));
});
await test("stops Vite if the backend exits after startup", { timeout: 10_000 }, async (context) => {
const run = await launcher(context);
await waitForFile(join(run.root, "frontend-worker-ready"));
+10 -19
View File
@@ -2,8 +2,8 @@
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_DIR="$ROOT_DIR/.venv"
REQ_FILE="$ROOT_DIR/s2s-client/requirements.txt"
CLIENT_DIR="$ROOT_DIR/s2s-client"
VENV_DIR="$CLIENT_DIR/.venv"
FRONTEND_DIR="$ROOT_DIR/s2s-frontend"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
@@ -20,7 +20,7 @@ if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) ))
exit 1
fi
for required_command in python3 node npm setsid curl; do
for required_command in uv node npm setsid curl; do
if ! command -v "$required_command" >/dev/null 2>&1; then
echo "Error: $required_command was not found."
exit 1
@@ -46,22 +46,13 @@ if [ "$UI_PORT" = "$S2S_BACKEND_PORT" ]; then
exit 1
fi
if [ ! -d "$VENV_DIR" ]; then
echo "Creating the virtual environment in .venv..."
python3 -m venv "$VENV_DIR"
fi
source "$VENV_DIR/bin/activate"
if [ ! -f "$REQ_FILE" ]; then
echo "Error: requirements file was not found: $REQ_FILE"
if [ ! -f "$CLIENT_DIR/pyproject.toml" ] || [ ! -f "$CLIENT_DIR/uv.lock" ]; then
echo "Error: s2s-client/pyproject.toml and s2s-client/uv.lock are required."
exit 1
fi
if ! python -c "import fastapi, uvicorn, httpx, dotenv, websockets" >/dev/null 2>&1; then
echo "Installing dependencies from s2s-client/requirements.txt..."
python -m pip install -r "$REQ_FILE"
fi
# uv manages only the client environment; an existing root speech venv is separate.
UV_PROJECT_ENVIRONMENT="$VENV_DIR" uv sync --project "$CLIENT_DIR" --locked
# Keep restarts offline when the installed frontend manifests have not changed.
if [ ! -f "$FRONTEND_DIR/node_modules/vite/bin/vite.js" ] || \
@@ -118,10 +109,10 @@ echo " Press Ctrl+C in this terminal to stop both servers."
echo "----------------------------------------------------"
cd "$ROOT_DIR"
setsid "$VENV_DIR/bin/python" -m uvicorn server:app \
--app-dir "$ROOT_DIR/s2s-client" \
setsid "$VENV_DIR/bin/python" -m uvicorn s2s_client.main:app \
--app-dir "$CLIENT_DIR" \
--host 127.0.0.1 --port "$S2S_BACKEND_PORT" \
--reload --reload-dir "$ROOT_DIR/s2s-client" &
--reload --reload-dir "$CLIENT_DIR" &
backend_pid=$!
child_pids+=("$backend_pid")