mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 09:40:40 +00:00
feat: harden docker runtime validation
This commit is contained in:
1 parent
004cce2a4f
commit
83e9e3d511
32 files changed
+1360
-40
No files matched your search
@@ -0,0 +1,16 @@
|
||||
.git
|
||||
.github
|
||||
.pytest_cache
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
build
|
||||
dist
|
||||
ci-logs
|
||||
docs
|
||||
tests
|
||||
imports
|
||||
site
|
||||
voxvera/host
|
||||
voxvera/resources/bin
|
||||
+11
-5
@@ -4,8 +4,8 @@ FROM python:3.11-slim
|
||||
# persistent-host deployment target.
|
||||
|
||||
# Install system dependencies (only runtime)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
tor onionshare-cli \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tini tor onionshare-cli \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy VoxVera source
|
||||
@@ -13,12 +13,18 @@ COPY . /opt/voxvera
|
||||
WORKDIR /opt/voxvera
|
||||
|
||||
# Install VoxVera and its Python dependencies
|
||||
RUN pip install .
|
||||
RUN pip install . \
|
||||
&& chmod +x scripts/docker-entrypoint.sh scripts/docker-healthcheck.sh \
|
||||
&& ln -sf /opt/voxvera/scripts/docker-entrypoint.sh /usr/local/bin/voxvera-docker-entrypoint \
|
||||
&& ln -sf /opt/voxvera/scripts/docker-healthcheck.sh /usr/local/bin/voxvera-docker-healthcheck
|
||||
|
||||
# Prepare flyers volume
|
||||
RUN mkdir /flyers
|
||||
ENV VOXVERA_DIR=/flyers
|
||||
ENV VOXVERA_RUNTIME=docker
|
||||
VOLUME /flyers
|
||||
|
||||
# Seed a default config when the volume is empty, then retry startup periodically.
|
||||
CMD ["sh", "-lc", "mkdir -p /flyers && if [ ! -f /flyers/config.json ]; then cp /opt/voxvera/voxvera/src/config.json /flyers/config.json; fi && voxvera quickstart --non-interactive && while true; do sleep 300; voxvera start-all || true; done"]
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 CMD ["voxvera-docker-healthcheck"]
|
||||
|
||||
ENTRYPOINT ["tini", "--", "voxvera-docker-entrypoint"]
|
||||
CMD []
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -0,0 +1,173 @@
|
||||
# macOS Validation Checklist
|
||||
|
||||
This checklist is for validating the experimental macOS CLI path on a real macOS machine.
|
||||
|
||||
Do not promote macOS above `experimental` until this checklist has been completed successfully on at least one native host and the results have been reviewed.
|
||||
|
||||
## Goal
|
||||
|
||||
Validate whether the current macOS runtime can:
|
||||
|
||||
1. install cleanly
|
||||
2. build and serve a flyer
|
||||
3. preserve the OnionShare session identity
|
||||
4. recover after login/reboot/offline periods
|
||||
5. surface understandable logs and status
|
||||
|
||||
## Environment Notes
|
||||
|
||||
- This checklist assumes the CLI runtime, not the Electron GUI.
|
||||
- Prefer a clean machine or disposable user account.
|
||||
- Record the exact macOS version, machine type, and install path used.
|
||||
|
||||
## 1. Install Surface
|
||||
|
||||
Record which install path was used:
|
||||
|
||||
- raw binary
|
||||
- DMG
|
||||
- `pipx`
|
||||
- `install.sh`
|
||||
|
||||
Capture:
|
||||
|
||||
```bash
|
||||
voxvera --version
|
||||
voxvera platform-status --json
|
||||
voxvera doctor --json
|
||||
voxvera autostart status --json
|
||||
```
|
||||
|
||||
Pass criteria:
|
||||
|
||||
- VoxVera launches
|
||||
- `platform-status` reports `macos_cli`
|
||||
- `doctor` runs without crashing
|
||||
- `autostart status` runs without crashing
|
||||
|
||||
## 2. Build + Serve
|
||||
|
||||
Create a fresh working directory and config:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/voxvera-test
|
||||
export VOXVERA_DIR=~/voxvera-test
|
||||
voxvera init --non-interactive
|
||||
voxvera build
|
||||
voxvera serve
|
||||
```
|
||||
|
||||
Capture:
|
||||
|
||||
- whether `index.html` is created under `~/voxvera-test/host/voxvera/`
|
||||
- whether `.onionshare-session` appears
|
||||
- whether an `.onion` URL is printed
|
||||
- whether `onionshare.log` is created
|
||||
|
||||
Pass criteria:
|
||||
|
||||
- build completes
|
||||
- serve completes
|
||||
- `.onionshare-session` exists
|
||||
- `onionshare.log` exists
|
||||
|
||||
## 3. Identity Persistence
|
||||
|
||||
Stop and restart the site:
|
||||
|
||||
```bash
|
||||
voxvera stop-all
|
||||
voxvera start-all
|
||||
```
|
||||
|
||||
Capture:
|
||||
|
||||
- whether the same `.onion` URL returns
|
||||
- whether `.onionshare-session` remains unchanged
|
||||
|
||||
Pass criteria:
|
||||
|
||||
- the onion identity persists across restart
|
||||
|
||||
## 4. Autostart Install
|
||||
|
||||
Install the experimental `launchd` path:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
voxvera autostart status --json
|
||||
```
|
||||
|
||||
Capture:
|
||||
|
||||
- generated plist path
|
||||
- any `launchctl` output or errors
|
||||
|
||||
Pass criteria:
|
||||
|
||||
- the plist is created
|
||||
- `autostart status` reports installed artifacts
|
||||
|
||||
## 5. Login / Reboot Recovery
|
||||
|
||||
After autostart is installed:
|
||||
|
||||
1. reboot the Mac or log out and back in
|
||||
2. wait a few minutes
|
||||
3. check:
|
||||
|
||||
```bash
|
||||
voxvera doctor --json
|
||||
voxvera autostart status --json
|
||||
voxvera start-all
|
||||
```
|
||||
|
||||
Capture:
|
||||
|
||||
- whether hosting resumed automatically
|
||||
- whether the onion identity persisted
|
||||
- whether the process had to be restarted manually
|
||||
|
||||
## 6. Offline / Reconnect Recovery
|
||||
|
||||
While a site is running:
|
||||
|
||||
1. disconnect the machine from the network
|
||||
2. wait at least 2 minutes
|
||||
3. reconnect
|
||||
4. wait a few minutes
|
||||
|
||||
Capture:
|
||||
|
||||
- whether hosting recovered automatically
|
||||
- whether manual `voxvera start-all` was required
|
||||
- any errors in `onionshare.log`
|
||||
|
||||
## 7. Logs And Status
|
||||
|
||||
Capture the practical debugging workflow:
|
||||
|
||||
```bash
|
||||
voxvera doctor --json
|
||||
voxvera autostart status --json
|
||||
ls -la ~/voxvera-test/host/voxvera
|
||||
```
|
||||
|
||||
Document:
|
||||
|
||||
- where logs live
|
||||
- what failed if recovery did not work
|
||||
- any permissions/path issues
|
||||
|
||||
## Report Back
|
||||
|
||||
A useful tester report should include:
|
||||
|
||||
- macOS version
|
||||
- install path used
|
||||
- whether build worked
|
||||
- whether serve worked
|
||||
- whether the onion identity persisted
|
||||
- whether login/reboot recovery worked
|
||||
- whether offline/reconnect recovery worked
|
||||
- exact command output or error text for failures
|
||||
@@ -0,0 +1,50 @@
|
||||
# Native Tester Guide
|
||||
|
||||
Use this guide when asking someone on macOS or Windows to validate VoxVera.
|
||||
|
||||
## Scope
|
||||
|
||||
VoxVera is not just an installer test. The real question is whether it can function as a reliable Tor hidden-service host on that platform.
|
||||
|
||||
That means every tester should be asked to validate:
|
||||
|
||||
1. install
|
||||
2. build
|
||||
3. serve
|
||||
4. onion identity persistence
|
||||
5. login/reboot recovery
|
||||
6. offline/reconnect recovery
|
||||
7. logs and diagnostics
|
||||
|
||||
## Canonical Checklists
|
||||
|
||||
- macOS: `docs/macos-validation-checklist.md`
|
||||
- Windows: `docs/windows-validation-checklist.md`
|
||||
|
||||
## What To Ask Testers To Send Back
|
||||
|
||||
- platform and OS version
|
||||
- install path used
|
||||
- `voxvera platform-status --json`
|
||||
- `voxvera doctor --json`
|
||||
- `voxvera autostart status --json`
|
||||
- whether `voxvera serve` produced an `.onion` URL
|
||||
- whether `.onionshare-session` persisted after restart
|
||||
- whether hosting resumed after reboot/login
|
||||
- whether hosting resumed after network reconnect
|
||||
- exact error output for any failure
|
||||
|
||||
## What Not To Conclude Prematurely
|
||||
|
||||
- successful installation does not prove hosting works
|
||||
- successful hosting once does not prove recovery works
|
||||
- binary availability does not prove platform parity
|
||||
|
||||
## Current Repo Position
|
||||
|
||||
- Linux CLI with `systemd --user`: supported
|
||||
- Docker CLI: experimental
|
||||
- macOS CLI: experimental
|
||||
- Windows CLI: experimental
|
||||
|
||||
Do not promote macOS or Windows above `experimental` without native recovery evidence.
|
||||
@@ -49,3 +49,11 @@ voxvera autostart
|
||||
```
|
||||
|
||||
That is the current reference deployment model the rest of the platform work should converge toward.
|
||||
|
||||
## Native Validation
|
||||
|
||||
When validating the experimental non-Linux paths on real machines, use:
|
||||
|
||||
- [macOS checklist](/home/user/Documents/GitHub/VoxVera/docs/macos-validation-checklist.md)
|
||||
- [Windows checklist](/home/user/Documents/GitHub/VoxVera/docs/windows-validation-checklist.md)
|
||||
- [Native tester guide](/home/user/Documents/GitHub/VoxVera/docs/native-tester-guide.md)
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -258,6 +258,16 @@ These target names are contract-level validation entry points. Only `linux-cli`
|
||||
|
||||
1. Add `voxvera doctor`.
|
||||
2. Add `voxvera autostart status`.
|
||||
|
||||
## Native Validation Assets
|
||||
|
||||
The repo now includes concrete native-host validation docs for the two major experimental desktop targets:
|
||||
|
||||
- `docs/macos-validation-checklist.md`
|
||||
- `docs/windows-validation-checklist.md`
|
||||
- `docs/native-tester-guide.md`
|
||||
|
||||
These should be used for the first real macOS/Windows validation cycles so testers capture the runtime evidence needed for future tier promotion.
|
||||
3. Add the platform adapter scaffold.
|
||||
4. Capture Linux reboot/offline recovery validation steps in docs and CI notes.
|
||||
5. Pick Docker's canonical runtime model.
|
||||
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
Vendored
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -0,0 +1,172 @@
|
||||
# Windows Validation Checklist
|
||||
|
||||
This checklist is for validating the experimental Windows CLI path on a real Windows machine.
|
||||
|
||||
Do not promote Windows above `experimental` until this checklist has been completed successfully on at least one native host and the results have been reviewed.
|
||||
|
||||
## Goal
|
||||
|
||||
Validate whether the current Windows runtime can:
|
||||
|
||||
1. install cleanly
|
||||
2. build and serve a flyer
|
||||
3. preserve the OnionShare session identity
|
||||
4. recover after login/reboot/offline periods
|
||||
5. surface understandable logs and status
|
||||
|
||||
## Environment Notes
|
||||
|
||||
- This checklist assumes the CLI runtime, not the Electron GUI.
|
||||
- Prefer a clean Windows machine or VM snapshot.
|
||||
- Record the exact Windows version and install path used.
|
||||
|
||||
## 1. Install Surface
|
||||
|
||||
Record which install path was used:
|
||||
|
||||
- standalone `.exe`
|
||||
- PowerShell installer
|
||||
- Chocolatey
|
||||
|
||||
Capture in PowerShell:
|
||||
|
||||
```powershell
|
||||
voxvera --version
|
||||
voxvera platform-status --json
|
||||
voxvera doctor --json
|
||||
voxvera autostart status --json
|
||||
```
|
||||
|
||||
Pass criteria:
|
||||
|
||||
- VoxVera launches
|
||||
- `platform-status` reports `windows_cli`
|
||||
- `doctor` runs without crashing
|
||||
- `autostart status` runs without crashing
|
||||
|
||||
## 2. Build + Serve
|
||||
|
||||
Create a fresh working directory and config:
|
||||
|
||||
```powershell
|
||||
$env:VOXVERA_DIR = "$HOME\\voxvera-test"
|
||||
New-Item -ItemType Directory -Force $env:VOXVERA_DIR | Out-Null
|
||||
voxvera init --non-interactive
|
||||
voxvera build
|
||||
voxvera serve
|
||||
```
|
||||
|
||||
Capture:
|
||||
|
||||
- whether `index.html` is created under `%USERPROFILE%\\voxvera-test\\host\\voxvera\\`
|
||||
- whether `.onionshare-session` appears
|
||||
- whether an `.onion` URL is printed
|
||||
- whether `onionshare.log` is created
|
||||
|
||||
Pass criteria:
|
||||
|
||||
- build completes
|
||||
- serve completes
|
||||
- `.onionshare-session` exists
|
||||
- `onionshare.log` exists
|
||||
|
||||
## 3. Identity Persistence
|
||||
|
||||
Stop and restart the site:
|
||||
|
||||
```powershell
|
||||
voxvera stop-all
|
||||
voxvera start-all
|
||||
```
|
||||
|
||||
Capture:
|
||||
|
||||
- whether the same `.onion` URL returns
|
||||
- whether `.onionshare-session` remains unchanged
|
||||
|
||||
Pass criteria:
|
||||
|
||||
- the onion identity persists across restart
|
||||
|
||||
## 4. Autostart Install
|
||||
|
||||
Install the experimental Task Scheduler path:
|
||||
|
||||
```powershell
|
||||
voxvera autostart
|
||||
voxvera autostart status --json
|
||||
```
|
||||
|
||||
Capture:
|
||||
|
||||
- scheduled task name
|
||||
- any `schtasks` output or errors
|
||||
|
||||
Pass criteria:
|
||||
|
||||
- scheduled task is created
|
||||
- `autostart status` reports installed artifacts
|
||||
|
||||
## 5. Login / Reboot Recovery
|
||||
|
||||
After autostart is installed:
|
||||
|
||||
1. reboot the machine or log out and back in
|
||||
2. wait a few minutes
|
||||
3. check:
|
||||
|
||||
```powershell
|
||||
voxvera doctor --json
|
||||
voxvera autostart status --json
|
||||
voxvera start-all
|
||||
```
|
||||
|
||||
Capture:
|
||||
|
||||
- whether hosting resumed automatically
|
||||
- whether the onion identity persisted
|
||||
- whether the process had to be restarted manually
|
||||
|
||||
## 6. Offline / Reconnect Recovery
|
||||
|
||||
While a site is running:
|
||||
|
||||
1. disconnect the machine from the network
|
||||
2. wait at least 2 minutes
|
||||
3. reconnect
|
||||
4. wait a few minutes
|
||||
|
||||
Capture:
|
||||
|
||||
- whether hosting recovered automatically
|
||||
- whether manual `voxvera start-all` was required
|
||||
- any errors in `onionshare.log`
|
||||
|
||||
## 7. Logs And Status
|
||||
|
||||
Capture the practical debugging workflow:
|
||||
|
||||
```powershell
|
||||
voxvera doctor --json
|
||||
voxvera autostart status --json
|
||||
Get-ChildItem "$HOME\\voxvera-test\\host\\voxvera" -Force
|
||||
```
|
||||
|
||||
Document:
|
||||
|
||||
- where logs live
|
||||
- what failed if recovery did not work
|
||||
- any permissions/path issues
|
||||
|
||||
## Report Back
|
||||
|
||||
A useful tester report should include:
|
||||
|
||||
- Windows version
|
||||
- install path used
|
||||
- whether build worked
|
||||
- whether serve worked
|
||||
- whether the onion identity persisted
|
||||
- whether login/reboot recovery worked
|
||||
- whether offline/reconnect recovery worked
|
||||
- exact command output or error text for failures
|
||||
+31
-2
@@ -11,9 +11,38 @@ docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
docker run -d \
|
||||
--name voxvera \
|
||||
--restart unless-stopped \
|
||||
-v "$(pwd)/flyers:/flyers" \
|
||||
ghcr.io/voxvera/voxvera:latest
|
||||
```
|
||||
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
The container now uses a dedicated entrypoint that:
|
||||
- stores runtime state under `/flyers`
|
||||
- seeds `/flyers/config.json` if needed
|
||||
- starts an in-container Tor daemon on `127.0.0.1:9050/9051`
|
||||
- bootstraps a default site when `/flyers/host` is empty
|
||||
- runs `voxvera start-all` on startup and retries it periodically
|
||||
- exposes a container healthcheck based on `voxvera doctor --json`
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker logs -f voxvera
|
||||
docker exec -it voxvera voxvera doctor --json
|
||||
docker exec -it voxvera voxvera autostart status --json
|
||||
```
|
||||
|
||||
If you prefer Compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
voxvera:
|
||||
image: ghcr.io/voxvera/voxvera:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./flyers:/flyers
|
||||
```
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -21,6 +21,10 @@ python3 -m voxvera.cli --lang en check
|
||||
if [[ "$(uname -s)" == "Linux" ]]; then
|
||||
echo "==> platform smoke"
|
||||
bash scripts/platform-smoke.sh linux-cli
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
echo "==> docker runtime smoke"
|
||||
bash scripts/docker-runtime-smoke.sh
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "==> build docs"
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VOXVERA_DIR="${VOXVERA_DIR:-/flyers}"
|
||||
VOXVERA_CONFIG_PATH="${VOXVERA_CONFIG_PATH:-$VOXVERA_DIR/config.json}"
|
||||
VOXVERA_START_INTERVAL="${VOXVERA_START_INTERVAL:-300}"
|
||||
VOXVERA_BOOTSTRAP_DEFAULT_SITE="${VOXVERA_BOOTSTRAP_DEFAULT_SITE:-1}"
|
||||
TOR_SOCKS_PORT="${TOR_SOCKS_PORT:-9050}"
|
||||
TOR_CONTROL_PORT="${TOR_CONTROL_PORT:-9051}"
|
||||
VOXVERA_TOR_DIR="${VOXVERA_TOR_DIR:-/tmp/voxvera-tor}"
|
||||
|
||||
TOR_PID=""
|
||||
SLEEP_PID=""
|
||||
|
||||
log() {
|
||||
printf '[voxvera-docker] %s\n' "$*"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$SLEEP_PID" ]] && kill -0 "$SLEEP_PID" 2>/dev/null; then
|
||||
kill "$SLEEP_PID" 2>/dev/null || true
|
||||
fi
|
||||
voxvera stop-all >/dev/null 2>&1 || true
|
||||
if [[ -n "$TOR_PID" ]] && kill -0 "$TOR_PID" 2>/dev/null; then
|
||||
kill "$TOR_PID" 2>/dev/null || true
|
||||
wait "$TOR_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
wait_for_port() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
python3 - "$host" "$port" <<'PY'
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
host = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
deadline = time.time() + 60
|
||||
|
||||
while time.time() < deadline:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.settimeout(1)
|
||||
if sock.connect_ex((host, port)) == 0:
|
||||
sys.exit(0)
|
||||
time.sleep(1)
|
||||
|
||||
sys.exit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
has_site_configs() {
|
||||
find "$VOXVERA_DIR/host" -mindepth 2 -maxdepth 2 -name config.json -print -quit 2>/dev/null | grep -q .
|
||||
}
|
||||
|
||||
seed_default_config() {
|
||||
mkdir -p "$VOXVERA_DIR"
|
||||
if [[ ! -f "$VOXVERA_CONFIG_PATH" ]]; then
|
||||
cp /opt/voxvera/voxvera/src/config.json "$VOXVERA_CONFIG_PATH"
|
||||
log "Seeded default config at $VOXVERA_CONFIG_PATH"
|
||||
fi
|
||||
}
|
||||
|
||||
bootstrap_default_site_if_needed() {
|
||||
if [[ "$VOXVERA_BOOTSTRAP_DEFAULT_SITE" != "1" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if has_site_configs; then
|
||||
return
|
||||
fi
|
||||
|
||||
log "No sites found under $VOXVERA_DIR/host; bootstrapping default site"
|
||||
voxvera --config "$VOXVERA_CONFIG_PATH" init --non-interactive
|
||||
}
|
||||
|
||||
start_tor() {
|
||||
mkdir -p "$VOXVERA_TOR_DIR"
|
||||
chmod 700 "$VOXVERA_TOR_DIR"
|
||||
|
||||
cat >"$VOXVERA_TOR_DIR/torrc" <<EOF
|
||||
DataDirectory $VOXVERA_TOR_DIR/data
|
||||
SocksPort 127.0.0.1:$TOR_SOCKS_PORT
|
||||
ControlPort 127.0.0.1:$TOR_CONTROL_PORT
|
||||
CookieAuthentication 0
|
||||
AvoidDiskWrites 1
|
||||
Log notice stdout
|
||||
EOF
|
||||
|
||||
mkdir -p "$VOXVERA_TOR_DIR/data"
|
||||
log "Starting Tor on 127.0.0.1:$TOR_SOCKS_PORT / $TOR_CONTROL_PORT"
|
||||
tor -f "$VOXVERA_TOR_DIR/torrc" &
|
||||
TOR_PID="$!"
|
||||
|
||||
wait_for_port 127.0.0.1 "$TOR_SOCKS_PORT"
|
||||
wait_for_port 127.0.0.1 "$TOR_CONTROL_PORT"
|
||||
log "Tor ports are reachable"
|
||||
}
|
||||
|
||||
start_loop() {
|
||||
export TOR_SOCKS_PORT TOR_CONTROL_PORT
|
||||
|
||||
while true; do
|
||||
if ! kill -0 "$TOR_PID" 2>/dev/null; then
|
||||
log "Tor exited unexpectedly"
|
||||
wait "$TOR_PID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Running voxvera start-all"
|
||||
voxvera start-all || true
|
||||
|
||||
sleep "$VOXVERA_START_INTERVAL" &
|
||||
SLEEP_PID="$!"
|
||||
wait "$SLEEP_PID"
|
||||
SLEEP_PID=""
|
||||
done
|
||||
}
|
||||
|
||||
seed_default_config
|
||||
start_tor
|
||||
bootstrap_default_site_if_needed
|
||||
start_loop
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VOXVERA_DIR="${VOXVERA_DIR:-/flyers}"
|
||||
TMP_JSON="$(mktemp)"
|
||||
|
||||
voxvera doctor --json >"$TMP_JSON"
|
||||
|
||||
python3 - "$TMP_JSON" "$VOXVERA_DIR" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
doctor = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
voxvera_dir = Path(sys.argv[2])
|
||||
checks = {check["name"]: check for check in doctor.get("checks", [])}
|
||||
|
||||
required = ("voxvera_cli", "onionshare_cli", "tor_socks_reachable", "host_root")
|
||||
missing = []
|
||||
failing = []
|
||||
|
||||
for name in required:
|
||||
check = checks.get(name)
|
||||
if not check:
|
||||
missing.append(name)
|
||||
continue
|
||||
if not check.get("ok"):
|
||||
failing.append(f"{name}: {check.get('detail', '')}")
|
||||
|
||||
site_configs = list((voxvera_dir / "host").glob("*/config.json"))
|
||||
running_sites = [cfg.parent for cfg in site_configs if (cfg.parent / "server.pid").exists()]
|
||||
|
||||
if missing or failing or not site_configs or not running_sites:
|
||||
if missing:
|
||||
print("Missing checks: " + ", ".join(missing), file=sys.stderr)
|
||||
if failing:
|
||||
print("Failing checks: " + "; ".join(failing), file=sys.stderr)
|
||||
if not site_configs:
|
||||
print("No configured sites found under host/", file=sys.stderr)
|
||||
if site_configs and not running_sites:
|
||||
print("Configured sites exist but no server.pid files are present", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("healthy")
|
||||
PY
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "docker is not installed; skipping Docker runtime smoke"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
IMAGE_TAG="${VOXVERA_DOCKER_SMOKE_TAG:-voxvera:smoke}"
|
||||
CONTAINER_NAME="${VOXVERA_DOCKER_SMOKE_CONTAINER:-voxvera-smoke}"
|
||||
TMP_DIR="${VOXVERA_DOCKER_SMOKE_TMPDIR:-$(mktemp -d /tmp/voxvera-docker-smoke-XXXXXX)}"
|
||||
FLYERS_DIR="$TMP_DIR/flyers"
|
||||
mkdir -p "$FLYERS_DIR"
|
||||
|
||||
cleanup() {
|
||||
docker logs "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
if [[ -z "${VOXVERA_DOCKER_SMOKE_TMPDIR:-}" ]]; then
|
||||
rm -rf "$TMP_DIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "==> building Docker image"
|
||||
docker build -t "$IMAGE_TAG" .
|
||||
|
||||
echo "==> starting Docker container"
|
||||
docker run -d \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--restart no \
|
||||
-e VOXVERA_START_INTERVAL=15 \
|
||||
-v "$FLYERS_DIR:/flyers" \
|
||||
"$IMAGE_TAG" >/dev/null
|
||||
|
||||
echo "==> waiting for healthy container"
|
||||
deadline=$((SECONDS + 240))
|
||||
while true; do
|
||||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' "$CONTAINER_NAME")"
|
||||
case "$status" in
|
||||
healthy)
|
||||
break
|
||||
;;
|
||||
unhealthy)
|
||||
echo "Container became unhealthy" >&2
|
||||
docker logs "$CONTAINER_NAME" >&2 || true
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
if (( SECONDS >= deadline )); then
|
||||
echo "Timed out waiting for healthy Docker container" >&2
|
||||
docker logs "$CONTAINER_NAME" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
PLATFORM_JSON="$TMP_DIR/platform-status.json"
|
||||
DOCTOR_JSON="$TMP_DIR/doctor.json"
|
||||
|
||||
docker exec "$CONTAINER_NAME" voxvera platform-status --json >"$PLATFORM_JSON"
|
||||
docker exec "$CONTAINER_NAME" voxvera doctor --json >"$DOCTOR_JSON"
|
||||
|
||||
PLATFORM_JSON="$PLATFORM_JSON" \
|
||||
DOCTOR_JSON="$DOCTOR_JSON" \
|
||||
FLYERS_DIR="$FLYERS_DIR" \
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
platform_status = json.loads(Path(os.environ["PLATFORM_JSON"]).read_text(encoding="utf-8"))
|
||||
doctor = json.loads(Path(os.environ["DOCTOR_JSON"]).read_text(encoding="utf-8"))
|
||||
flyers_dir = Path(os.environ["FLYERS_DIR"])
|
||||
|
||||
errors = []
|
||||
|
||||
if platform_status.get("platform_id") != "docker_cli":
|
||||
errors.append(f"platform_id mismatch: {platform_status.get('platform_id')}")
|
||||
if platform_status.get("tier") != "experimental":
|
||||
errors.append(f"tier mismatch: {platform_status.get('tier')}")
|
||||
|
||||
checks = {check["name"]: check for check in doctor.get("checks", [])}
|
||||
for required in ("container_marker", "docker_entrypoint", "docker_healthcheck", "tor_socks_reachable", "host_root"):
|
||||
check = checks.get(required)
|
||||
if not check:
|
||||
errors.append(f"missing doctor check: {required}")
|
||||
elif not check.get("ok"):
|
||||
errors.append(f"doctor check failed: {required} ({check.get('detail')})")
|
||||
|
||||
site_dir = flyers_dir / "host" / "voxvera"
|
||||
for path in (flyers_dir / "config.json", site_dir / "config.json", site_dir / "index.html", site_dir / ".onionshare-session", site_dir / "server.pid"):
|
||||
if not path.exists():
|
||||
errors.append(f"missing expected Docker runtime artifact: {path}")
|
||||
|
||||
if errors:
|
||||
print("Docker runtime smoke failed:", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(f"- {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("Docker runtime smoke OK")
|
||||
print(f"Platform: {platform_status['label']} [{platform_status['tier']}]")
|
||||
print(f"Hosting model: {platform_status.get('hosting_model', '')}")
|
||||
PY
|
||||
@@ -36,8 +36,8 @@ case "$TARGET" in
|
||||
REQUIRE_ONIONSHARE="${VOXVERA_REQUIRE_ONIONSHARE:-0}"
|
||||
;;
|
||||
docker-cli)
|
||||
EXPECTED_PLATFORM_ID="linux_cli_systemd"
|
||||
EXPECTED_TIER="${VOXVERA_EXPECTED_TIER_OVERRIDE:-supported}"
|
||||
EXPECTED_PLATFORM_ID="docker_cli"
|
||||
EXPECTED_TIER="${VOXVERA_EXPECTED_TIER_OVERRIDE:-experimental}"
|
||||
REQUIRE_ONIONSHARE="${VOXVERA_REQUIRE_ONIONSHARE:-0}"
|
||||
;;
|
||||
list)
|
||||
|
||||
+3
-3
@@ -38,7 +38,7 @@
|
||||
"id": "docker_cli",
|
||||
"label": "Docker CLI container",
|
||||
"tier": "experimental",
|
||||
"hosting_model": "Containerized VoxVera runtime with state under /flyers and periodic start-all retries.",
|
||||
"hosting_model": "Single-container VoxVera runtime with state under /flyers, an in-container Tor daemon, healthchecks, and periodic start-all retries.",
|
||||
"distribution_surfaces": [
|
||||
"Dockerfile",
|
||||
"ghcr image"
|
||||
@@ -49,11 +49,11 @@
|
||||
"validated_state_volume_layout"
|
||||
],
|
||||
"current_notes": [
|
||||
"State now routes through VOXVERA_DIR=/flyers.",
|
||||
"State routes through VOXVERA_DIR=/flyers.",
|
||||
"Container entrypoint now starts Tor internally and retries start-all on a fixed interval.",
|
||||
"Not yet validated as a production-grade hidden-service host."
|
||||
],
|
||||
"next_gaps": [
|
||||
"Decide whether the canonical model is single-container or compose-based.",
|
||||
"Validate restart behavior with real container restart policies."
|
||||
]
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ from voxvera.platforms import base as platform_base
|
||||
from voxvera.platforms import linux as platform_linux
|
||||
from voxvera.platforms import macos as platform_macos
|
||||
from voxvera.platforms import windows as platform_windows
|
||||
from voxvera.platforms import docker as platform_docker
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
@@ -53,10 +54,20 @@ def test_help(capsys):
|
||||
|
||||
def test_get_platform_adapter_linux(monkeypatch):
|
||||
monkeypatch.setattr(platform, "system", lambda: "Linux")
|
||||
monkeypatch.delenv("VOXVERA_RUNTIME", raising=False)
|
||||
monkeypatch.setattr(platform_docker.DockerPlatformAdapter, "is_container_runtime", lambda self: False)
|
||||
adapter = get_platform_adapter(cli_module=cli)
|
||||
assert adapter.platform_id == "linux_cli_systemd"
|
||||
|
||||
|
||||
def test_get_platform_adapter_docker(monkeypatch):
|
||||
monkeypatch.setattr(platform, "system", lambda: "Linux")
|
||||
monkeypatch.setenv("VOXVERA_RUNTIME", "docker")
|
||||
monkeypatch.setattr(platform_docker.DockerPlatformAdapter, "is_container_runtime", lambda self: True)
|
||||
adapter = get_platform_adapter(cli_module=cli)
|
||||
assert adapter.platform_id == "docker_cli"
|
||||
|
||||
|
||||
def test_get_platform_adapter_macos(monkeypatch):
|
||||
monkeypatch.setattr(platform, "system", lambda: "Darwin")
|
||||
adapter = get_platform_adapter(cli_module=cli)
|
||||
|
||||
@@ -76,6 +76,27 @@ def test_release_artifact_validation_is_wired_into_binaries_workflow():
|
||||
assert "platform-status --json" in script_text
|
||||
assert "init --non-interactive" in script_text
|
||||
|
||||
|
||||
def test_docker_runtime_assets_are_present():
|
||||
dockerfile = (REPO_ROOT / "Dockerfile").read_text(encoding="utf-8")
|
||||
dockerignore = REPO_ROOT / ".dockerignore"
|
||||
entrypoint = REPO_ROOT / "scripts" / "docker-entrypoint.sh"
|
||||
healthcheck = REPO_ROOT / "scripts" / "docker-healthcheck.sh"
|
||||
runtime_smoke = REPO_ROOT / "scripts" / "docker-runtime-smoke.sh"
|
||||
ci_workflow = (REPO_ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8")
|
||||
docker_doc = (REPO_ROOT / "docs" / "templates" / "docker.md").read_text(encoding="utf-8")
|
||||
|
||||
assert dockerignore.exists()
|
||||
assert entrypoint.exists()
|
||||
assert healthcheck.exists()
|
||||
assert runtime_smoke.exists()
|
||||
assert "tini" in dockerfile
|
||||
assert "docker-entrypoint.sh" in dockerfile
|
||||
assert "docker-healthcheck.sh" in dockerfile
|
||||
assert "HEALTHCHECK" in dockerfile
|
||||
assert "scripts/docker-runtime-smoke.sh" in ci_workflow
|
||||
assert "starts an in-container Tor daemon" in docker_doc
|
||||
|
||||
def test_locale_completeness():
|
||||
"""Verify that all non-English locales have the same keys as English."""
|
||||
locales_dir = REPO_ROOT / "voxvera" / "locales"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import platform
|
||||
|
||||
from .base import PlatformAdapter
|
||||
from .docker import DockerPlatformAdapter
|
||||
from .linux import LinuxPlatformAdapter
|
||||
from .macos import MacOSPlatformAdapter
|
||||
from .windows import WindowsPlatformAdapter
|
||||
@@ -9,6 +10,9 @@ from .windows import WindowsPlatformAdapter
|
||||
def get_platform_adapter(cli_module=None) -> PlatformAdapter:
|
||||
system = platform.system()
|
||||
if system == "Linux":
|
||||
docker_adapter = DockerPlatformAdapter(cli_module=cli_module)
|
||||
if docker_adapter.is_container_runtime():
|
||||
return docker_adapter
|
||||
return LinuxPlatformAdapter(cli_module=cli_module)
|
||||
if system == "Darwin":
|
||||
return MacOSPlatformAdapter(cli_module=cli_module)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
from pathlib import Path
|
||||
|
||||
from .base import PlatformAdapter
|
||||
|
||||
|
||||
class DockerPlatformAdapter(PlatformAdapter):
|
||||
platform_id = "docker_cli"
|
||||
tier = "experimental"
|
||||
label = "Docker CLI container"
|
||||
|
||||
@property
|
||||
def container_marker_paths(self) -> list[Path]:
|
||||
return [Path("/.dockerenv"), Path("/run/.containerenv")]
|
||||
|
||||
def is_container_runtime(self) -> bool:
|
||||
return any(path.exists() for path in self.container_marker_paths) or os.environ.get("VOXVERA_RUNTIME") == "docker"
|
||||
|
||||
def autostart_status(self) -> dict:
|
||||
artifacts = []
|
||||
if shutil.which("voxvera-docker-entrypoint"):
|
||||
artifacts.append("voxvera-docker-entrypoint")
|
||||
if shutil.which("voxvera-docker-healthcheck"):
|
||||
artifacts.append("voxvera-docker-healthcheck")
|
||||
|
||||
checks = [
|
||||
{
|
||||
"name": "container_marker",
|
||||
"ok": self.is_container_runtime(),
|
||||
"detail": "container marker present"
|
||||
if self.is_container_runtime()
|
||||
else "no Docker marker detected",
|
||||
},
|
||||
{
|
||||
"name": "docker_entrypoint",
|
||||
"ok": bool(shutil.which("voxvera-docker-entrypoint")),
|
||||
"detail": shutil.which("voxvera-docker-entrypoint") or "voxvera-docker-entrypoint not found",
|
||||
},
|
||||
{
|
||||
"name": "docker_healthcheck",
|
||||
"ok": bool(shutil.which("voxvera-docker-healthcheck")),
|
||||
"detail": shutil.which("voxvera-docker-healthcheck") or "voxvera-docker-healthcheck not found",
|
||||
},
|
||||
{
|
||||
"name": "restart_policy_external",
|
||||
"ok": True,
|
||||
"detail": "Container restart behavior is managed by Docker/Compose, not VoxVera autostart.",
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
"platform_id": self.platform_id,
|
||||
"label": self.label,
|
||||
"tier": self.tier,
|
||||
"supported": all(check["ok"] for check in checks[:3]),
|
||||
"message": "Docker recovery depends on the container entrypoint plus an external restart policy.",
|
||||
"artifacts": artifacts,
|
||||
"checks": checks,
|
||||
}
|
||||
|
||||
def install_autostart(self) -> None:
|
||||
raise RuntimeError("Docker autostart is managed by the container runtime. Use a restart policy such as '--restart unless-stopped'.")
|
||||
|
||||
def uninstall_autostart(self) -> None:
|
||||
raise RuntimeError("Docker autostart is managed by the container runtime. Remove or change the container restart policy instead.")
|
||||
|
||||
def doctor_report(self) -> dict:
|
||||
report = super().doctor_report()
|
||||
checks = report["checks"]
|
||||
|
||||
marker_present = self.is_container_runtime()
|
||||
checks.append(
|
||||
{
|
||||
"name": "container_marker",
|
||||
"ok": marker_present,
|
||||
"detail": "Docker marker detected" if marker_present else "no Docker marker detected",
|
||||
}
|
||||
)
|
||||
|
||||
socks_port = os.getenv("TOR_SOCKS_PORT", "9050")
|
||||
reachable = False
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(0.25)
|
||||
reachable = sock.connect_ex(("127.0.0.1", int(socks_port))) == 0
|
||||
sock.close()
|
||||
except Exception:
|
||||
reachable = False
|
||||
checks.append(
|
||||
{
|
||||
"name": "tor_socks_reachable",
|
||||
"ok": reachable,
|
||||
"detail": f"reachable on {socks_port}" if reachable else f"Tor SOCKS not reachable on {socks_port}",
|
||||
}
|
||||
)
|
||||
|
||||
host_root = self.data_dir / "host"
|
||||
checks.append(
|
||||
{
|
||||
"name": "host_root",
|
||||
"ok": host_root.exists(),
|
||||
"detail": str(host_root) if host_root.exists() else f"missing: {host_root}",
|
||||
}
|
||||
)
|
||||
|
||||
status = self.autostart_status()
|
||||
for name in ("docker_entrypoint", "docker_healthcheck"):
|
||||
check = next((item for item in status["checks"] if item["name"] == name), None)
|
||||
if check:
|
||||
checks.append(
|
||||
{
|
||||
"name": name,
|
||||
"ok": check["ok"],
|
||||
"detail": check["detail"],
|
||||
}
|
||||
)
|
||||
|
||||
report["summary"] = {
|
||||
"ok": all(check.get("ok") for check in checks),
|
||||
"failing_checks": [check["name"] for check in checks if not check.get("ok")],
|
||||
}
|
||||
report["sections"] = {
|
||||
"platform_contract": self._doctor_section("Platform contract", ["platform", "container_marker"], checks),
|
||||
"dependencies": self._doctor_section("Dependencies", ["voxvera_cli", "onionshare_cli", "docker_entrypoint", "docker_healthcheck"], checks),
|
||||
"network": self._doctor_section("Network", ["tor_socks_reachable"], checks),
|
||||
"autostart": self._doctor_section("Autostart", ["docker_entrypoint", "docker_healthcheck"], checks),
|
||||
"content_state": self._doctor_section("Content state", ["host_root"], checks),
|
||||
}
|
||||
return report
|
||||
Reference in new issue
Block a user