Compare commits

...
5 Commits
41 changed files with 1450 additions and 46 deletions

No files matched your search

+16
View File
@@ -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
+8
View File
@@ -62,3 +62,11 @@ jobs:
- name: Validate autostart JSON
shell: bash
run: python -m voxvera.cli autostart status --json
docker-runtime:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Docker runtime smoke
run: bash scripts/docker-runtime-smoke.sh
+11 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+4
View File
@@ -106,6 +106,10 @@ When you host a flyer, the **"Download Source Code"** button on the landing page
- Installation and setup scripts for all platforms.
- Complete documentation and templates.
To keep the mirrored download practical for low-bandwidth redistribution, the
source archive does not ship pre-downloaded Tor binaries. Normal installer and
release builds still fetch or bundle those artifacts separately.
This allows anyone who scans your flyer to become a new distributor of the VoxVera tool.
## Export & Backup
+31 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+4
View File
@@ -106,6 +106,10 @@ When you host a flyer, the **"הורד כלי וקוד מקור"** button on the
- Installation and setup scripts for all platforms.
- Complete documentation and templates.
To keep the mirrored download practical for low-bandwidth redistribution, the
source archive does not ship pre-downloaded Tor binaries. Normal installer and
release builds still fetch or bundle those artifacts separately.
This allows anyone who scans your flyer to become a new distributor of the VoxVera tool.
## Export & Backup
+31 -2
View File
@@ -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
View File
@@ -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.
+173
View File
@@ -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
+50
View File
@@ -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.
+8
View File
@@ -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
View File
@@ -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.
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+4
View File
@@ -106,6 +106,10 @@ When you host a flyer, the **"{{t('web.download_button')}}"** button on the land
- Installation and setup scripts for all platforms.
- Complete documentation and templates.
To keep the mirrored download practical for low-bandwidth redistribution, the
source archive does not ship pre-downloaded Tor binaries. Normal installer and
release builds still fetch or bundle those artifacts separately.
This allows anyone who scans your flyer to become a new distributor of the VoxVera tool.
## Export & Backup
+31 -2
View File
@@ -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.
+172
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
[project]
name = "voxvera"
version = "0.3.10"
version = "0.3.12"
description = "VoxVera CLI utilities"
readme = "README.md"
authors = [{name = "VoxVera"}]
+4
View File
@@ -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"
+125
View File
@@ -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
+45
View File
@@ -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
+109
View File
@@ -0,0 +1,109 @@
#!/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
chmod -R u+w "$TMP_DIR" 2>/dev/null || true
docker run --rm -v "$TMP_DIR:/cleanup" python:3.11-slim sh -lc 'rm -rf /cleanup/* /cleanup/.[!.]* /cleanup/..?* 2>/dev/null || true' >/dev/null 2>&1 || true
rm -rf "$TMP_DIR" 2>/dev/null || true
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
+2 -2
View File
@@ -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)
Binary file not shown.
+3 -3
View File
@@ -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."
]
},
+11
View File
@@ -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)
+54
View File
@@ -76,6 +76,35 @@ 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_support_matrix_path_has_runtime_fallbacks():
base_platform = (REPO_ROOT / "voxvera" / "platforms" / "base.py").read_text(encoding="utf-8")
assert "VOXVERA_SUPPORT_MATRIX_PATH" in base_platform
assert 'Path.cwd() / "support-matrix.json"' in base_platform
assert 'Path("/opt/voxvera/support-matrix.json")' in base_platform
def test_locale_completeness():
"""Verify that all non-English locales have the same keys as English."""
locales_dir = REPO_ROOT / "voxvera" / "locales"
@@ -230,3 +259,28 @@ def test_portable_bundle_integrity(tmp_path, monkeypatch):
# Verify root scripts
assert "voxvera-run.sh" in names
def test_portable_bundle_excludes_prebuilt_tor_binaries(tmp_path, monkeypatch):
project_root = tmp_path
voxvera_dir = project_root / "voxvera"
voxvera_dir.mkdir()
shutil.copytree(REPO_ROOT / "voxvera" / "src", voxvera_dir / "src")
shutil.copytree(REPO_ROOT / "voxvera" / "locales", voxvera_dir / "locales")
shutil.copytree(REPO_ROOT / "voxvera" / "resources", voxvera_dir / "resources")
os.makedirs(voxvera_dir / "vendor", exist_ok=True)
(voxvera_dir / "vendor" / "anchor.txt").write_text("anchor")
for script in ["voxvera-run.sh", "uninstall.sh", "uninstall.ps1", "install.sh", "install.ps1", "setup.sh"]:
if (REPO_ROOT / script).exists():
shutil.copy(REPO_ROOT / script, project_root / script)
monkeypatch.setattr(cli, "ROOT", voxvera_dir)
bundle_zip = project_root / "voxvera-portable.zip"
cli.bundle_portable(bundle_zip)
with zipfile.ZipFile(bundle_zip, "r") as zf:
names = zf.namelist()
assert not any(name.startswith("voxvera/resources/tor/") for name in names)
+1 -1
View File
@@ -1,7 +1,7 @@
import sys
from pathlib import Path
__version__ = "0.3.10"
__version__ = "0.3.12"
# Add the vendor directory to sys.path ONLY for PyInstaller (frozen) builds.
# For pip/pipx installs the system/venv packages are correct and the vendored
+18 -4
View File
@@ -693,12 +693,26 @@ def copy_template(name: str) -> str:
def bundle_source(dest_zip: Path):
"""Bundle the VoxVera source code, dependencies (vendor), and root scripts."""
"""Bundle the VoxVera source tree and install scripts for viral mirroring.
We intentionally exclude pre-downloaded Tor binaries from the mirrored source
archive. They dominate the zip size and are regenerated by the normal
packaging/build flows, while the mirrored source bundle is meant to stay
small enough for practical redistribution from flyer sites.
"""
with zipfile.ZipFile(dest_zip, 'w', zipfile.ZIP_DEFLATED) as zipf:
# 1. Include the voxvera/ source directory (excluding host, bin, pycache)
# We DO include vendor here so the source zip is fully functional offline.
# 1. Include the voxvera/ source directory (excluding host, bin,
# bundled Tor artifacts, and pycache). We still include vendor so the
# source zip remains portable without re-resolving Python deps.
for root, dirs, files in os.walk(ROOT):
if "__pycache__" in root or "host" in root or "resources/bin" in root:
root_path = Path(root)
if (
"__pycache__" in root
or "host" in root
or "resources/bin" in root
or root_path == ROOT / "resources" / "tor"
or (ROOT / "resources" / "tor") in root_path.parents
):
continue
for file in files:
file_path = Path(root) / file
+4
View File
@@ -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)
+15
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import os
import platform
import shutil
import subprocess
@@ -33,6 +34,20 @@ class PlatformAdapter:
@property
def support_matrix_path(self) -> Path:
candidates = []
env_path = os.environ.get("VOXVERA_SUPPORT_MATRIX_PATH")
if env_path:
candidates.append(Path(env_path))
candidates.extend(
[
self.repo_root / "support-matrix.json",
Path.cwd() / "support-matrix.json",
Path("/opt/voxvera/support-matrix.json"),
]
)
for candidate in candidates:
if candidate.exists():
return candidate
return self.repo_root / "support-matrix.json"
def _run_capture(self, args: list[str]) -> tuple[bool, str]:
+133
View File
@@ -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