mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 17:50:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec0abd856c | ||
|
|
7724ac78a7 | ||
|
|
c1a5e608de | ||
|
|
44cf452f67 | ||
|
|
4e49762a85 | ||
|
|
2de7ea073c | ||
|
|
2dd0cf4ed8 | ||
|
|
6c4d6d6aef |
No files matched your search
@@ -70,10 +70,27 @@ jobs:
|
||||
--add-data "voxvera/src${SEP}voxvera/src" \
|
||||
--add-data "voxvera/templates${SEP}voxvera/templates" \
|
||||
--add-data "voxvera/resources/tor${SEP}voxvera/resources/tor" \
|
||||
--add-data "support-matrix.json${SEP}." \
|
||||
--add-data "requirements.txt${SEP}." \
|
||||
--name "${{ matrix.artifact_name }}" \
|
||||
voxvera/cli.py
|
||||
|
||||
- name: Validate built binary contract surface (Linux/macOS)
|
||||
if: matrix.os != 'windows-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
./dist/${{ matrix.artifact_name }} platform-status --json
|
||||
./dist/${{ matrix.artifact_name }} doctor --json
|
||||
./dist/${{ matrix.artifact_name }} autostart status --json
|
||||
|
||||
- name: Validate built binary contract surface (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: |
|
||||
& ".\dist\${{ matrix.artifact_name }}" platform-status --json
|
||||
& ".\dist\${{ matrix.artifact_name }}" doctor --json
|
||||
& ".\dist\${{ matrix.artifact_name }}" autostart status --json
|
||||
|
||||
- name: Build .deb (Linux)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
@@ -90,6 +107,13 @@ jobs:
|
||||
chmod +x packaging/build_appimage.sh
|
||||
./packaging/build_appimage.sh dist/${{ matrix.artifact_name }}
|
||||
|
||||
- name: Validate Linux release artifacts
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x scripts/validate-release-artifacts.sh
|
||||
./scripts/validate-release-artifacts.sh
|
||||
|
||||
- name: Build Flatpak (Linux)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
|
||||
@@ -16,10 +16,49 @@ jobs:
|
||||
python-version: '3.12'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y tor
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8
|
||||
pip install -e ".[test]"
|
||||
pip install git+https://github.com/onionshare/onionshare.git#subdirectory=cli
|
||||
- name: Lint
|
||||
run: flake8 voxvera tests --exclude=voxvera/vendor --ignore=E501,W503,W293,E402,E306,E704,E261,F841,E302,F401,E401,F541
|
||||
- name: Test
|
||||
run: pytest tests/ -v
|
||||
- name: Platform smoke
|
||||
run: bash scripts/platform-smoke.sh linux-cli
|
||||
|
||||
platform-contract:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
smoke_target: macos-cli
|
||||
- os: windows-latest
|
||||
smoke_target: windows-cli
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e ".[test]"
|
||||
- name: Verify smoke target listing
|
||||
shell: bash
|
||||
run: bash scripts/platform-smoke.sh list
|
||||
- name: Validate platform-status JSON
|
||||
shell: bash
|
||||
run: python -m voxvera.cli platform-status --json
|
||||
- name: Validate doctor JSON
|
||||
shell: bash
|
||||
run: python -m voxvera.cli doctor --json
|
||||
- name: Validate autostart JSON
|
||||
shell: bash
|
||||
run: python -m voxvera.cli autostart status --json
|
||||
+7
-5
@@ -1,5 +1,8 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Experimental container path: Linux systemd remains the supported
|
||||
# persistent-host deployment target.
|
||||
|
||||
# Install system dependencies (only runtime)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
tor onionshare-cli \
|
||||
@@ -13,10 +16,9 @@ WORKDIR /opt/voxvera
|
||||
RUN pip install .
|
||||
|
||||
# Prepare flyers volume
|
||||
RUN mkdir /flyers && ln -s /flyers /opt/voxvera/host
|
||||
RUN mkdir /flyers
|
||||
ENV VOXVERA_DIR=/flyers
|
||||
VOLUME /flyers
|
||||
|
||||
# VoxVera expects to run in /opt/voxvera for local assets
|
||||
WORKDIR /opt/voxvera
|
||||
|
||||
CMD ["voxvera", "quickstart"]
|
||||
# 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"]
|
||||
@@ -10,11 +10,17 @@ Generate printable flyers with QR codes linking to Tor (.onion) hidden services.
|
||||
|
||||
VoxVera is designed for maximum portability and censorship resistance.
|
||||
|
||||
Linux with systemd is the supported persistent-host deployment target.
|
||||
Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey remain experimental until their background Tor hosting and auto-recovery paths are validated end-to-end.
|
||||
|
||||
See [docs/platform-support-matrix.md](docs/platform-support-matrix.md) for the canonical platform status table and [docs/roadmap-platform-parity.md](docs/roadmap-platform-parity.md) for the parity plan.
|
||||
For the supported Linux hosting path, use [docs/linux-hosting-validation.md](docs/linux-hosting-validation.md) as the operational validation runbook.
|
||||
|
||||
### 1. Standalone Binaries (Recommended)
|
||||
Download the zero-dependency executable for your OS:
|
||||
Download the executable for your OS:
|
||||
- 🐧 [**Linux**](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-linux)
|
||||
- 🪟 [**Windows**](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-windows.exe)
|
||||
- 🍏 [**macOS**](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-macos)
|
||||
- 🪟 [**Windows (Experimental)**](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-windows.exe)
|
||||
- 🍏 [**macOS (Experimental)**](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-macos)
|
||||
|
||||
### 2. Universal Mirroring (The "Viral" Method)
|
||||
If you found a physical VoxVera flyer, scan it and click **"Download"**. You will receive a `voxvera-portable.zip` containing the full tool, all dependencies, and all 14 languages—ready to run offline.
|
||||
@@ -23,6 +29,7 @@ If you found a physical VoxVera flyer, scan it and click **"Download"**. You wil
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.sh | bash
|
||||
```
|
||||
On Linux, this now installs the recurring `systemd --user` recovery timer used to retry hidden-service startup after boot or offline periods. On macOS, the installer path is still experimental.
|
||||
|
||||
### 4. Developer / Python Install
|
||||
```bash
|
||||
|
||||
+7
-1
@@ -46,7 +46,9 @@ python3 -m voxvera.cli build-site
|
||||
|
||||
## 3. Build Distribution Binaries
|
||||
|
||||
Standardized binaries and AppImages must be generated for the current platform.
|
||||
Linux CLI delivery is the supported deployment target today. Other package formats should be treated as experimental release artifacts until their Tor/OnionShare background-hosting behavior is validated end-to-end.
|
||||
Capture platform evidence before promoting any support tier: run `bash scripts/platform-smoke.sh linux-cli` locally for the supported Linux path, and keep CI/workflow evidence aligned with the same command.
|
||||
As other runtime targets mature, use `bash scripts/platform-smoke.sh macos-cli`, `bash scripts/platform-smoke.sh windows-cli`, or `bash scripts/platform-smoke.sh docker-cli` on their native environments before changing their support labels.
|
||||
|
||||
### Standard Binary
|
||||
Generates an architecture-specific binary (e.g., `voxvera-linux-x86_64`) in `voxvera/resources/bin/`.
|
||||
@@ -60,24 +62,28 @@ Wraps the standard binary into a self-contained AppImage.
|
||||
```bash
|
||||
./packaging/build_appimage.sh
|
||||
```
|
||||
Experimental package surface.
|
||||
|
||||
### Flatpak (Linux)
|
||||
Generates a standalone `.flatpak` bundle. Requires `flatpak-builder`.
|
||||
```bash
|
||||
./packaging/build_flatpak.sh
|
||||
```
|
||||
Experimental package surface.
|
||||
|
||||
### Debian Package (Linux)
|
||||
Generates a `.deb` package for Debian/Ubuntu systems.
|
||||
```bash
|
||||
./packaging/build_deb.sh
|
||||
```
|
||||
Experimental package surface unless its runtime dependencies and recovery behavior are explicitly validated for the release.
|
||||
|
||||
### DMG (macOS)
|
||||
Generates a `.dmg` installer for macOS (must be run on macOS).
|
||||
```bash
|
||||
./packaging/build_dmg.sh
|
||||
```
|
||||
Experimental package surface.
|
||||
|
||||
## 4. Release Execution
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Upcoming Release Notes
|
||||
@@ -26,6 +26,9 @@ fi
|
||||
|
||||
echo "VoxVera command: $voxvera_cmd"
|
||||
|
||||
echo "Checking platform contract before workflow..."
|
||||
bash scripts/platform-smoke.sh linux-cli
|
||||
|
||||
# Generate demo flyer
|
||||
echo "Initializing demo flyer..."
|
||||
"$voxvera_cmd" --lang en init --template voxvera <<EOI
|
||||
@@ -104,6 +107,9 @@ if [ "${VOXVERA_E2E_OFFLINE:-}" != "1" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checking platform contract with live Tor..."
|
||||
VOXVERA_REQUIRE_TOR=1 bash scripts/platform-smoke.sh linux-cli
|
||||
|
||||
# Start OnionShare
|
||||
echo "Starting OnionShare..."
|
||||
# Explicitly set environment variables that OnionShare-cli respects
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
+15
-1
@@ -6,18 +6,22 @@ This guide covers common CLI workflows. See `docs/docker.md` for Docker instruct
|
||||
|
||||
VoxVera is designed to be highly portable. You can run it directly from the source code on any platform with Python installed.
|
||||
|
||||
Linux with `systemd --user` is the supported persistent-host environment. Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and should not yet be treated as equally reliable hidden-service deployment targets.
|
||||
|
||||
### 1. Automated Install (Recommended)
|
||||
The easiest way to install VoxVera is using our cross-platform installer script:
|
||||
The easiest way to install VoxVera is using the Linux-first installer script:
|
||||
|
||||
**Linux / macOS:**
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.sh | bash
|
||||
```
|
||||
On Linux, this also installs the recurring recovery timer used to retry hidden-service startup after boot and after missed/offline periods.
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.ps1 | iex
|
||||
```
|
||||
The Windows installer is experimental and does not yet guarantee production-ready background hosting behavior.
|
||||
|
||||
### 2. Manual Source Install
|
||||
If you have downloaded the source code ZIP:
|
||||
@@ -50,6 +54,16 @@ If you wish to remove VoxVera from your system, you can use the provided uninsta
|
||||
```
|
||||
This automatically detects your Tor instance, starts OnionShare, and writes the generated .onion address into the flyer's tear-off links.
|
||||
|
||||
### Persistent Hosting On Linux
|
||||
|
||||
For a machine that should resume hosting automatically after boot or after being offline, install the autostart recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
```
|
||||
|
||||
This installs a `systemd --user` timer that reruns `voxvera start-all` every few minutes. The retry loop is the supported way to recover hidden-service hosting after suspend, reboot, or temporary network loss.
|
||||
|
||||
## Language Support
|
||||
|
||||
VoxVera is fully localized. You can change your language preference permanently using either the interactive selector or a direct shortcut:
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
+15
-1
@@ -6,18 +6,22 @@ This guide covers common CLI workflows. See `docs/docker.md` for Docker instruct
|
||||
|
||||
VoxVera is designed to be highly portable. You can run it directly from the source code on any platform with Python installed.
|
||||
|
||||
Linux with `systemd --user` is the supported persistent-host environment. Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and should not yet be treated as equally reliable hidden-service deployment targets.
|
||||
|
||||
### 1. Automated Install (Recommended)
|
||||
The easiest way to install VoxVera is using our cross-platform installer script:
|
||||
The easiest way to install VoxVera is using the Linux-first installer script:
|
||||
|
||||
**Linux / macOS:**
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.sh | bash
|
||||
```
|
||||
On Linux, this also installs the recurring recovery timer used to retry hidden-service startup after boot and after missed/offline periods.
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.ps1 | iex
|
||||
```
|
||||
The Windows installer is experimental and does not yet guarantee production-ready background hosting behavior.
|
||||
|
||||
### 2. Manual Source Install
|
||||
If you have downloaded the source code ZIP:
|
||||
@@ -50,6 +54,16 @@ If you wish to remove VoxVera from your system, you can use the provided uninsta
|
||||
```
|
||||
This automatically detects your Tor instance, starts OnionShare, and writes the generated .onion address into the flyer's tear-off links.
|
||||
|
||||
### Persistent Hosting On Linux
|
||||
|
||||
For a machine that should resume hosting automatically after boot or after being offline, install the autostart recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
```
|
||||
|
||||
This installs a `systemd --user` timer that reruns `voxvera start-all` every few minutes. The retry loop is the supported way to recover hidden-service hosting after suspend, reboot, or temporary network loss.
|
||||
|
||||
## Language Support
|
||||
|
||||
VoxVera is fully localized. You can change your language preference permanently using either the interactive selector or a direct shortcut:
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
# Linux Hosting Validation
|
||||
|
||||
This runbook defines the operational validation standard for VoxVera's only supported hosting target:
|
||||
|
||||
- Linux CLI with `systemd --user`
|
||||
|
||||
Everything else remains below that bar until equivalent evidence exists.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `voxvera` installed and on `PATH`
|
||||
- `tor` installed
|
||||
- `onionshare-cli` available
|
||||
- a user account that can run `systemctl --user`
|
||||
|
||||
## Quick Verification
|
||||
|
||||
Use the helper script first:
|
||||
|
||||
```bash
|
||||
bash scripts/linux-recovery-check.sh
|
||||
```
|
||||
|
||||
This checks:
|
||||
|
||||
- `platform-status --json`
|
||||
- `doctor --json`
|
||||
- `autostart status --json`
|
||||
- expected Linux support tier and platform id
|
||||
- presence of `~/host`
|
||||
- Tor and OnionShare discovery
|
||||
- autostart artifacts and timer state
|
||||
- per-site state for `.onionshare-session`, `onionshare.log`, and `server.pid`
|
||||
|
||||
## Core Commands
|
||||
|
||||
Install autostart:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
```
|
||||
|
||||
Inspect autostart:
|
||||
|
||||
```bash
|
||||
voxvera autostart status
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
Remove autostart:
|
||||
|
||||
```bash
|
||||
voxvera autostart uninstall
|
||||
```
|
||||
|
||||
Inspect runtime diagnostics:
|
||||
|
||||
```bash
|
||||
voxvera doctor
|
||||
voxvera doctor --json
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
### 1. Fresh install
|
||||
|
||||
Run the Linux installer or install through the supported CLI path, then confirm:
|
||||
|
||||
```bash
|
||||
voxvera platform-status --json
|
||||
voxvera doctor --json
|
||||
voxvera autostart status --json
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- `platform_id` is `linux_cli_systemd`
|
||||
- support tier is `supported`
|
||||
- `voxvera_cli`, `onionshare_cli`, and `tor_binary` pass
|
||||
|
||||
### 2. Build and serve
|
||||
|
||||
Create or edit a site, then build and serve it:
|
||||
|
||||
```bash
|
||||
voxvera build
|
||||
voxvera serve
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- site output appears under `~/host/<folder_name>/`
|
||||
- `index.html` and QR code images exist
|
||||
- `onionshare.log` is written
|
||||
- `.onionshare-session` appears after the hidden service is started
|
||||
|
||||
### 3. Identity persistence
|
||||
|
||||
Confirm the site keeps its identity across rebuilds:
|
||||
|
||||
```bash
|
||||
ls -la ~/host/<folder_name>/
|
||||
voxvera build --config ~/host/<folder_name>/config.json
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- `.onionshare-session` still exists after rebuild
|
||||
|
||||
### 4. Autostart persistence
|
||||
|
||||
Confirm the recovery timer exists and is enabled:
|
||||
|
||||
```bash
|
||||
voxvera autostart status
|
||||
systemctl --user is-enabled voxvera-start.timer
|
||||
systemctl --user is-active voxvera-start.timer
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- timer artifacts exist under `~/.config/systemd/user/`
|
||||
- timer is enabled
|
||||
|
||||
### 5. Reboot recovery
|
||||
|
||||
After a reboot, verify:
|
||||
|
||||
```bash
|
||||
bash scripts/linux-recovery-check.sh
|
||||
systemctl --user status voxvera-start.timer
|
||||
journalctl --user -u voxvera-start.service --no-pager -n 50
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- timer remains enabled
|
||||
- service has retried `start-all`
|
||||
- configured sites are restorable without manual reconfiguration
|
||||
|
||||
### 6. Offline/reconnect recovery
|
||||
|
||||
Temporarily disconnect the host from the network, reconnect, then wait through at least one timer interval.
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
systemctl --user status voxvera-start.timer
|
||||
journalctl --user -u voxvera-start.service --no-pager -n 50
|
||||
bash scripts/linux-recovery-check.sh
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- timer is still active
|
||||
- `start-all` retries appear in the service logs
|
||||
- Tor becomes reachable again
|
||||
|
||||
## Paths That Matter
|
||||
|
||||
- `~/.config/systemd/user/voxvera-start.service`
|
||||
- `~/.config/systemd/user/voxvera-start.timer`
|
||||
- `~/host/<folder_name>/config.json`
|
||||
- `~/host/<folder_name>/.onionshare-session`
|
||||
- `~/host/<folder_name>/onionshare.log`
|
||||
- `~/host/<folder_name>/server.pid`
|
||||
|
||||
## Failure Interpretation
|
||||
|
||||
If `bash scripts/linux-recovery-check.sh` fails:
|
||||
|
||||
- missing `onionshare_cli` or `tor_binary` means the runtime is incomplete
|
||||
- missing `host_root` means no sites are built yet
|
||||
- missing autostart artifacts means recovery was never installed or was removed
|
||||
- `tor_socks_reachable` warning means Tor is not currently reachable even if it is installed
|
||||
|
||||
Linux should remain the only supported target until this runbook is repeatable on real hosts without caveats.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Platform Support Matrix
|
||||
|
||||
This file is the canonical human-readable summary of VoxVera's actual platform support posture.
|
||||
|
||||
The machine-readable companion file is [`support-matrix.json`](/home/user/Documents/GitHub/VoxVera/support-matrix.json).
|
||||
|
||||
## Support Tiers
|
||||
|
||||
- **Supported**: validated for persistent hidden-service hosting with documented recovery behavior.
|
||||
- **Beta**: feature-complete enough for broader testing, but not yet validated for production-grade recovery claims.
|
||||
- **Experimental**: may install or run, but is not yet validated for reliable background hidden-service hosting.
|
||||
- **Unsupported**: not currently a target for reliable hosting.
|
||||
|
||||
## Current Matrix
|
||||
|
||||
| Platform | Tier | Canonical Runtime Model | Distribution Surfaces | Notes |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| Linux CLI with `systemd --user` | Supported | System Tor or Tor Browser plus `onionshare-cli`, with recurring `systemd --user` timer recovery | `install.sh`, `voxvera-install.sh`, `pip/pipx`, Linux binary | Current reference implementation |
|
||||
| Docker CLI | Experimental | Containerized runtime with `VOXVERA_DIR=/flyers` and periodic `start-all` retries | Dockerfile, GHCR image | Needs end-to-end restart validation |
|
||||
| macOS CLI | Experimental | CLI runtime with `launchd` autostart path and external Tor/OnionShare dependencies | `install.sh`, `pip/pipx`, macOS binary, DMG | No validated recovery claim yet |
|
||||
| Windows CLI | Experimental | CLI runtime with Task Scheduler path and external Tor/OnionShare dependencies | `install.ps1`, Windows binary, Chocolatey | Binary naming and runtime model need cleanup |
|
||||
| Electron GUI | Experimental | GUI frontend over the CLI runtime | `gui/electron` | Must converge on shared runtime behavior |
|
||||
| Flatpak/AppImage/Homebrew/Chocolatey/Deb | Experimental | Distribution-only until runtime parity exists | packaging scripts/manifests | Package availability does not imply hosting parity |
|
||||
|
||||
## What "Supported" Means For VoxVera
|
||||
|
||||
To move a platform to **Supported**, VoxVera should have evidence for all of the following:
|
||||
|
||||
1. Fresh install works on a clean machine.
|
||||
2. Manual `build` and `serve` work.
|
||||
3. `.onionshare-session` survives rebuilds and restarts.
|
||||
4. Hosting returns after reboot or login.
|
||||
5. Hosting returns after a temporary offline period.
|
||||
6. Logs and status are inspectable with documented commands.
|
||||
7. The docs and release artifacts do not overstate capability.
|
||||
|
||||
## Promotion Rules
|
||||
|
||||
- Do not promote a package surface before the underlying runtime model is validated.
|
||||
- Do not call a platform "recommended" unless it is also `supported`.
|
||||
- If a platform lacks validated auto-recovery after reboot or network interruption, it must remain `experimental` or `beta`.
|
||||
|
||||
## Current Recommendation
|
||||
|
||||
If your goal is reliable Tor hidden-service hosting today, use the Linux CLI path and install the recurring recovery timer with:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
```
|
||||
|
||||
That is the current reference deployment model the rest of the platform work should converge toward.
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
# Platform Parity Roadmap
|
||||
|
||||
This roadmap focuses on one question:
|
||||
|
||||
How do we expand VoxVera beyond Linux without overstating reliability, while still converging on real cross-platform parity for Tor hidden-service hosting?
|
||||
|
||||
## Strategic Principle
|
||||
|
||||
Parity is not a packaging milestone. It is a runtime-contract milestone.
|
||||
|
||||
Every platform must eventually implement the same hosting contract:
|
||||
|
||||
1. install VoxVera
|
||||
2. ensure Tor and OnionShare are available
|
||||
3. build site assets
|
||||
4. serve the hidden service
|
||||
5. preserve `.onionshare-session`
|
||||
6. recover hosting after reboot, login, suspend, or offline periods
|
||||
7. surface logs and status
|
||||
|
||||
Package formats should inherit that contract. They should not define independent behavior.
|
||||
|
||||
## Phase 0: Truth Alignment
|
||||
|
||||
Goal: bring repo claims, docs, and release posture into line with current reality.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- canonical support matrix
|
||||
- machine-readable support declaration
|
||||
- installer/package warnings for experimental paths
|
||||
- README and docs aligned to the actual supported target
|
||||
|
||||
Definition of done:
|
||||
|
||||
- no docs imply parity that tests do not support
|
||||
- every platform surface has an explicit support tier
|
||||
- release artifacts are not treated as proof of hosting reliability
|
||||
|
||||
## Phase 1: Harden Linux As Reference
|
||||
|
||||
Goal: make Linux CLI with `systemd --user` the clearly defensible model that other platforms copy.
|
||||
|
||||
Work items:
|
||||
|
||||
1. Add `voxvera doctor` for runtime validation.
|
||||
- Check Tor reachability.
|
||||
- Check OnionShare availability.
|
||||
- Check `start-all` surfaces.
|
||||
- Check autostart timer installation.
|
||||
- Check host directories and session files.
|
||||
|
||||
2. Add `voxvera autostart status`.
|
||||
- Show service and timer files.
|
||||
- Show whether the timer is enabled.
|
||||
- Show next/last run timestamps if available.
|
||||
|
||||
3. Normalize Linux log/status guidance.
|
||||
- Document host log paths.
|
||||
- Document timer/service status commands.
|
||||
- Document expected recovery flow after reboot/network loss.
|
||||
|
||||
4. Validate restart behavior.
|
||||
- fresh install
|
||||
- first serve
|
||||
- reboot
|
||||
- suspend/resume if possible
|
||||
- temporary offline period followed by reconnect
|
||||
|
||||
Definition of done:
|
||||
|
||||
- Linux remains the only `supported` path until the above evidence exists and is documented.
|
||||
|
||||
## Phase 2: Platform Adapter Layer
|
||||
|
||||
Goal: stop encoding platform logic in ad hoc installer scripts.
|
||||
|
||||
Recommended structure:
|
||||
|
||||
- `voxvera/platforms/base.py`
|
||||
- `voxvera/platforms/linux.py`
|
||||
- `voxvera/platforms/macos.py`
|
||||
- `voxvera/platforms/windows.py`
|
||||
- `voxvera/platforms/docker.py`
|
||||
|
||||
Each adapter should define:
|
||||
|
||||
- support tier
|
||||
- Tor discovery strategy
|
||||
- OnionShare discovery strategy
|
||||
- state root
|
||||
- autostart installation strategy
|
||||
- autostart status strategy
|
||||
- log/status surface
|
||||
- known limitations
|
||||
|
||||
CLI surfaces to add:
|
||||
|
||||
- `voxvera doctor`
|
||||
- `voxvera platform-status`
|
||||
- `voxvera autostart`
|
||||
- `voxvera autostart status`
|
||||
- `voxvera autostart uninstall`
|
||||
|
||||
Definition of done:
|
||||
|
||||
- shell installers become thin wrappers over shared Python logic instead of owning runtime policy themselves
|
||||
|
||||
## Phase 3: Docker Beta Candidate
|
||||
|
||||
Goal: make Docker a coherent, testable deployment surface.
|
||||
|
||||
Decisions to settle:
|
||||
|
||||
- single-container runtime vs compose-based runtime
|
||||
- host Tor vs container Tor
|
||||
- log and state persistence model
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- keep runtime state under one explicit volume root
|
||||
- document restart policy clearly
|
||||
- make health and recovery behavior observable
|
||||
|
||||
Validation checklist:
|
||||
|
||||
- container restart preserves onion identity
|
||||
- host reboot with restart policy restores hosting
|
||||
- state remains in mounted volume
|
||||
- logs remain inspectable
|
||||
|
||||
Promotion target:
|
||||
|
||||
- `experimental` -> `beta`
|
||||
|
||||
## Phase 4: macOS CLI
|
||||
|
||||
Goal: establish one honest CLI-based macOS hosting model before packaging.
|
||||
|
||||
Work items:
|
||||
|
||||
1. define Tor discovery strategy
|
||||
2. define OnionShare discovery/install strategy
|
||||
3. validate manual `build` + `serve`
|
||||
4. implement and validate `launchd` recovery
|
||||
5. document known limitations
|
||||
|
||||
Promotion target:
|
||||
|
||||
- remain `experimental` until `launchd` recovery is validated
|
||||
- move to `beta` only after login/reboot/offline recovery evidence exists
|
||||
|
||||
## Phase 5: Windows CLI
|
||||
|
||||
Goal: establish one canonical Windows runtime model before claiming package parity.
|
||||
|
||||
Work items:
|
||||
|
||||
1. unify binary naming across installer, Chocolatey, docs, and release assets
|
||||
2. define Tor discovery strategy
|
||||
3. define OnionShare discovery strategy
|
||||
4. validate manual `build` + `serve`
|
||||
5. implement Task Scheduler recovery path
|
||||
6. add Windows-specific status/log guidance
|
||||
|
||||
Promotion target:
|
||||
|
||||
- remain `experimental` until scheduled recovery and state persistence are proven
|
||||
- move to `beta` only after reboot/login/offline recovery evidence exists
|
||||
|
||||
## Phase 6: Electron GUI Convergence
|
||||
|
||||
Goal: make the GUI a frontend to the supported runtime, not a separate runtime.
|
||||
|
||||
Work items:
|
||||
|
||||
1. route the GUI through shared CLI/runtime surfaces
|
||||
2. reduce duplicated Tor startup behavior
|
||||
3. add visibility for:
|
||||
- Tor status
|
||||
- OnionShare status
|
||||
- hosting status
|
||||
- onion URL
|
||||
- logs
|
||||
4. make window-close behavior explicit and safe
|
||||
|
||||
Promotion target:
|
||||
|
||||
- GUI remains `experimental` until it stops diverging from the CLI runtime contract
|
||||
|
||||
## Phase 7: Package Surface Promotion
|
||||
|
||||
Goal: package formats should become trustworthy wrappers around validated runtimes.
|
||||
|
||||
Work items:
|
||||
|
||||
1. Debian package
|
||||
- declare actual runtime dependencies
|
||||
- validate install + host + recovery path
|
||||
|
||||
2. AppImage
|
||||
- decide whether it is distribution-only or a real hosting target
|
||||
|
||||
3. Homebrew
|
||||
- fix artifact URL/sha/version policy
|
||||
- clarify whether it installs a hosting target or just a CLI
|
||||
|
||||
4. Chocolatey
|
||||
- align artifact naming and installer semantics
|
||||
|
||||
5. Flatpak
|
||||
- decide whether Tor/OnionShare can be supported sanely in-sandbox
|
||||
|
||||
Rule:
|
||||
|
||||
- no package surface should be promoted above the support tier of its underlying runtime
|
||||
|
||||
## Testing Program
|
||||
|
||||
### 1. Unit tests
|
||||
|
||||
- config/state logic
|
||||
- autostart file generation
|
||||
- platform adapter selection
|
||||
|
||||
### 2. Mocked integration tests
|
||||
|
||||
- `serve`
|
||||
- `autostart`
|
||||
- `doctor`
|
||||
- `platform-status`
|
||||
|
||||
### 3. Platform smoke tests
|
||||
|
||||
- fresh install
|
||||
- build
|
||||
- serve
|
||||
- persist identity
|
||||
- restart
|
||||
- recover
|
||||
|
||||
Current smoke target names:
|
||||
|
||||
- `linux-cli`
|
||||
- `macos-cli`
|
||||
- `windows-cli`
|
||||
- `docker-cli`
|
||||
|
||||
These target names are contract-level validation entry points. Only `linux-cli` currently represents a supported hosting path; the others should be used to validate experimental posture and dependency discovery on their native platforms until their recovery behavior is proven.
|
||||
|
||||
### 4. Release checks
|
||||
|
||||
- support matrix is current
|
||||
- docs match support tiers
|
||||
- artifact names match scripts and package manifests
|
||||
|
||||
## Immediate Next Tasks
|
||||
|
||||
1. Add `voxvera doctor`.
|
||||
2. Add `voxvera autostart status`.
|
||||
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.
|
||||
6. Normalize Windows binary naming before further Windows work.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- claiming parity before recovery behavior is validated
|
||||
- treating package availability as support proof
|
||||
- maintaining separate runtime semantics in GUI vs CLI
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
Vendored
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
Vendored
+15
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
Vendored
+15
-1
@@ -6,18 +6,22 @@ This guide covers common CLI workflows. See `docs/docker.md` for Docker instruct
|
||||
|
||||
VoxVera is designed to be highly portable. You can run it directly from the source code on any platform with Python installed.
|
||||
|
||||
Linux with `systemd --user` is the supported persistent-host environment. Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and should not yet be treated as equally reliable hidden-service deployment targets.
|
||||
|
||||
### 1. Automated Install (Recommended)
|
||||
The easiest way to install VoxVera is using our cross-platform installer script:
|
||||
The easiest way to install VoxVera is using the Linux-first installer script:
|
||||
|
||||
**Linux / macOS:**
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.sh | bash
|
||||
```
|
||||
On Linux, this also installs the recurring recovery timer used to retry hidden-service startup after boot and after missed/offline periods.
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.ps1 | iex
|
||||
```
|
||||
The Windows installer is experimental and does not yet guarantee production-ready background hosting behavior.
|
||||
|
||||
### 2. Manual Source Install
|
||||
If you have downloaded the source code ZIP:
|
||||
@@ -50,6 +54,16 @@ If you wish to remove VoxVera from your system, you can use the provided uninsta
|
||||
```
|
||||
This automatically detects your Tor instance, starts OnionShare, and writes the generated .onion address into the flyer's tear-off links.
|
||||
|
||||
### Persistent Hosting On Linux
|
||||
|
||||
For a machine that should resume hosting automatically after boot or after being offline, install the autostart recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
```
|
||||
|
||||
This installs a `systemd --user` timer that reruns `voxvera start-all` every few minutes. The retry loop is the supported way to recover hidden-service hosting after suspend, reboot, or temporary network loss.
|
||||
|
||||
## Language Support
|
||||
|
||||
VoxVera is fully localized. You can change your language preference permanently using either the interactive selector or a direct shortcut:
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
+6
-3
@@ -1,5 +1,7 @@
|
||||
# Docker Usage
|
||||
|
||||
Experimental: the container path is not yet a production-grade hidden-service deployment target. Linux with `systemd --user` remains the supported persistent-host environment.
|
||||
|
||||
A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published to
|
||||
`ghcr.io/voxvera/voxvera`.
|
||||
|
||||
@@ -7,10 +9,11 @@ A prebuilt image containing Tor, OnionShare and the `voxvera` CLI is published t
|
||||
# pull the image
|
||||
docker pull ghcr.io/voxvera/voxvera:latest
|
||||
|
||||
# run voxvera quickstart with flyers stored in ./flyers
|
||||
# run the experimental container with flyers stored in ./flyers
|
||||
mkdir -p flyers
|
||||
docker run -it --rm -v "$(pwd)/flyers:/flyers" ghcr.io/voxvera/voxvera
|
||||
```
|
||||
|
||||
The container uses `/flyers` as the working directory and runs `voxvera quickstart`
|
||||
by default. All generated flyer files will appear in the mounted `flyers` folder.
|
||||
The container stores runtime state under `/flyers`, seeds a default `config.json` if needed, and retries `voxvera start-all` periodically after the initial `quickstart`.
|
||||
|
||||
Use this path for testing only until the container runtime and background recovery behavior are validated end-to-end.
|
||||
@@ -6,6 +6,10 @@ This page collects common issues encountered when hosting or accessing flyers.
|
||||
- Ensure Tor is allowed through your firewall. On systems using `ufw` you may need to run `sudo ufw allow tor`.
|
||||
- Some networks block Tor entirely. If you cannot reach onion services, try connecting over a different network or use a Tor bridge.
|
||||
|
||||
## Supported deployment target
|
||||
- The supported persistent-host setup is Linux with `systemd --user`.
|
||||
- Windows, macOS, Docker, Flatpak, AppImage, Homebrew, and Chocolatey are currently experimental and may not recover hosting automatically after reboot or network interruptions.
|
||||
|
||||
## Tor port detection
|
||||
|
||||
`voxvera serve` auto-detects Tor by probing common ports (9050/9150 for SOCKS, 9051/9151 for control). If detection fails -- for example when Tor runs on non-standard ports -- you can override with environment variables:
|
||||
@@ -39,6 +43,17 @@ keypair (and URL) will be generated on the next `voxvera serve`. The
|
||||
`voxvera import` command preserves this file automatically, but manual
|
||||
deletion of the `host/` directory will destroy it.
|
||||
|
||||
## Hosting does not return after reboot or network loss
|
||||
On Linux, install or verify the recovery timer:
|
||||
|
||||
```bash
|
||||
voxvera autostart
|
||||
systemctl --user status voxvera-start.timer
|
||||
systemctl --user status voxvera-start.service
|
||||
```
|
||||
|
||||
The supported recovery path is the recurring `systemd --user` timer, which reruns `voxvera start-all` every few minutes.
|
||||
|
||||
## QR codes not generated
|
||||
If `voxvera build` reports "No URLs configured yet", this is expected before the first `voxvera serve`. QR codes for tear-off links are generated automatically once the .onion address is known. QR codes for the content link are generated if you provided a URL during `voxvera init`.
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ This directory contains a minimal Electron wrapper around the `voxvera` CLI.
|
||||
It exposes a simple "Quickstart" button so non-technical users can generate
|
||||
flyers without touching the command line.
|
||||
|
||||
Status: experimental. The GUI is not the supported path for reliable background hidden-service hosting or automatic recovery after reboot/network loss.
|
||||
The GUI now reads `voxvera platform-status --json`, `voxvera doctor --json`, and `voxvera autostart status --json` so its runtime messaging reflects the same platform contract as the CLI instead of inventing a separate compatibility story.
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
|
||||
@@ -4,6 +4,36 @@
|
||||
<meta charset="UTF-8">
|
||||
<title>VoxVera GUI</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 1rem;
|
||||
}
|
||||
#status {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #fafafa;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.status-supported {
|
||||
background: #dff3e4;
|
||||
color: #14532d;
|
||||
}
|
||||
.status-experimental {
|
||||
background: #fff4d6;
|
||||
color: #854d0e;
|
||||
}
|
||||
.status-error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
#log {
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px;
|
||||
@@ -16,6 +46,13 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>VoxVera</h1>
|
||||
<div id="status">
|
||||
<strong>Runtime status</strong>
|
||||
<div id="platform-tier"></div>
|
||||
<div id="platform-model"></div>
|
||||
<div id="autostart-state"></div>
|
||||
<div id="doctor-summary"></div>
|
||||
</div>
|
||||
<form id="config-form">
|
||||
<div><label>Name <input type="text" id="name"></label></div>
|
||||
<div><label>Subdomain <input type="text" id="folder_name"></label></div>
|
||||
@@ -32,12 +69,56 @@
|
||||
<p id="onion-address"></p>
|
||||
<pre id="log"></pre>
|
||||
<script>
|
||||
function setStatusBadge(targetId, label, tier) {
|
||||
const el = document.getElementById(targetId);
|
||||
const className = tier === 'supported' ? 'status-supported'
|
||||
: tier === 'experimental' ? 'status-experimental'
|
||||
: 'status-error';
|
||||
el.innerHTML = `<span class="status-badge ${className}">${label}</span>`;
|
||||
}
|
||||
|
||||
function summarizeDoctorChecks(doctor) {
|
||||
const sections = doctor && doctor.sections ? Object.values(doctor.sections) : [];
|
||||
if (sections.length > 0) {
|
||||
const failedSections = sections.filter(section => section.checks && section.checks.length > 0 && !section.ok);
|
||||
if (failedSections.length === 0) return 'All doctor sections passed.';
|
||||
return `Doctor warnings in: ${failedSections.map(section => section.title).join(', ')}`;
|
||||
}
|
||||
const checks = doctor && Array.isArray(doctor.checks) ? doctor.checks : [];
|
||||
if (checks.length === 0) return 'No diagnostics available.';
|
||||
const failed = checks.filter(check => !check.ok);
|
||||
if (failed.length === 0) return 'All doctor checks passed.';
|
||||
return `Doctor reports ${failed.length} failing check(s): ${failed.map(check => check.name).join(', ')}`;
|
||||
}
|
||||
|
||||
async function loadRuntimeStatus() {
|
||||
const runtime = await window.voxvera.loadRuntimeStatus();
|
||||
if (!runtime.ok) {
|
||||
setStatusBadge('platform-tier', 'error', 'error');
|
||||
document.getElementById('platform-model').textContent = runtime.error;
|
||||
document.getElementById('autostart-state').textContent = '';
|
||||
document.getElementById('doctor-summary').textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const { platformStatus, autostart, doctor } = runtime;
|
||||
setStatusBadge('platform-tier', platformStatus.tier, platformStatus.tier);
|
||||
document.getElementById('platform-model').textContent = platformStatus.hosting_model || '';
|
||||
if (autostart && !autostart.error) {
|
||||
document.getElementById('autostart-state').textContent = `Autostart: ${autostart.message}`;
|
||||
} else if (autostart && autostart.error) {
|
||||
document.getElementById('autostart-state').textContent = `Autostart status unavailable: ${autostart.error}`;
|
||||
}
|
||||
document.getElementById('doctor-summary').textContent = summarizeDoctorChecks(doctor);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const cfg = await window.voxvera.loadConfig();
|
||||
for (const [k, v] of Object.entries(cfg)) {
|
||||
const el = document.getElementById(k);
|
||||
if (el) el.value = v;
|
||||
}
|
||||
await loadRuntimeStatus();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', load);
|
||||
|
||||
@@ -49,6 +130,7 @@
|
||||
if (el) cfg[id] = el.value;
|
||||
});
|
||||
await window.voxvera.quickstart(cfg);
|
||||
await loadRuntimeStatus();
|
||||
});
|
||||
|
||||
window.voxvera.onOnionUrl(url => {
|
||||
|
||||
+55
-67
@@ -3,13 +3,10 @@ const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const which = require('which');
|
||||
const fs = require('fs');
|
||||
const { launchTor } = require('./tor.js');
|
||||
const { extractOnionUrl } = require('./runtime-utils');
|
||||
|
||||
let mainWindow;
|
||||
let onionProc;
|
||||
// 🔧 merged conflicting changes from codex/start-onionshare-and-generate-static-site vs main
|
||||
let restarting = false;
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
@@ -22,74 +19,42 @@ function createWindow() {
|
||||
mainWindow.loadFile('index.html');
|
||||
}
|
||||
|
||||
// 🔧 merged conflicting changes from codex/start-onionshare-and-generate-static-site vs main
|
||||
function startOnionShare() {
|
||||
const configPath = getConfigPath();
|
||||
function getVoxveraPath() {
|
||||
const voxveraPath = which.sync('voxvera', { nothrow: true });
|
||||
if (!voxveraPath) {
|
||||
dialog.showErrorBox(
|
||||
'voxvera not found',
|
||||
'Install the voxvera CLI and ensure it is in your PATH.'
|
||||
);
|
||||
return;
|
||||
throw new Error('Install the voxvera CLI and ensure it is in your PATH.');
|
||||
}
|
||||
const build = spawn(voxveraPath, ['--config', configPath, 'build']);
|
||||
build.on('error', err => {
|
||||
dialog.showErrorBox('voxvera build error', err.message);
|
||||
});
|
||||
build.on('close', code => {
|
||||
if (code !== 0) {
|
||||
dialog.showErrorBox('voxvera build error', `build exited with code ${code}.`);
|
||||
return;
|
||||
}
|
||||
runServe();
|
||||
});
|
||||
return voxveraPath;
|
||||
}
|
||||
|
||||
async function runServe (retry = false) {
|
||||
let torProc, socksPort, controlPort;
|
||||
try {
|
||||
({ torProc, socksPort, controlPort } = await launchTor());
|
||||
} catch (err) {
|
||||
dialog.showErrorBox('Tor error', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const env = { ...process.env,
|
||||
TOR_SOCKS_PORT: socksPort.toString(),
|
||||
TOR_CONTROL_PORT: controlPort.toString() };
|
||||
|
||||
const configPath = getConfigPath();
|
||||
const voxveraPath = which.sync('voxvera', { nothrow: true });
|
||||
onionProc = spawn(voxveraPath, ['--config', configPath, 'serve'], { env });
|
||||
|
||||
let gotURL = false;
|
||||
const softTimeout = setTimeout(() => {
|
||||
if (!gotURL) {
|
||||
mainWindow.webContents.send('log',
|
||||
{ text: 'Tor timed out, retrying…', isError: true });
|
||||
onionProc.kill(); torProc.kill();
|
||||
runServe(true);
|
||||
}
|
||||
}, 90_000);
|
||||
|
||||
onionProc.stdout.on('data', buf => {
|
||||
const line = buf.toString();
|
||||
const onionUrl = extractOnionUrl(line);
|
||||
if (onionUrl) {
|
||||
gotURL = true;
|
||||
clearTimeout(softTimeout);
|
||||
mainWindow.webContents.send('onion-url', onionUrl);
|
||||
}
|
||||
mainWindow.webContents.send('log', { text: line });
|
||||
function runJsonCommand(args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
const proc = spawn(getVoxveraPath(), args);
|
||||
proc.stdout.on('data', data => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
proc.stderr.on('data', data => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
proc.on('close', code => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(stderr || `voxvera exited with code ${code}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(stdout));
|
||||
} catch (err) {
|
||||
reject(new Error(`Could not parse VoxVera JSON output: ${err.message}`));
|
||||
}
|
||||
});
|
||||
proc.on('error', reject);
|
||||
});
|
||||
|
||||
onionProc.on('exit', () => torProc.kill());
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
startOnionShare();
|
||||
});
|
||||
|
||||
function getConfigPath() {
|
||||
@@ -118,17 +83,40 @@ ipcMain.handle('load-config', async () => {
|
||||
return config;
|
||||
});
|
||||
|
||||
ipcMain.handle('load-runtime-status', async () => {
|
||||
try {
|
||||
const platformStatus = await runJsonCommand(['platform-status', '--json']);
|
||||
const doctor = await runJsonCommand(['doctor', '--json']);
|
||||
let autostart = null;
|
||||
try {
|
||||
autostart = await runJsonCommand(['autostart', 'status', '--json']);
|
||||
} catch (err) {
|
||||
autostart = { error: err.message };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
platformStatus,
|
||||
doctor,
|
||||
autostart
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
error: err.message
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('run-quickstart', async (_, config) => {
|
||||
const configPath = getConfigPath();
|
||||
if (config && typeof config === 'object') {
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
}
|
||||
const voxveraPath = which.sync('voxvera', { nothrow: true });
|
||||
if (!voxveraPath) {
|
||||
dialog.showErrorBox(
|
||||
'voxvera not found',
|
||||
'Install the voxvera CLI and ensure it is in your PATH.'
|
||||
);
|
||||
let voxveraPath;
|
||||
try {
|
||||
voxveraPath = getVoxveraPath();
|
||||
} catch (err) {
|
||||
dialog.showErrorBox('voxvera not found', err.message);
|
||||
return -1;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('voxvera', {
|
||||
loadConfig: () => ipcRenderer.invoke('load-config'),
|
||||
loadRuntimeStatus: () => ipcRenderer.invoke('load-runtime-status'),
|
||||
quickstart: (config) => ipcRenderer.invoke('run-quickstart', config),
|
||||
onOnionUrl: (cb) => ipcRenderer.on('onion-url', (_, url) => cb(url)),
|
||||
onLog: (cb) => ipcRenderer.on('log', (_, data) => cb(data.text, data.isError))
|
||||
|
||||
+41
-3
@@ -1,6 +1,8 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$packages = @('tor', 'onionshare', 'jq', 'qrencode', 'imagemagick')
|
||||
Write-Warning 'The Windows installer path is experimental. It is not yet validated for reliable background Tor hidden-service hosting or automatic recovery after connectivity changes.'
|
||||
|
||||
$packages = @('tor', 'onionshare')
|
||||
|
||||
function Install-Choco {
|
||||
param([string]$Name)
|
||||
@@ -76,6 +78,41 @@ function Check-LocalBin {
|
||||
}
|
||||
}
|
||||
|
||||
function Show-VoxVeraStatus {
|
||||
if (-not (Get-Command voxvera -ErrorAction SilentlyContinue)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
$platform = voxvera platform-status --json | ConvertFrom-Json
|
||||
Write-Host ("Platform tier: {0} ({1})" -f $platform.tier, $platform.label)
|
||||
if ($platform.hosting_model) {
|
||||
Write-Host ("Hosting model: {0}" -f $platform.hosting_model)
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Could not read platform status from voxvera."
|
||||
}
|
||||
|
||||
try {
|
||||
$doctor = voxvera doctor --json | ConvertFrom-Json
|
||||
$failed = @($doctor.checks | Where-Object { -not $_.ok } | ForEach-Object { $_.name })
|
||||
if ($failed.Count -gt 0) {
|
||||
Write-Host ("Doctor checks needing attention: {0}" -f ($failed -join ', '))
|
||||
} else {
|
||||
Write-Host 'Doctor checks: all passed'
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Could not read doctor output from voxvera."
|
||||
}
|
||||
|
||||
try {
|
||||
$autostart = voxvera autostart status --json | ConvertFrom-Json
|
||||
Write-Host ("Autostart: {0}" -f $autostart.message)
|
||||
} catch {
|
||||
Write-Warning "Could not read autostart status from voxvera."
|
||||
}
|
||||
}
|
||||
|
||||
if (Get-Command pipx -ErrorAction SilentlyContinue) {
|
||||
try {
|
||||
pipx install voxvera --force
|
||||
@@ -84,7 +121,7 @@ if (Get-Command pipx -ErrorAction SilentlyContinue) {
|
||||
$home = [Environment]::GetFolderPath('UserProfile')
|
||||
$dest = Join-Path $home '.local\bin'
|
||||
New-Item -ItemType Directory -Path $dest -Force | Out-Null
|
||||
$url = 'https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera.exe'
|
||||
$url = 'https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-windows.exe'
|
||||
$status = Download-Binary $url "$dest/voxvera.exe"
|
||||
if ($status -eq 200) {
|
||||
Check-LocalBin
|
||||
@@ -100,7 +137,7 @@ if (Get-Command pipx -ErrorAction SilentlyContinue) {
|
||||
$home = [Environment]::GetFolderPath('UserProfile')
|
||||
$dest = Join-Path $home '.local\bin'
|
||||
New-Item -ItemType Directory -Path $dest -Force | Out-Null
|
||||
$url = 'https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera.exe'
|
||||
$url = 'https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-windows.exe'
|
||||
$status = Download-Binary $url "$dest/voxvera.exe"
|
||||
if ($status -eq 200) {
|
||||
Check-LocalBin
|
||||
@@ -114,3 +151,4 @@ if (Get-Command pipx -ErrorAction SilentlyContinue) {
|
||||
}
|
||||
|
||||
Write-Host 'VoxVera installed successfully.'
|
||||
Show-VoxVeraStatus
|
||||
+57
@@ -4,6 +4,7 @@ set -euo pipefail
|
||||
REPO="PR0M3TH3AN/VoxVera"
|
||||
REPO_URL="https://github.com/${REPO}"
|
||||
BRANCH="main"
|
||||
OS_NAME="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
# --- Network resilience settings ---
|
||||
# Kill git operations that stall below 1 KB/s for 30 seconds
|
||||
@@ -49,6 +50,45 @@ msg() { echo -e "\e[32m$*\e[0m"; }
|
||||
warn() { echo -e "\e[33m$*\e[0m" >&2; }
|
||||
die() { echo -e "\e[31m$*\e[0m" >&2; exit 1; }
|
||||
|
||||
report_cli_status() {
|
||||
command_exists voxvera || return 0
|
||||
|
||||
local platform_json doctor_json autostart_json
|
||||
platform_json="$(voxvera platform-status --json 2>/dev/null || true)"
|
||||
doctor_json="$(voxvera doctor --json 2>/dev/null || true)"
|
||||
autostart_json="$(voxvera autostart status --json 2>/dev/null || true)"
|
||||
|
||||
if [ -n "$platform_json" ]; then
|
||||
PLATFORM_JSON="$platform_json" python3 - <<'PY'
|
||||
import json, os
|
||||
data = json.loads(os.environ["PLATFORM_JSON"])
|
||||
print(f"Platform tier: {data.get('tier', 'unknown')} ({data.get('label', 'unknown')})")
|
||||
if data.get("hosting_model"):
|
||||
print(f"Hosting model: {data['hosting_model']}")
|
||||
PY
|
||||
fi
|
||||
|
||||
if [ -n "$doctor_json" ]; then
|
||||
DOCTOR_JSON="$doctor_json" python3 - <<'PY'
|
||||
import json, os
|
||||
data = json.loads(os.environ["DOCTOR_JSON"])
|
||||
failed = [check["name"] for check in data.get("checks", []) if not check.get("ok")]
|
||||
if failed:
|
||||
print("Doctor checks needing attention: " + ", ".join(failed))
|
||||
else:
|
||||
print("Doctor checks: all passed")
|
||||
PY
|
||||
fi
|
||||
|
||||
if [ -n "$autostart_json" ]; then
|
||||
AUTOSTART_JSON="$autostart_json" python3 - <<'PY'
|
||||
import json, os
|
||||
data = json.loads(os.environ["AUTOSTART_JSON"])
|
||||
print("Autostart: " + data.get("message", "unknown"))
|
||||
PY
|
||||
fi
|
||||
}
|
||||
|
||||
install_pkg() {
|
||||
case "$PM" in
|
||||
apt) $SUDO apt-get install -y "$@" ;;
|
||||
@@ -108,6 +148,9 @@ get_latest_tag() {
|
||||
|
||||
# Ensure core dependencies are present
|
||||
msg "Checking and updating system dependencies..."
|
||||
if [[ "$OS_NAME" != linux* ]]; then
|
||||
warn "This installer is optimized for Linux. Non-Linux paths are experimental and are not validated for reliable background hidden-service hosting."
|
||||
fi
|
||||
if [ "$PM" = "apt" ]; then
|
||||
$SUDO apt-get update
|
||||
SYSTEM_PKGS=(tor curl wget git python3-pip python3-venv)
|
||||
@@ -386,9 +429,23 @@ for strategy in install_tarball install_shallow install_pipx install_pip_git ins
|
||||
if command_exists voxvera; then
|
||||
msg "Rebuilding existing sites with updated template..."
|
||||
voxvera rebuild-all 2>/dev/null || true
|
||||
if [[ "$OS_NAME" == linux* ]]; then
|
||||
msg "Installing Linux autostart recovery timer..."
|
||||
voxvera autostart 2>/dev/null || warn "Could not install the VoxVera autostart timer automatically. Run 'voxvera autostart' manually."
|
||||
else
|
||||
warn "Autostart was not configured automatically because this platform is experimental."
|
||||
fi
|
||||
msg "Inspecting installed runtime posture..."
|
||||
report_cli_status
|
||||
fi
|
||||
|
||||
msg "\nVoxVera installed/updated successfully. Run 'voxvera check' to verify."
|
||||
if [[ "$OS_NAME" == linux* ]]; then
|
||||
msg "Linux systemd user autostart should now retry hidden-service startup every few minutes."
|
||||
msg "Inspect with: voxvera autostart status"
|
||||
msg "Remove with: voxvera autostart uninstall"
|
||||
msg "Validate with: bash scripts/linux-recovery-check.sh"
|
||||
fi
|
||||
# Detect if the parent shell will still have a stale hash
|
||||
msg "If 'voxvera' is not found, run: hash -r (or open a new terminal)"
|
||||
exit 0
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "Warning: AppImage packaging is experimental and is not yet validated for reliable background hidden-service hosting."
|
||||
|
||||
# Ensure we are in project root
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "Warning: Debian packaging is currently experimental until runtime dependencies and hidden-service recovery are validated end-to-end."
|
||||
|
||||
# Ensure we are in project root
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "Warning: DMG packaging is experimental and is not yet validated for reliable background hidden-service hosting."
|
||||
|
||||
# Ensure we are in project root
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Experimental package surface: not yet validated for reliable background hidden-service hosting.
|
||||
$toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$url = 'https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-windows.exe'
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
app-id: org.voxvera.VoxVera
|
||||
# Experimental package surface: not yet validated for reliable background hidden-service hosting.
|
||||
runtime: org.freedesktop.Platform
|
||||
runtime-version: '23.08'
|
||||
sdk: org.freedesktop.Sdk
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Voxvera < Formula
|
||||
desc "Generate QR-coded flyers"
|
||||
desc "Experimental VoxVera formula"
|
||||
homepage "https://github.com/PR0M3TH3AN/VoxVera"
|
||||
url "https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera"
|
||||
version "0.1.0"
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
|
||||
|
||||
[project]
|
||||
name = "voxvera"
|
||||
version = "0.3.6"
|
||||
version = "0.3.10"
|
||||
description = "VoxVera CLI utilities"
|
||||
readme = "README.md"
|
||||
authors = [{name = "VoxVera"}]
|
||||
|
||||
@@ -45,6 +45,7 @@ $PY_CMD --onefile \
|
||||
--add-data "voxvera/src:voxvera/src" \
|
||||
--add-data "voxvera/templates:voxvera/templates" \
|
||||
--add-data "voxvera/resources/tor:voxvera/resources/tor" \
|
||||
--add-data "support-matrix.json:." \
|
||||
--add-data "requirements.txt:." \
|
||||
--name "$BINARY_NAME" \
|
||||
--distpath "$ARTIFACT_DIR" \
|
||||
|
||||
@@ -18,6 +18,11 @@ echo "==> electron smoke tests"
|
||||
echo "==> dependency check"
|
||||
python3 -m voxvera.cli --lang en check
|
||||
|
||||
if [[ "$(uname -s)" == "Linux" ]]; then
|
||||
echo "==> platform smoke"
|
||||
bash scripts/platform-smoke.sh linux-cli
|
||||
fi
|
||||
|
||||
echo "==> build docs"
|
||||
python3 -m voxvera.cli build-docs
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [[ "$(uname -s)" != "Linux" ]]; then
|
||||
echo "linux-recovery-check.sh is only for Linux hosts." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
command -v voxvera >/dev/null 2>&1 || {
|
||||
echo "voxvera is not on PATH" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
TMP_DIR="${VOXVERA_RECOVERY_TMPDIR:-$(mktemp -d /tmp/voxvera-linux-recovery-XXXXXX)}"
|
||||
cleanup() {
|
||||
if [[ -z "${VOXVERA_RECOVERY_TMPDIR:-}" ]]; then
|
||||
rm -rf "$TMP_DIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
PLATFORM_JSON="$TMP_DIR/platform-status.json"
|
||||
DOCTOR_JSON="$TMP_DIR/doctor.json"
|
||||
AUTOSTART_JSON="$TMP_DIR/autostart-status.json"
|
||||
|
||||
voxvera platform-status --json >"$PLATFORM_JSON"
|
||||
voxvera doctor --json >"$DOCTOR_JSON"
|
||||
voxvera autostart status --json >"$AUTOSTART_JSON"
|
||||
|
||||
PLATFORM_JSON="$PLATFORM_JSON" \
|
||||
DOCTOR_JSON="$DOCTOR_JSON" \
|
||||
AUTOSTART_JSON="$AUTOSTART_JSON" \
|
||||
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"))
|
||||
autostart = json.loads(Path(os.environ["AUTOSTART_JSON"]).read_text(encoding="utf-8"))
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
if platform_status.get("platform_id") != "linux_cli_systemd":
|
||||
errors.append(f"expected linux_cli_systemd, got {platform_status.get('platform_id')}")
|
||||
if platform_status.get("tier") != "supported":
|
||||
errors.append(f"expected supported tier, got {platform_status.get('tier')}")
|
||||
|
||||
checks = {check["name"]: check for check in doctor.get("checks", [])}
|
||||
|
||||
required_ok = ["platform", "voxvera_cli", "onionshare_cli", "tor_binary", "host_root"]
|
||||
for name in required_ok:
|
||||
check = checks.get(name)
|
||||
if not check:
|
||||
errors.append(f"missing doctor check: {name}")
|
||||
elif not check.get("ok"):
|
||||
errors.append(f"{name} failed: {check.get('detail')}")
|
||||
|
||||
tor_check = checks.get("tor_socks_reachable")
|
||||
if not tor_check:
|
||||
errors.append("missing doctor check: tor_socks_reachable")
|
||||
elif not tor_check.get("ok"):
|
||||
warnings.append(f"tor_socks_reachable: {tor_check.get('detail')}")
|
||||
|
||||
timer_check = next((check for check in autostart.get("checks", []) if check["name"] == "systemd_timer_enabled"), None)
|
||||
if not timer_check:
|
||||
errors.append("missing autostart check: systemd_timer_enabled")
|
||||
elif not timer_check.get("ok"):
|
||||
warnings.append(f"systemd_timer_enabled: {timer_check.get('detail')}")
|
||||
|
||||
artifacts = [Path(path) for path in autostart.get("artifacts", [])]
|
||||
missing = [str(path) for path in artifacts if not path.exists()]
|
||||
if missing:
|
||||
warnings.append("missing autostart artifacts: " + ", ".join(missing))
|
||||
|
||||
host_root = Path.home() / "host"
|
||||
sites = []
|
||||
if host_root.exists():
|
||||
for site_dir in sorted(path for path in host_root.iterdir() if path.is_dir()):
|
||||
config = site_dir / "config.json"
|
||||
if not config.exists():
|
||||
continue
|
||||
session = site_dir / ".onionshare-session"
|
||||
log = site_dir / "onionshare.log"
|
||||
pid = site_dir / "server.pid"
|
||||
sites.append(
|
||||
{
|
||||
"name": site_dir.name,
|
||||
"session": session.exists(),
|
||||
"log": log.exists(),
|
||||
"pid": pid.exists(),
|
||||
}
|
||||
)
|
||||
|
||||
if errors:
|
||||
print("Linux recovery check failed:", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(f"- {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Platform: {platform_status['label']} [{platform_status['tier']}]")
|
||||
print(f"Hosting model: {platform_status.get('hosting_model', '')}")
|
||||
print(f"Autostart: {autostart.get('message', '')}")
|
||||
if warnings:
|
||||
print("Warnings:")
|
||||
for warning in warnings:
|
||||
print(f"- {warning}")
|
||||
else:
|
||||
print("Warnings: none")
|
||||
|
||||
if sites:
|
||||
print("Site state:")
|
||||
for site in sites:
|
||||
print(
|
||||
f"- {site['name']}: session={'yes' if site['session'] else 'no'}, "
|
||||
f"log={'yes' if site['log'] else 'no'}, pid={'yes' if site['pid'] else 'no'}"
|
||||
)
|
||||
else:
|
||||
print("Site state: no configured sites under ~/host")
|
||||
PY
|
||||
|
||||
echo "Detailed JSON artifacts written to $TMP_DIR"
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
TARGET="${1:-linux-cli}"
|
||||
TMP_DIR="${VOXVERA_SMOKE_TMPDIR:-$(mktemp -d /tmp/voxvera-platform-smoke-XXXXXX)}"
|
||||
mkdir -p "$TMP_DIR"
|
||||
|
||||
cleanup() {
|
||||
if [[ -z "${VOXVERA_SMOKE_TMPDIR:-}" ]]; then
|
||||
rm -rf "$TMP_DIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
command -v voxvera >/dev/null 2>&1 || {
|
||||
echo "voxvera is not on PATH" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
case "$TARGET" in
|
||||
linux-cli)
|
||||
EXPECTED_PLATFORM_ID="linux_cli_systemd"
|
||||
EXPECTED_TIER="supported"
|
||||
REQUIRE_ONIONSHARE="${VOXVERA_REQUIRE_ONIONSHARE:-1}"
|
||||
;;
|
||||
macos-cli)
|
||||
EXPECTED_PLATFORM_ID="macos_cli"
|
||||
EXPECTED_TIER="experimental"
|
||||
REQUIRE_ONIONSHARE="${VOXVERA_REQUIRE_ONIONSHARE:-0}"
|
||||
;;
|
||||
windows-cli)
|
||||
EXPECTED_PLATFORM_ID="windows_cli"
|
||||
EXPECTED_TIER="experimental"
|
||||
REQUIRE_ONIONSHARE="${VOXVERA_REQUIRE_ONIONSHARE:-0}"
|
||||
;;
|
||||
docker-cli)
|
||||
EXPECTED_PLATFORM_ID="linux_cli_systemd"
|
||||
EXPECTED_TIER="${VOXVERA_EXPECTED_TIER_OVERRIDE:-supported}"
|
||||
REQUIRE_ONIONSHARE="${VOXVERA_REQUIRE_ONIONSHARE:-0}"
|
||||
;;
|
||||
list)
|
||||
printf '%s\n' linux-cli macos-cli windows-cli docker-cli
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported smoke target: $TARGET" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
PLATFORM_JSON="$TMP_DIR/platform-status.json"
|
||||
DOCTOR_JSON="$TMP_DIR/doctor.json"
|
||||
AUTOSTART_JSON="$TMP_DIR/autostart-status.json"
|
||||
|
||||
voxvera platform-status --json >"$PLATFORM_JSON"
|
||||
voxvera doctor --json >"$DOCTOR_JSON"
|
||||
voxvera autostart status --json >"$AUTOSTART_JSON"
|
||||
|
||||
PLATFORM_JSON="$PLATFORM_JSON" \
|
||||
DOCTOR_JSON="$DOCTOR_JSON" \
|
||||
AUTOSTART_JSON="$AUTOSTART_JSON" \
|
||||
EXPECTED_PLATFORM_ID="$EXPECTED_PLATFORM_ID" \
|
||||
EXPECTED_TIER="$EXPECTED_TIER" \
|
||||
VOXVERA_REQUIRE_TOR="${VOXVERA_REQUIRE_TOR:-0}" \
|
||||
VOXVERA_REQUIRE_AUTOSTART_ARTIFACTS="${VOXVERA_REQUIRE_AUTOSTART_ARTIFACTS:-0}" \
|
||||
VOXVERA_REQUIRE_ONIONSHARE="$REQUIRE_ONIONSHARE" \
|
||||
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"))
|
||||
autostart = json.loads(Path(os.environ["AUTOSTART_JSON"]).read_text(encoding="utf-8"))
|
||||
|
||||
expected_platform_id = os.environ["EXPECTED_PLATFORM_ID"]
|
||||
expected_tier = os.environ["EXPECTED_TIER"]
|
||||
require_tor = os.environ.get("VOXVERA_REQUIRE_TOR") == "1"
|
||||
require_autostart_artifacts = os.environ.get("VOXVERA_REQUIRE_AUTOSTART_ARTIFACTS") == "1"
|
||||
require_onionshare = os.environ.get("VOXVERA_REQUIRE_ONIONSHARE") == "1"
|
||||
|
||||
errors = []
|
||||
|
||||
if platform_status.get("platform_id") != expected_platform_id:
|
||||
errors.append(
|
||||
f"platform-status platform_id mismatch: {platform_status.get('platform_id')} != {expected_platform_id}"
|
||||
)
|
||||
if platform_status.get("tier") != expected_tier:
|
||||
errors.append(f"platform-status tier mismatch: {platform_status.get('tier')} != {expected_tier}")
|
||||
if doctor.get("platform_id") != expected_platform_id:
|
||||
errors.append(f"doctor platform_id mismatch: {doctor.get('platform_id')} != {expected_platform_id}")
|
||||
if autostart.get("platform_id") != expected_platform_id:
|
||||
errors.append(f"autostart platform_id mismatch: {autostart.get('platform_id')} != {expected_platform_id}")
|
||||
|
||||
check_map = {check["name"]: check for check in doctor.get("checks", [])}
|
||||
for required_check in ("platform", "voxvera_cli"):
|
||||
if required_check not in check_map:
|
||||
errors.append(f"doctor missing required check: {required_check}")
|
||||
elif not check_map[required_check].get("ok"):
|
||||
errors.append(f"doctor check failed: {required_check} ({check_map[required_check].get('detail')})")
|
||||
|
||||
if require_onionshare:
|
||||
required_check = "onionshare_cli"
|
||||
if required_check not in check_map:
|
||||
errors.append(f"doctor missing required check: {required_check}")
|
||||
elif not check_map[required_check].get("ok"):
|
||||
errors.append(f"doctor check failed: {required_check} ({check_map[required_check].get('detail')})")
|
||||
|
||||
if require_tor:
|
||||
for required_check in ("tor_binary", "tor_socks_reachable"):
|
||||
if required_check not in check_map:
|
||||
errors.append(f"doctor missing Tor check: {required_check}")
|
||||
elif not check_map[required_check].get("ok"):
|
||||
errors.append(f"doctor Tor check failed: {required_check} ({check_map[required_check].get('detail')})")
|
||||
|
||||
if require_autostart_artifacts:
|
||||
if not autostart.get("artifacts"):
|
||||
errors.append("autostart status returned no artifacts")
|
||||
missing_artifacts = [artifact for artifact in autostart.get("artifacts", []) if not Path(artifact).exists()]
|
||||
if missing_artifacts:
|
||||
errors.append("autostart artifacts missing: " + ", ".join(missing_artifacts))
|
||||
|
||||
if errors:
|
||||
print("Platform smoke validation failed:", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(f"- {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Platform smoke OK: {platform_status['label']} [{platform_status['tier']}]")
|
||||
print(f"Hosting model: {platform_status.get('hosting_model', '')}")
|
||||
failing_checks = [check["name"] for check in doctor.get("checks", []) if not check.get("ok")]
|
||||
if failing_checks:
|
||||
print("Doctor warnings: " + ", ".join(failing_checks))
|
||||
else:
|
||||
print("Doctor warnings: none")
|
||||
print("Autostart message: " + autostart.get("message", ""))
|
||||
PY
|
||||
|
||||
echo "Artifacts written to $TMP_DIR"
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
ARCH="${ARCH_OVERRIDE:-$(uname -m)}"
|
||||
if [[ "$ARCH" == "x86_64" ]]; then
|
||||
DEB_ARCH="amd64"
|
||||
else
|
||||
DEB_ARCH="$ARCH"
|
||||
fi
|
||||
|
||||
DEB_PATH="${1:-voxvera/resources/bin/voxvera_$(python3 - <<'PY'
|
||||
from voxvera import __version__
|
||||
print(__version__)
|
||||
PY
|
||||
)_${DEB_ARCH}.deb}"
|
||||
APPIMAGE_PATH="${2:-voxvera/resources/bin/VoxVera-${ARCH}.AppImage}"
|
||||
|
||||
tmpdir="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
json_get() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
python3 - "$file" "$key" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
path = sys.argv[2].split(".")
|
||||
data = json.load(open(sys.argv[1], "r", encoding="utf-8"))
|
||||
for part in path:
|
||||
data = data[part]
|
||||
if isinstance(data, list):
|
||||
print("\n".join(str(item) for item in data))
|
||||
else:
|
||||
print(data)
|
||||
PY
|
||||
}
|
||||
|
||||
require_non_empty_json_field() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
local value
|
||||
value="$(json_get "$file" "$key")"
|
||||
if [[ -z "${value// }" ]]; then
|
||||
echo "Expected non-empty JSON field '$key' in $file" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Validating Debian artifact: $DEB_PATH"
|
||||
if [[ ! -f "$DEB_PATH" ]]; then
|
||||
echo "Debian package not found: $DEB_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$tmpdir/deb-root" "$tmpdir/deb-data"
|
||||
dpkg-deb -x "$DEB_PATH" "$tmpdir/deb-root"
|
||||
|
||||
if [[ ! -x "$tmpdir/deb-root/usr/bin/voxvera" ]]; then
|
||||
echo "Packaged voxvera binary missing from .deb payload" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VOXVERA_DIR="$tmpdir/deb-data" \
|
||||
"$tmpdir/deb-root/usr/bin/voxvera" \
|
||||
--config "$tmpdir/deb-data/config.json" \
|
||||
init --non-interactive
|
||||
|
||||
if [[ ! -f "$tmpdir/deb-data/config.json" ]]; then
|
||||
echo ".deb validation failed: config.json was not created" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$tmpdir/deb-data/host/voxvera/index.html" ]]; then
|
||||
echo ".deb validation failed: built site missing index.html" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Validating AppImage artifact: $APPIMAGE_PATH"
|
||||
if [[ ! -f "$APPIMAGE_PATH" ]]; then
|
||||
echo "AppImage not found: $APPIMAGE_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x "$APPIMAGE_PATH"
|
||||
"$APPIMAGE_PATH" platform-status --json > "$tmpdir/platform-status.json"
|
||||
require_non_empty_json_field "$tmpdir/platform-status.json" "tier_description"
|
||||
require_non_empty_json_field "$tmpdir/platform-status.json" "hosting_model"
|
||||
|
||||
VOXVERA_DIR="$tmpdir/appimage-data" \
|
||||
"$APPIMAGE_PATH" \
|
||||
--config "$tmpdir/appimage-data/config.json" \
|
||||
init --non-interactive
|
||||
|
||||
if [[ ! -f "$tmpdir/appimage-data/config.json" ]]; then
|
||||
echo "AppImage validation failed: config.json was not created" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$tmpdir/appimage-data/host/voxvera/index.html" ]]; then
|
||||
echo "AppImage validation failed: built site missing index.html" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Release artifact validation passed."
|
||||
Binary file not shown.
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"generated_at": "2026-03-19",
|
||||
"default_supported_target": "linux_cli_systemd",
|
||||
"support_tiers": {
|
||||
"supported": "Validated for persistent hidden-service hosting with documented recovery behavior.",
|
||||
"beta": "Feature-complete enough for broader testing, but not yet validated for production-grade recovery claims.",
|
||||
"experimental": "May install or run, but is not yet validated for reliable background hidden-service hosting.",
|
||||
"unsupported": "Not currently a target for reliable hosting."
|
||||
},
|
||||
"platforms": [
|
||||
{
|
||||
"id": "linux_cli_systemd",
|
||||
"label": "Linux CLI with systemd --user",
|
||||
"tier": "supported",
|
||||
"hosting_model": "System Tor or Tor Browser plus onionshare-cli, with VoxVera-managed recurring systemd user timer recovery.",
|
||||
"distribution_surfaces": [
|
||||
"install.sh",
|
||||
"voxvera-install.sh",
|
||||
"pip/pipx",
|
||||
"linux binary"
|
||||
],
|
||||
"required_capabilities": [
|
||||
"manual_build_and_serve",
|
||||
"persistent_onion_identity",
|
||||
"background_recovery_after_boot",
|
||||
"background_recovery_after_offline_periods",
|
||||
"inspectable_logs"
|
||||
],
|
||||
"current_notes": [
|
||||
"Canonical deployment target.",
|
||||
"Systemd user timer retries start-all every few minutes."
|
||||
],
|
||||
"next_gaps": [
|
||||
"Real-machine reboot and network-loss validation evidence should be captured in CI/docs."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "docker_cli",
|
||||
"label": "Docker CLI container",
|
||||
"tier": "experimental",
|
||||
"hosting_model": "Containerized VoxVera runtime with state under /flyers and periodic start-all retries.",
|
||||
"distribution_surfaces": [
|
||||
"Dockerfile",
|
||||
"ghcr image"
|
||||
],
|
||||
"required_capabilities": [
|
||||
"persistent_onion_identity",
|
||||
"restart_policy_integration",
|
||||
"validated_state_volume_layout"
|
||||
],
|
||||
"current_notes": [
|
||||
"State now routes through VOXVERA_DIR=/flyers.",
|
||||
"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."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "macos_cli",
|
||||
"label": "macOS CLI",
|
||||
"tier": "experimental",
|
||||
"hosting_model": "CLI runtime with launchd autostart path and external Tor/OnionShare dependencies.",
|
||||
"distribution_surfaces": [
|
||||
"install.sh",
|
||||
"pip/pipx",
|
||||
"macOS binary",
|
||||
"DMG"
|
||||
],
|
||||
"required_capabilities": [
|
||||
"manual_build_and_serve",
|
||||
"launchd_recovery",
|
||||
"persistent_onion_identity"
|
||||
],
|
||||
"current_notes": [
|
||||
"Installer and autostart warnings are explicit.",
|
||||
"No validated background recovery claim."
|
||||
],
|
||||
"next_gaps": [
|
||||
"Define the canonical Tor/OnionShare discovery model.",
|
||||
"Validate launchd behavior after login, reboot, and offline periods."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "windows_cli",
|
||||
"label": "Windows CLI",
|
||||
"tier": "experimental",
|
||||
"hosting_model": "CLI runtime with Task Scheduler autostart path and external Tor/OnionShare dependencies.",
|
||||
"distribution_surfaces": [
|
||||
"install.ps1",
|
||||
"windows binary",
|
||||
"Chocolatey"
|
||||
],
|
||||
"required_capabilities": [
|
||||
"manual_build_and_serve",
|
||||
"task_scheduler_recovery",
|
||||
"persistent_onion_identity",
|
||||
"consistent_binary_naming"
|
||||
],
|
||||
"current_notes": [
|
||||
"Installer warnings are explicit.",
|
||||
"Packaging surfaces are not yet trustworthy for hosting claims."
|
||||
],
|
||||
"next_gaps": [
|
||||
"Unify binary naming and installer expectations.",
|
||||
"Define canonical Tor and OnionShare discovery on Windows.",
|
||||
"Validate restart behavior after reboot and reconnect."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "electron_gui",
|
||||
"label": "Electron GUI",
|
||||
"tier": "experimental",
|
||||
"hosting_model": "GUI frontend over the CLI runtime.",
|
||||
"distribution_surfaces": [
|
||||
"gui/electron"
|
||||
],
|
||||
"required_capabilities": [
|
||||
"shared_runtime_contract_with_cli",
|
||||
"status_visibility",
|
||||
"non-destructive_window_close_behavior"
|
||||
],
|
||||
"current_notes": [
|
||||
"GUI is documented as experimental."
|
||||
],
|
||||
"next_gaps": [
|
||||
"Remove duplicate runtime logic where possible.",
|
||||
"Ensure GUI is a controller over supported runtime surfaces, not a separate hosting model."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "flatpak_appimage_homebrew_chocolatey",
|
||||
"label": "Package-only surfaces",
|
||||
"tier": "experimental",
|
||||
"hosting_model": "Artifact distribution only until runtime parity exists.",
|
||||
"distribution_surfaces": [
|
||||
"Flatpak",
|
||||
"AppImage",
|
||||
"Homebrew",
|
||||
"Chocolatey",
|
||||
"Debian package"
|
||||
],
|
||||
"required_capabilities": [
|
||||
"runtime_dependency_model",
|
||||
"support-tier-specific_release_checks"
|
||||
],
|
||||
"current_notes": [
|
||||
"All are now explicitly marked experimental."
|
||||
],
|
||||
"next_gaps": [
|
||||
"Do not promote any package surface until the underlying platform runtime is validated."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+376
-6
@@ -5,9 +5,15 @@ import json
|
||||
import zipfile
|
||||
import time
|
||||
import socket
|
||||
import platform
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from voxvera import cli
|
||||
from voxvera.platforms import get_platform_adapter
|
||||
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
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
@@ -25,6 +31,7 @@ def _setup_tmp(monkeypatch, tmp_path):
|
||||
shutil.copytree(repo_root / "voxvera" / "src", res_root / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", res_root / "locales")
|
||||
shutil.copytree(repo_root / "voxvera" / "templates", res_root / "templates")
|
||||
shutil.copy(repo_root / "support-matrix.json", test_data_dir / "support-matrix.json")
|
||||
|
||||
monkeypatch.setattr(cli, "ROOT", res_root)
|
||||
monkeypatch.setattr(cli, "DATA_DIR", test_data_dir)
|
||||
@@ -44,6 +51,24 @@ def test_help(capsys):
|
||||
assert "usage:" in captured.out
|
||||
|
||||
|
||||
def test_get_platform_adapter_linux(monkeypatch):
|
||||
monkeypatch.setattr(platform, "system", lambda: "Linux")
|
||||
adapter = get_platform_adapter(cli_module=cli)
|
||||
assert adapter.platform_id == "linux_cli_systemd"
|
||||
|
||||
|
||||
def test_get_platform_adapter_macos(monkeypatch):
|
||||
monkeypatch.setattr(platform, "system", lambda: "Darwin")
|
||||
adapter = get_platform_adapter(cli_module=cli)
|
||||
assert adapter.platform_id == "macos_cli"
|
||||
|
||||
|
||||
def test_get_platform_adapter_windows(monkeypatch):
|
||||
monkeypatch.setattr(platform, "system", lambda: "Windows")
|
||||
adapter = get_platform_adapter(cli_module=cli)
|
||||
assert adapter.platform_id == "windows_cli"
|
||||
|
||||
|
||||
def test_init_template(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
test_data_dir = tmp_path / "data"
|
||||
@@ -56,6 +81,101 @@ def test_init_template(tmp_path, monkeypatch):
|
||||
assert (dest / "index.html").exists()
|
||||
|
||||
|
||||
def test_init_seeds_missing_default_config(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
test_data_dir = tmp_path / "data"
|
||||
config_path = test_data_dir / "fresh-config.json"
|
||||
answers = iter([
|
||||
"Fresh Site",
|
||||
"freshsite",
|
||||
"Fresh Title",
|
||||
"Fresh Subtitle",
|
||||
"Fresh Headline",
|
||||
"https://example.com",
|
||||
"Read more",
|
||||
"Footer bits",
|
||||
"",
|
||||
])
|
||||
|
||||
monkeypatch.setattr(cli, "choose_language", lambda *a: "en")
|
||||
monkeypatch.setattr(cli, "prompt", lambda questions: {question["name"]: next(answers) for question in questions})
|
||||
monkeypatch.setattr(cli, "open_editor", lambda initial: "Fresh content")
|
||||
|
||||
class _Confirm:
|
||||
def execute(self):
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(cli.inquirer, "confirm", lambda **kwargs: _Confirm())
|
||||
|
||||
cli.main(["--config", str(config_path), "init"])
|
||||
|
||||
assert config_path.exists()
|
||||
saved = cli.load_config(config_path)
|
||||
assert saved["folder_name"] == "freshsite"
|
||||
assert saved["content"] == "Fresh content"
|
||||
|
||||
|
||||
def test_init_noninteractive_does_not_prompt_for_language(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
test_data_dir = tmp_path / "data"
|
||||
config_path = test_data_dir / "fresh-config.json"
|
||||
|
||||
def _fail_choose_language(*args, **kwargs):
|
||||
raise AssertionError("choose_language should not run for non-interactive init")
|
||||
|
||||
monkeypatch.setattr(cli, "choose_language", _fail_choose_language)
|
||||
|
||||
cli.main(["--config", str(config_path), "init", "--non-interactive"])
|
||||
|
||||
saved = cli.load_config(config_path)
|
||||
assert saved["lang"] == "en"
|
||||
assert (test_data_dir / "host" / saved["folder_name"] / "index.html").exists()
|
||||
|
||||
|
||||
def test_create_from_template_persists_current_language(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
cli.load_locale("es")
|
||||
|
||||
class _Prompt:
|
||||
def execute(self):
|
||||
return cli.list_presets()[0]
|
||||
|
||||
monkeypatch.setattr(cli.inquirer, "select", lambda **kwargs: _Prompt())
|
||||
monkeypatch.setattr(cli, "interactive_update", lambda path: None)
|
||||
monkeypatch.setattr(cli, "resolve_new_site_folder", lambda path: cli.load_config(path)["folder_name"])
|
||||
monkeypatch.setattr(cli, "build_assets", lambda path: None)
|
||||
monkeypatch.setattr(cli, "serve", lambda path: None)
|
||||
|
||||
cli.create_from_template(None)
|
||||
|
||||
config = cli.load_config(tmp_path / "data" / "config.json")
|
||||
assert config["lang"] == "es"
|
||||
|
||||
|
||||
def test_resolve_new_site_folder_versions_existing_folder(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
test_data_dir = tmp_path / "data"
|
||||
host_dir = test_data_dir / "host"
|
||||
existing_dir = host_dir / "jesus"
|
||||
existing_dir.mkdir(parents=True)
|
||||
(existing_dir / "config.json").write_text(json.dumps({"folder_name": "jesus"}), encoding="utf-8")
|
||||
|
||||
config_path = test_data_dir / "config.json"
|
||||
config_path.write_text(json.dumps({"folder_name": "jesus"}), encoding="utf-8")
|
||||
|
||||
class _Prompt:
|
||||
def execute(self):
|
||||
return "version"
|
||||
|
||||
monkeypatch.setattr(cli.inquirer, "select", lambda **kwargs: _Prompt())
|
||||
|
||||
resolved = cli.resolve_new_site_folder(str(config_path))
|
||||
updated = cli.load_config(config_path)
|
||||
|
||||
assert resolved == "jesus_01"
|
||||
assert updated["folder_name"] == "jesus_01"
|
||||
|
||||
|
||||
def test_build(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
cli.main(["build"])
|
||||
@@ -76,6 +196,26 @@ def test_build_with_download(tmp_path, monkeypatch):
|
||||
assert dest.is_file()
|
||||
|
||||
|
||||
def test_build_generates_tear_off_qr_from_url_when_onion_missing(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
test_data_dir = tmp_path / "data"
|
||||
config_path = test_data_dir / "config.json"
|
||||
config = cli.load_config(config_path)
|
||||
config["url"] = "https://example.com/learn-more"
|
||||
config["tear_off_link"] = ""
|
||||
cli.save_config(config, config_path)
|
||||
|
||||
cli.build_assets(str(config_path))
|
||||
|
||||
dest = test_data_dir / "host" / config["folder_name"]
|
||||
html = (dest / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
assert (dest / "qrcode-content.png").exists()
|
||||
assert (dest / "qrcode-tear-offs.png").exists()
|
||||
assert 'class="container no-tear-offs"' not in html
|
||||
assert "https://example.com/learn-more" in html
|
||||
|
||||
|
||||
def test_import(tmp_path, monkeypatch):
|
||||
repo = _setup_tmp(monkeypatch, tmp_path)
|
||||
test_data_dir = tmp_path / "data"
|
||||
@@ -174,6 +314,212 @@ def test_check_missing(capsys, monkeypatch):
|
||||
assert "missing" in captured.out
|
||||
|
||||
|
||||
def test_doctor_command_outputs_platform_report(tmp_path, monkeypatch, capsys):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(platform, "system", lambda: "Linux")
|
||||
monkeypatch.setattr(shutil, "which", lambda cmd: f"/usr/bin/{cmd}" if cmd in {"voxvera", "tor", "onionshare-cli", "systemctl"} else None)
|
||||
|
||||
class FakeSocket:
|
||||
def settimeout(self, value):
|
||||
return None
|
||||
|
||||
def connect_ex(self, addr):
|
||||
return 0
|
||||
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
def fake_check_output(args, stderr=None):
|
||||
if "is-enabled" in args:
|
||||
return b"enabled"
|
||||
if "is-active" in args:
|
||||
return b"active"
|
||||
raise AssertionError(args)
|
||||
|
||||
monkeypatch.setattr(platform_linux.socket, "socket", lambda *args, **kwargs: FakeSocket())
|
||||
monkeypatch.setattr(platform_base.subprocess, "check_output", fake_check_output)
|
||||
cli.main(["doctor"])
|
||||
captured = capsys.readouterr()
|
||||
assert "VoxVera Doctor" in captured.out
|
||||
assert "linux_cli_systemd" in captured.out
|
||||
|
||||
|
||||
def test_doctor_json_output(tmp_path, monkeypatch, capsys):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(platform, "system", lambda: "Linux")
|
||||
monkeypatch.setattr(shutil, "which", lambda cmd: f"/usr/bin/{cmd}" if cmd in {"voxvera", "tor", "onionshare-cli"} else None)
|
||||
|
||||
class FakeSocket:
|
||||
def settimeout(self, value):
|
||||
return None
|
||||
|
||||
def connect_ex(self, addr):
|
||||
return 0
|
||||
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(platform_linux.socket, "socket", lambda *args, **kwargs: FakeSocket())
|
||||
monkeypatch.setattr(platform_base.subprocess, "check_output", lambda *args, **kwargs: b"disabled")
|
||||
|
||||
cli.main(["doctor", "--json"])
|
||||
captured = capsys.readouterr()
|
||||
report = json.loads(captured.out)
|
||||
assert report["platform_id"] == "linux_cli_systemd"
|
||||
assert "summary" in report
|
||||
assert "sections" in report
|
||||
assert "dependencies" in report["sections"]
|
||||
assert "checks" in report
|
||||
|
||||
|
||||
def test_platform_status_json_output(tmp_path, monkeypatch, capsys):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(platform, "system", lambda: "Linux")
|
||||
|
||||
cli.main(["platform-status", "--json"])
|
||||
captured = capsys.readouterr()
|
||||
report = json.loads(captured.out)
|
||||
assert report["platform_id"] == "linux_cli_systemd"
|
||||
assert report["hosting_model"]
|
||||
assert "checks" not in report
|
||||
|
||||
|
||||
def test_autostart_status_command_outputs_timer_state(tmp_path, monkeypatch, capsys):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
fake_home = tmp_path / "home"
|
||||
monkeypatch.setattr(platform, "system", lambda: "Linux")
|
||||
monkeypatch.setattr(platform_base.Path, "home", lambda: fake_home)
|
||||
systemd_dir = fake_home / ".config" / "systemd" / "user"
|
||||
systemd_dir.mkdir(parents=True, exist_ok=True)
|
||||
(systemd_dir / "voxvera-start.service").write_text("service", encoding="utf-8")
|
||||
(systemd_dir / "voxvera-start.timer").write_text("timer", encoding="utf-8")
|
||||
|
||||
def fake_check_output(args, stderr=None):
|
||||
if "is-enabled" in args:
|
||||
return b"enabled"
|
||||
if "is-active" in args:
|
||||
return b"active"
|
||||
raise AssertionError(args)
|
||||
|
||||
monkeypatch.setattr(shutil, "which", lambda cmd: f"/usr/bin/{cmd}" if cmd == "systemctl" else None)
|
||||
monkeypatch.setattr(platform_base.subprocess, "check_output", fake_check_output)
|
||||
|
||||
cli.main(["autostart", "status"])
|
||||
captured = capsys.readouterr()
|
||||
assert "Autostart Status" in captured.out
|
||||
assert "systemd_timer_enabled" in captured.out
|
||||
|
||||
|
||||
def test_autostart_status_json_output(tmp_path, monkeypatch, capsys):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
fake_home = tmp_path / "home"
|
||||
monkeypatch.setattr(platform, "system", lambda: "Linux")
|
||||
monkeypatch.setattr(platform_base.Path, "home", lambda: fake_home)
|
||||
systemd_dir = fake_home / ".config" / "systemd" / "user"
|
||||
systemd_dir.mkdir(parents=True, exist_ok=True)
|
||||
(systemd_dir / "voxvera-start.service").write_text("service", encoding="utf-8")
|
||||
(systemd_dir / "voxvera-start.timer").write_text("timer", encoding="utf-8")
|
||||
monkeypatch.setattr(shutil, "which", lambda cmd: f"/usr/bin/{cmd}" if cmd == "systemctl" else None)
|
||||
monkeypatch.setattr(platform_base.subprocess, "check_output", lambda *args, **kwargs: b"enabled")
|
||||
|
||||
cli.main(["autostart", "status", "--json"])
|
||||
captured = capsys.readouterr()
|
||||
status = json.loads(captured.out)
|
||||
assert status["platform_id"] == "linux_cli_systemd"
|
||||
assert len(status["artifacts"]) == 2
|
||||
|
||||
|
||||
def test_linux_autostart_uninstall_removes_artifacts(tmp_path, monkeypatch, capsys):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
fake_home = tmp_path / "home"
|
||||
commands = []
|
||||
monkeypatch.setattr(platform, "system", lambda: "Linux")
|
||||
monkeypatch.setattr(platform_linux.Path, "home", lambda: fake_home)
|
||||
monkeypatch.setattr(platform_linux.subprocess, "run", lambda args, **kwargs: commands.append(args))
|
||||
systemd_dir = fake_home / ".config" / "systemd" / "user"
|
||||
systemd_dir.mkdir(parents=True, exist_ok=True)
|
||||
service = systemd_dir / "voxvera-start.service"
|
||||
timer = systemd_dir / "voxvera-start.timer"
|
||||
service.write_text("service", encoding="utf-8")
|
||||
timer.write_text("timer", encoding="utf-8")
|
||||
|
||||
cli.main(["autostart", "uninstall"])
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert "Removed systemd user service" in captured.out
|
||||
assert not service.exists()
|
||||
assert not timer.exists()
|
||||
assert ["systemctl", "--user", "disable", "--now", "voxvera-start.timer"] in commands
|
||||
|
||||
|
||||
def test_macos_autostart_status_and_render(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
fake_home = tmp_path / "home"
|
||||
monkeypatch.setattr(platform, "system", lambda: "Darwin")
|
||||
monkeypatch.setattr(platform_base.Path, "home", lambda: fake_home)
|
||||
|
||||
adapter = get_platform_adapter(cli_module=cli)
|
||||
assert adapter.platform_id == "macos_cli"
|
||||
|
||||
plist = adapter.render_launchd_plist("/tmp/voxvera")
|
||||
assert "org.voxvera.start-all" in plist
|
||||
assert "<string>/tmp/voxvera</string>" in plist
|
||||
assert "start-all" in plist
|
||||
|
||||
status = adapter.autostart_status()
|
||||
assert status["platform_id"] == "macos_cli"
|
||||
assert status["artifacts"] == [str(fake_home / "Library" / "LaunchAgents" / "org.voxvera.start-all.plist")]
|
||||
assert any(check["name"] == "launchd_loaded" for check in status["checks"])
|
||||
|
||||
|
||||
def test_macos_autostart_uninstall_removes_plist(tmp_path, monkeypatch, capsys):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
fake_home = tmp_path / "home"
|
||||
commands = []
|
||||
monkeypatch.setattr(platform, "system", lambda: "Darwin")
|
||||
monkeypatch.setattr(platform_base.Path, "home", lambda: fake_home)
|
||||
monkeypatch.setattr(platform_base.subprocess, "check_output", lambda args, stderr=None: commands.append(args) or b"")
|
||||
plist = fake_home / "Library" / "LaunchAgents" / "org.voxvera.start-all.plist"
|
||||
plist.parent.mkdir(parents=True, exist_ok=True)
|
||||
plist.write_text("plist", encoding="utf-8")
|
||||
|
||||
cli.main(["autostart", "uninstall"])
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert "Removed LaunchAgent" in captured.out
|
||||
assert not plist.exists()
|
||||
assert ["launchctl", "unload", str(plist)] in commands
|
||||
|
||||
|
||||
def test_windows_autostart_status_and_render(monkeypatch):
|
||||
monkeypatch.setattr(platform, "system", lambda: "Windows")
|
||||
monkeypatch.setattr(platform_base.subprocess, "check_output", lambda *args, **kwargs: b"TaskName: VoxVera Start All")
|
||||
|
||||
adapter = get_platform_adapter(cli_module=cli)
|
||||
assert adapter.platform_id == "windows_cli"
|
||||
|
||||
command = adapter.render_schtasks_create_command("C:\\voxvera.exe")
|
||||
assert command[:6] == ["schtasks", "/Create", "/F", "/SC", "ONLOGON", "/TN"]
|
||||
assert "VoxVera Start All" in command
|
||||
assert '"C:\\voxvera.exe" start-all' in command
|
||||
|
||||
status = adapter.autostart_status()
|
||||
assert status["platform_id"] == "windows_cli"
|
||||
assert any(check["name"] == "scheduled_task_present" for check in status["checks"])
|
||||
|
||||
|
||||
def test_windows_autostart_uninstall_uses_schtasks(monkeypatch, capsys):
|
||||
commands = []
|
||||
monkeypatch.setattr(platform, "system", lambda: "Windows")
|
||||
monkeypatch.setattr(platform_base.subprocess, "check_output", lambda args, stderr=None: commands.append(args) or b"SUCCESS")
|
||||
|
||||
cli.main(["autostart", "uninstall"])
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert "removed" in captured.out.lower()
|
||||
assert ["schtasks", "/Delete", "/TN", "VoxVera Start All", "/F"] in commands
|
||||
|
||||
|
||||
def test_build_download_zip(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
@@ -246,7 +592,7 @@ def test_serve_updates_url(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
def test_quickstart_noninteractive(tmp_path, monkeypatch):
|
||||
"""Test quickstart auto-generates onion URL for tear-off links."""
|
||||
"""Test quickstart builds and serves in one command."""
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
@@ -287,8 +633,6 @@ def test_quickstart_noninteractive(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(cli, "choose_language", lambda *a: "en")
|
||||
config_path = test_data_dir / "config.json"
|
||||
cli.main(["quickstart", "--non-interactive"])
|
||||
# quickstart no longer calls serve, so we call it manually to test URL update
|
||||
cli.serve(str(config_path))
|
||||
|
||||
dir_path = test_data_dir / "host" / folder_name
|
||||
assert (dir_path / "index.html").exists()
|
||||
@@ -298,6 +642,30 @@ def test_quickstart_noninteractive(tmp_path, monkeypatch):
|
||||
# tear_off_link should be set to onion address (for tear-off tabs)
|
||||
assert updated["tear_off_link"] == onion_url
|
||||
# url should be preserved (user-configured content link)
|
||||
assert updated["url"] == config["url"]
|
||||
|
||||
|
||||
def test_install_systemd_autostart_writes_recovery_timer(tmp_path, monkeypatch):
|
||||
"""Test Linux autostart installs a recurring recovery service and timer."""
|
||||
commands = []
|
||||
|
||||
monkeypatch.setattr(platform, "system", lambda: "Linux")
|
||||
monkeypatch.setattr(platform_linux.Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setattr(cli, "_find_voxvera_bin", lambda: "/tmp/voxvera")
|
||||
monkeypatch.setattr(platform_linux.subprocess, "run", lambda args, **kwargs: commands.append(args))
|
||||
monkeypatch.setenv("USER", "tester")
|
||||
|
||||
cli._install_systemd_autostart()
|
||||
|
||||
service_dir = tmp_path / ".config" / "systemd" / "user"
|
||||
service_text = (service_dir / "voxvera-start.service").read_text(encoding="utf-8")
|
||||
timer_text = (service_dir / "voxvera-start.timer").read_text(encoding="utf-8")
|
||||
|
||||
assert "ExecStart=/tmp/voxvera start-all" in service_text
|
||||
assert "OnUnitActiveSec=5min" in timer_text
|
||||
assert "Persistent=true" in timer_text
|
||||
assert any(args[:4] == ["systemctl", "--user", "enable", "--now"] and args[4] == "voxvera-start.timer" for args in commands)
|
||||
assert any(args[:4] == ["systemctl", "--user", "start", "voxvera-start.service"] for args in commands)
|
||||
|
||||
|
||||
def test_get_tor_ports_with_env_vars(monkeypatch):
|
||||
@@ -387,7 +755,7 @@ def test_build_injects_localized_tor_browser_download_links(tmp_path, monkeypatc
|
||||
|
||||
|
||||
def test_build_with_only_url(tmp_path, monkeypatch):
|
||||
"""Test that QR codes work when only url (content link) is set."""
|
||||
"""Test that the main URL also populates tear-off rendering before onion setup."""
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
|
||||
# Set only url (content link)
|
||||
@@ -407,9 +775,11 @@ def test_build_with_only_url(tmp_path, monkeypatch):
|
||||
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / config["folder_name"]
|
||||
# Only content QR should exist (tear-off will be set by serve)
|
||||
html = (dest / "index.html").read_text(encoding="utf-8")
|
||||
assert (dest / "qrcode-content.png").exists()
|
||||
assert not (dest / "qrcode-tear-offs.png").exists()
|
||||
assert (dest / "qrcode-tear-offs.png").exists()
|
||||
assert 'class="container no-tear-offs"' not in html
|
||||
assert "https://example.com/external-resource" in html
|
||||
|
||||
|
||||
def test_build_with_only_tear_off_link(tmp_path, monkeypatch):
|
||||
|
||||
@@ -8,6 +8,74 @@ from voxvera import cli
|
||||
# Helper to find project root
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def test_support_matrix_exists_and_matches_docs():
|
||||
support_matrix_path = REPO_ROOT / "support-matrix.json"
|
||||
support_doc_path = REPO_ROOT / "docs" / "platform-support-matrix.md"
|
||||
|
||||
assert support_matrix_path.exists()
|
||||
assert support_doc_path.exists()
|
||||
|
||||
support_data = json.loads(support_matrix_path.read_text(encoding="utf-8"))
|
||||
assert support_data["default_supported_target"] == "linux_cli_systemd"
|
||||
assert any(platform["tier"] == "supported" for platform in support_data["platforms"])
|
||||
assert any(platform["id"] == "windows_cli" for platform in support_data["platforms"])
|
||||
|
||||
support_doc = support_doc_path.read_text(encoding="utf-8")
|
||||
assert "Linux CLI with `systemd --user`" in support_doc
|
||||
assert "Experimental" in support_doc
|
||||
|
||||
|
||||
def test_platform_smoke_is_wired_into_validation_surfaces():
|
||||
smoke_script = REPO_ROOT / "scripts" / "platform-smoke.sh"
|
||||
ci_workflow = (REPO_ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8")
|
||||
e2e_script = (REPO_ROOT / "ci" / "test-e2e.sh").read_text(encoding="utf-8")
|
||||
binaries_workflow = (REPO_ROOT / ".github" / "workflows" / "binaries.yml").read_text(encoding="utf-8")
|
||||
smoke_text = smoke_script.read_text(encoding="utf-8")
|
||||
|
||||
assert smoke_script.exists()
|
||||
assert "scripts/platform-smoke.sh linux-cli" in ci_workflow
|
||||
assert "scripts/platform-smoke.sh linux-cli" in e2e_script
|
||||
assert "platform-status --json" in ci_workflow
|
||||
assert "doctor --json" in ci_workflow
|
||||
assert "autostart status --json" in ci_workflow
|
||||
assert "platform-status --json" in binaries_workflow
|
||||
assert "doctor --json" in binaries_workflow
|
||||
assert "autostart status --json" in binaries_workflow
|
||||
for target in ("linux-cli", "macos-cli", "windows-cli", "docker-cli"):
|
||||
assert target in smoke_text
|
||||
|
||||
|
||||
def test_linux_validation_runbook_and_helper_are_linked():
|
||||
runbook = REPO_ROOT / "docs" / "linux-hosting-validation.md"
|
||||
helper = REPO_ROOT / "scripts" / "linux-recovery-check.sh"
|
||||
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
|
||||
|
||||
assert runbook.exists()
|
||||
assert helper.exists()
|
||||
assert "docs/linux-hosting-validation.md" in readme
|
||||
assert "scripts/linux-recovery-check.sh" in runbook.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_frozen_builds_bundle_support_matrix():
|
||||
build_script = (REPO_ROOT / "scripts" / "build_binaries.sh").read_text(encoding="utf-8")
|
||||
binaries_workflow = (REPO_ROOT / ".github" / "workflows" / "binaries.yml").read_text(encoding="utf-8")
|
||||
|
||||
assert '--add-data "support-matrix.json:."' in build_script
|
||||
assert '--add-data "support-matrix.json${SEP}."' in binaries_workflow
|
||||
|
||||
|
||||
def test_release_artifact_validation_is_wired_into_binaries_workflow():
|
||||
validation_script = REPO_ROOT / "scripts" / "validate-release-artifacts.sh"
|
||||
binaries_workflow = (REPO_ROOT / ".github" / "workflows" / "binaries.yml").read_text(encoding="utf-8")
|
||||
script_text = validation_script.read_text(encoding="utf-8")
|
||||
|
||||
assert validation_script.exists()
|
||||
assert "scripts/validate-release-artifacts.sh" in binaries_workflow
|
||||
assert "dpkg-deb -x" in script_text
|
||||
assert "platform-status --json" in script_text
|
||||
assert "init --non-interactive" in script_text
|
||||
|
||||
def test_locale_completeness():
|
||||
"""Verify that all non-English locales have the same keys as English."""
|
||||
locales_dir = REPO_ROOT / "voxvera" / "locales"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# Installs Tor + OnionShare and VoxVera itself.
|
||||
# --------------------------------------------------------------------
|
||||
set -euo pipefail
|
||||
OS_NAME="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
## -------- helper functions -----------------------------------------
|
||||
command_exists() { command -v "$1" &>/dev/null; }
|
||||
@@ -12,6 +13,45 @@ msg() { printf "\e[32m%s\e[0m\n" "$*"; }
|
||||
warn() { printf "\e[33m%s\e[0m\n" "$*" >&2; }
|
||||
die() { printf "\e[31mERROR: %s\e[0m\n" "$*" >&2; exit 1; }
|
||||
|
||||
report_cli_status() {
|
||||
command_exists voxvera || return 0
|
||||
|
||||
local platform_json doctor_json autostart_json
|
||||
platform_json="$(voxvera platform-status --json 2>/dev/null || true)"
|
||||
doctor_json="$(voxvera doctor --json 2>/dev/null || true)"
|
||||
autostart_json="$(voxvera autostart status --json 2>/dev/null || true)"
|
||||
|
||||
if [ -n "$platform_json" ]; then
|
||||
PLATFORM_JSON="$platform_json" python3 - <<'PY'
|
||||
import json, os
|
||||
data = json.loads(os.environ["PLATFORM_JSON"])
|
||||
print(f"Platform tier: {data.get('tier', 'unknown')} ({data.get('label', 'unknown')})")
|
||||
if data.get("hosting_model"):
|
||||
print(f"Hosting model: {data['hosting_model']}")
|
||||
PY
|
||||
fi
|
||||
|
||||
if [ -n "$doctor_json" ]; then
|
||||
DOCTOR_JSON="$doctor_json" python3 - <<'PY'
|
||||
import json, os
|
||||
data = json.loads(os.environ["DOCTOR_JSON"])
|
||||
failed = [check["name"] for check in data.get("checks", []) if not check.get("ok")]
|
||||
if failed:
|
||||
print("Doctor checks needing attention: " + ", ".join(failed))
|
||||
else:
|
||||
print("Doctor checks: all passed")
|
||||
PY
|
||||
fi
|
||||
|
||||
if [ -n "$autostart_json" ]; then
|
||||
AUTOSTART_JSON="$autostart_json" python3 - <<'PY'
|
||||
import json, os
|
||||
data = json.loads(os.environ["AUTOSTART_JSON"])
|
||||
print("Autostart: " + data.get("message", "unknown"))
|
||||
PY
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect sudo/root
|
||||
if command_exists sudo; then
|
||||
SUDO="sudo"
|
||||
@@ -31,6 +71,9 @@ else die "Unsupported distro – can't find apt/yum/dnf/pacman/apk."
|
||||
fi
|
||||
|
||||
msg ">> Detected package manager: $PM"
|
||||
if [[ "$OS_NAME" != linux* ]]; then
|
||||
warn "This installer is experimental outside Linux and is not validated for reliable background hidden-service hosting."
|
||||
fi
|
||||
|
||||
## -------- install system packages ----------------------------------
|
||||
msg ">> Installing system dependencies..."
|
||||
@@ -142,6 +185,16 @@ if command_exists systemctl; then
|
||||
$SUDO systemctl restart tor || true
|
||||
fi
|
||||
|
||||
if command_exists voxvera && [[ "$OS_NAME" == linux* ]]; then
|
||||
msg ">> Installing Linux autostart recovery timer..."
|
||||
voxvera autostart 2>/dev/null || warn "Could not install the VoxVera autostart timer automatically. Run 'voxvera autostart' manually."
|
||||
fi
|
||||
|
||||
if command_exists voxvera; then
|
||||
msg ">> Inspecting installed runtime posture..."
|
||||
report_cli_status
|
||||
fi
|
||||
|
||||
msg "------------------------------------------------------------------"
|
||||
msg " VoxVera install finished!"
|
||||
msg " Tor rc file : $TOR_DIR/torrc"
|
||||
@@ -149,5 +202,11 @@ msg ""
|
||||
msg " IMPORTANT: Please restart your terminal or run 'source ~/.bashrc'"
|
||||
msg " (or your shell config) to use 'voxvera'."
|
||||
msg ""
|
||||
msg " Linux is the supported persistent-host target."
|
||||
msg " Other package/install paths remain experimental."
|
||||
msg ""
|
||||
msg " Run 'voxvera check' to verify your setup after refreshing."
|
||||
msg " Run 'voxvera autostart status' to inspect the recovery timer."
|
||||
msg " Run 'voxvera autostart uninstall' to remove the recovery timer."
|
||||
msg " Run 'bash scripts/linux-recovery-check.sh' from the repo for the Linux validation checklist."
|
||||
msg "------------------------------------------------------------------"
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.3.6"
|
||||
__version__ = "0.3.10"
|
||||
|
||||
# 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
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ def generate_qr(config_path: Path, output_dir: Path) -> None:
|
||||
config = json.load(f)
|
||||
|
||||
url = config.get("url", "")
|
||||
tear = config.get("tear_off_link", "")
|
||||
tear = config.get("tear_off_link", "") or url
|
||||
|
||||
# Skip QR generation if no URLs configured yet (will be generated after serve)
|
||||
if not url and not tear:
|
||||
|
||||
+247
-97
@@ -27,6 +27,7 @@ if getattr(sys, 'frozen', False):
|
||||
from InquirerPy import prompt, inquirer
|
||||
from rich.console import Console
|
||||
from voxvera import __version__
|
||||
from voxvera.platforms import get_platform_adapter
|
||||
|
||||
GITHUB_RECOMMENDED_BINARY_LIMIT = 95 * 1024 * 1024
|
||||
LEGACY_CONFIG_KEYS = {
|
||||
@@ -61,6 +62,7 @@ DATA_DIR = Path(os.environ.get("VOXVERA_DIR", Path.home()))
|
||||
|
||||
# Global locale state
|
||||
CURRENT_LOCALE = {}
|
||||
CURRENT_LANG_CODE = "en"
|
||||
|
||||
|
||||
def tor_browser_download_url(lang_code: str) -> str:
|
||||
@@ -80,10 +82,17 @@ def normalize_config(data: dict) -> dict:
|
||||
return normalized
|
||||
|
||||
|
||||
def get_effective_tear_off_link(data: dict) -> str:
|
||||
"""Use the explicit tear-off link when present, otherwise fall back to the main URL."""
|
||||
return (data.get("tear_off_link") or "").strip() or (data.get("url") or "").strip()
|
||||
|
||||
|
||||
def build_render_context(data: dict) -> dict:
|
||||
"""Add derived template values without mutating persisted config shape."""
|
||||
render_data = dict(data)
|
||||
render_data["tear_off_state_class"] = "" if render_data.get("tear_off_link", "").strip() else "no-tear-offs"
|
||||
effective_tear_off_link = get_effective_tear_off_link(render_data)
|
||||
render_data["tear_off_link"] = effective_tear_off_link
|
||||
render_data["tear_off_state_class"] = "" if effective_tear_off_link else "no-tear-offs"
|
||||
return render_data
|
||||
|
||||
|
||||
@@ -137,7 +146,7 @@ def _locale_res(*parts) -> Traversable:
|
||||
|
||||
def load_locale(lang_code: str = "en"):
|
||||
"""Load the specified locale JSON file."""
|
||||
global CURRENT_LOCALE
|
||||
global CURRENT_LOCALE, CURRENT_LANG_CODE
|
||||
try:
|
||||
res = _locale_res(f"{lang_code}.json")
|
||||
if getattr(sys, 'frozen', False):
|
||||
@@ -147,6 +156,7 @@ def load_locale(lang_code: str = "en"):
|
||||
lang_code = "en"
|
||||
locale_file = _locale_res("en.json")
|
||||
CURRENT_LOCALE = json.loads(locale_file.read_text(encoding="utf-8"))
|
||||
CURRENT_LANG_CODE = lang_code
|
||||
else:
|
||||
with resources.as_file(res) as locale_file:
|
||||
if not locale_file.exists():
|
||||
@@ -155,10 +165,12 @@ def load_locale(lang_code: str = "en"):
|
||||
CURRENT_LOCALE = json.loads(fallback_file.read_text(encoding="utf-8"))
|
||||
else:
|
||||
CURRENT_LOCALE = json.loads(locale_file.read_text(encoding="utf-8"))
|
||||
CURRENT_LANG_CODE = lang_code
|
||||
except Exception as e:
|
||||
print(f"Error loading locale {lang_code}: {e}")
|
||||
# Final safety fallback to empty dict to avoid KeyErrors
|
||||
CURRENT_LOCALE = {"cli": {}, "web": {}, "meta": {}}
|
||||
CURRENT_LANG_CODE = "en"
|
||||
|
||||
|
||||
def t(path: str, **kwargs) -> str:
|
||||
@@ -172,6 +184,14 @@ def t(path: str, **kwargs) -> str:
|
||||
return val.format(**kwargs)
|
||||
|
||||
|
||||
def t_fallback(path: str, default: str, **kwargs) -> str:
|
||||
"""Retrieve a localized string, but fall back to a provided default."""
|
||||
value = t(path, **kwargs)
|
||||
if value == path:
|
||||
return default.format(**kwargs)
|
||||
return value
|
||||
|
||||
|
||||
def require_cmd(cmd: str):
|
||||
if cmd == "onionshare-cli":
|
||||
try:
|
||||
@@ -233,6 +253,84 @@ def check_deps():
|
||||
console.print(f"[red]{t('cli.all_deps_installed')}[/red]")
|
||||
|
||||
|
||||
def print_doctor_report():
|
||||
print_report("doctor")
|
||||
|
||||
|
||||
def print_platform_status():
|
||||
print_report("platform-status")
|
||||
|
||||
|
||||
def print_report(report_type: str, json_output: bool = False):
|
||||
adapter = get_platform_adapter(cli_module=sys.modules[__name__])
|
||||
report = adapter.doctor_report() if report_type == "doctor" else adapter.platform_status()
|
||||
if json_output:
|
||||
print(json.dumps(report, indent=2))
|
||||
return
|
||||
|
||||
console = Console()
|
||||
if report_type == "doctor":
|
||||
console.rule(f"VoxVera Doctor: {report['label']}")
|
||||
console.print(f"Tier: {report['tier']}")
|
||||
if report["hosting_model"]:
|
||||
console.print(f"Hosting model: {report['hosting_model']}")
|
||||
summary = report.get("summary", {})
|
||||
if summary:
|
||||
console.print(f"Overall: {'OK' if summary.get('ok') else 'WARN'}")
|
||||
for section in report.get("sections", {}).values():
|
||||
if not section.get("checks"):
|
||||
continue
|
||||
console.print(f"{section['title']}: {'OK' if section.get('ok') else 'WARN'}")
|
||||
for check in report["checks"]:
|
||||
style = "green" if check["ok"] else "red"
|
||||
state = "OK" if check["ok"] else "FAIL"
|
||||
console.print(f"[{style}]{state}[/{style}] {check['name']}: {check['detail']}")
|
||||
return
|
||||
|
||||
console.rule(f"Platform Status: {report['label']}")
|
||||
console.print(f"Tier: {report['tier']}")
|
||||
if report["tier_description"]:
|
||||
console.print(report["tier_description"])
|
||||
if report["hosting_model"]:
|
||||
console.print(f"Hosting model: {report['hosting_model']}")
|
||||
if report["distribution_surfaces"]:
|
||||
console.print("Distribution surfaces:")
|
||||
for surface in report["distribution_surfaces"]:
|
||||
console.print(f" - {surface}")
|
||||
if report["required_capabilities"]:
|
||||
console.print("Required capabilities:")
|
||||
for capability in report["required_capabilities"]:
|
||||
console.print(f" - {capability}")
|
||||
if report["current_notes"]:
|
||||
console.print("Current notes:")
|
||||
for note in report["current_notes"]:
|
||||
console.print(f" - {note}")
|
||||
if report["next_gaps"]:
|
||||
console.print("Next gaps:")
|
||||
for gap in report["next_gaps"]:
|
||||
console.print(f" - {gap}")
|
||||
|
||||
|
||||
def print_autostart_status(json_output: bool = False):
|
||||
adapter = get_platform_adapter(cli_module=sys.modules[__name__])
|
||||
status = adapter.autostart_status()
|
||||
if json_output:
|
||||
print(json.dumps(status, indent=2))
|
||||
return
|
||||
console = Console()
|
||||
console.rule(f"Autostart Status: {status['label']}")
|
||||
console.print(f"Tier: {status['tier']}")
|
||||
console.print(status["message"])
|
||||
if status["artifacts"]:
|
||||
console.print("Artifacts:")
|
||||
for artifact in status["artifacts"]:
|
||||
console.print(f" - {artifact}")
|
||||
for check in status["checks"]:
|
||||
style = "green" if check["ok"] else "red"
|
||||
state = "OK" if check["ok"] else "FAIL"
|
||||
console.print(f"[{style}]{state}[/{style}] {check['name']}: {check['detail']}")
|
||||
|
||||
|
||||
def load_config(path: str) -> dict:
|
||||
with open(path, "r") as fh:
|
||||
return normalize_config(json.load(fh))
|
||||
@@ -243,6 +341,18 @@ def save_config(data: dict, path: str):
|
||||
json.dump(normalize_config(data), fh, indent=2)
|
||||
|
||||
|
||||
def ensure_config_exists(path: str | Path) -> Path:
|
||||
"""Seed a config file from the bundled defaults when missing."""
|
||||
config_path = Path(path)
|
||||
if config_path.exists():
|
||||
return config_path
|
||||
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
default_config = json.loads((_src_res("config.json")).read_text(encoding="utf-8"))
|
||||
config_path.write_text(json.dumps(normalize_config(default_config), indent=2), encoding="utf-8")
|
||||
return config_path
|
||||
|
||||
|
||||
def _open_editor_terminal(initial: str) -> str:
|
||||
"""Fallback to opening the user's $EDITOR in the terminal."""
|
||||
import tempfile
|
||||
@@ -357,12 +467,82 @@ def _len_validator(limit: int):
|
||||
def _folder_name_validator(val: str):
|
||||
if len(val) > 63:
|
||||
return "Folder name must be at most 63 characters"
|
||||
if not re.fullmatch(r"[a-z0-9-]+", val):
|
||||
return "Use only lowercase letters, numbers and '-'"
|
||||
if not re.fullmatch(r"[a-z0-9_-]+", val):
|
||||
return "Use only lowercase letters, numbers, '-' and '_'"
|
||||
return True
|
||||
|
||||
|
||||
def _next_versioned_folder_name(folder_name: str) -> str:
|
||||
"""Return the next available versioned folder name like jesus_01."""
|
||||
host_dir = DATA_DIR / "host"
|
||||
for index in range(1, 100):
|
||||
candidate = f"{folder_name}_{index:02d}"
|
||||
if not (host_dir / candidate).exists():
|
||||
return candidate
|
||||
raise RuntimeError(f"Could not allocate a versioned folder name for '{folder_name}'")
|
||||
|
||||
|
||||
def resolve_new_site_folder(config_path: str, preserve_session_on_overwrite: bool = True) -> str | None:
|
||||
"""Resolve collisions for a newly created site before the first build."""
|
||||
data = load_config(config_path)
|
||||
folder_name = data["folder_name"]
|
||||
dest_dir = DATA_DIR / "host" / folder_name
|
||||
if not dest_dir.exists():
|
||||
return folder_name
|
||||
|
||||
suggested_name = _next_versioned_folder_name(folder_name)
|
||||
action = inquirer.select(
|
||||
message=t_fallback(
|
||||
"cli.site_exists_action",
|
||||
"Site '{name}' already exists. What do you want to do?",
|
||||
name=folder_name,
|
||||
),
|
||||
choices=[
|
||||
{
|
||||
"name": t_fallback("cli.site_exists_overwrite", "Overwrite existing site"),
|
||||
"value": "overwrite",
|
||||
},
|
||||
{
|
||||
"name": t_fallback(
|
||||
"cli.site_exists_version",
|
||||
"Create new version ({suggested})",
|
||||
suggested=suggested_name,
|
||||
),
|
||||
"value": "version",
|
||||
},
|
||||
{
|
||||
"name": t_fallback("cli.site_exists_cancel", "Cancel"),
|
||||
"value": "cancel",
|
||||
},
|
||||
],
|
||||
default="version",
|
||||
).execute()
|
||||
|
||||
if action in (None, "cancel"):
|
||||
print(t_fallback("cli.site_exists_cancelled", "Site creation cancelled."))
|
||||
return None
|
||||
|
||||
if action == "overwrite":
|
||||
if preserve_session_on_overwrite:
|
||||
_clear_host_dir(dest_dir)
|
||||
else:
|
||||
shutil.rmtree(dest_dir, ignore_errors=True)
|
||||
return folder_name
|
||||
|
||||
data["folder_name"] = suggested_name
|
||||
save_config(data, config_path)
|
||||
print(
|
||||
t_fallback(
|
||||
"cli.site_exists_versioned",
|
||||
"Using versioned folder name: {name}",
|
||||
name=suggested_name,
|
||||
)
|
||||
)
|
||||
return suggested_name
|
||||
|
||||
|
||||
def interactive_update(config_path: str):
|
||||
ensure_config_exists(config_path)
|
||||
data = load_config(config_path)
|
||||
console = Console()
|
||||
|
||||
@@ -1362,6 +1542,7 @@ def create_from_template(console):
|
||||
# Load the preset JSON
|
||||
preset_data = json.loads(Path(selected["file"]).read_text(encoding="utf-8"))
|
||||
template_id = selected["id"]
|
||||
preset_data["lang"] = CURRENT_LANG_CODE or preset_data.get("lang", "en")
|
||||
|
||||
# Override content fields with locale translations if available
|
||||
for field in ["name", "title", "subtitle", "headline", "content", "url_message"]:
|
||||
@@ -1375,6 +1556,8 @@ def create_from_template(console):
|
||||
|
||||
# Let user customize, build, and serve
|
||||
interactive_update(str(config_path))
|
||||
if resolve_new_site_folder(str(config_path)) is None:
|
||||
return
|
||||
build_assets(str(config_path))
|
||||
serve(str(config_path))
|
||||
|
||||
@@ -1530,20 +1713,23 @@ def stop_all_servers():
|
||||
|
||||
def install_autostart():
|
||||
"""Install a platform-appropriate mechanism to start sites on boot."""
|
||||
import platform
|
||||
system = platform.system()
|
||||
|
||||
if system == "Linux":
|
||||
_install_systemd_autostart()
|
||||
elif system == "Darwin":
|
||||
_install_launchd_autostart()
|
||||
elif system == "Windows":
|
||||
_install_windows_autostart()
|
||||
else:
|
||||
print(f"Autostart not supported on {system}. "
|
||||
adapter = get_platform_adapter(cli_module=sys.modules[__name__])
|
||||
try:
|
||||
adapter.install_autostart()
|
||||
except NotImplementedError:
|
||||
print(f"Autostart not supported on {adapter.label}. "
|
||||
"Run 'voxvera start-all' manually or add it to your init system.")
|
||||
|
||||
|
||||
def uninstall_autostart():
|
||||
"""Remove the platform-appropriate autostart mechanism."""
|
||||
adapter = get_platform_adapter(cli_module=sys.modules[__name__])
|
||||
try:
|
||||
adapter.uninstall_autostart()
|
||||
except NotImplementedError:
|
||||
print(f"Autostart uninstall not supported on {adapter.label}.")
|
||||
|
||||
|
||||
def _find_voxvera_bin() -> str:
|
||||
"""Return the absolute path to the voxvera binary/script."""
|
||||
voxvera_bin = shutil.which("voxvera")
|
||||
@@ -1554,90 +1740,21 @@ def _find_voxvera_bin() -> str:
|
||||
|
||||
|
||||
def _install_systemd_autostart():
|
||||
"""Install a systemd user service for Linux."""
|
||||
service_dir = Path.home() / ".config" / "systemd" / "user"
|
||||
service_dir.mkdir(parents=True, exist_ok=True)
|
||||
service_file = service_dir / "voxvera.service"
|
||||
voxvera_bin = _find_voxvera_bin()
|
||||
|
||||
unit = f"""\
|
||||
[Unit]
|
||||
Description=VoxVera Onion Sites
|
||||
After=network-online.target tor.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart={voxvera_bin} start-all
|
||||
ExecStop={voxvera_bin} stop-all
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
"""
|
||||
service_file.write_text(unit)
|
||||
os.system("systemctl --user daemon-reload")
|
||||
os.system("systemctl --user enable voxvera.service")
|
||||
os.system("systemctl --user start voxvera.service")
|
||||
# loginctl enable-linger ensures user services start at boot even without login
|
||||
os.system(f"loginctl enable-linger {os.environ.get('USER', '')}")
|
||||
print(f"Systemd user service installed: {service_file}")
|
||||
print("Sites will start automatically on boot.")
|
||||
print(" Manage with: systemctl --user [start|stop|status|disable] voxvera")
|
||||
"""Backward-compatible wrapper for Linux autostart installation."""
|
||||
adapter = get_platform_adapter(cli_module=sys.modules[__name__])
|
||||
adapter.install_autostart()
|
||||
|
||||
|
||||
def _install_launchd_autostart():
|
||||
"""Install a launchd plist for macOS."""
|
||||
agents_dir = Path.home() / "Library" / "LaunchAgents"
|
||||
agents_dir.mkdir(parents=True, exist_ok=True)
|
||||
plist_file = agents_dir / "org.voxvera.start-all.plist"
|
||||
voxvera_bin = _find_voxvera_bin()
|
||||
|
||||
plist = f"""\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>org.voxvera.start-all</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{voxvera_bin}</string>
|
||||
<string>start-all</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{Path.home()}/host/voxvera-autostart.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{Path.home()}/host/voxvera-autostart.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
plist_file.write_text(plist)
|
||||
os.system(f"launchctl load -w {plist_file}")
|
||||
print(f"LaunchAgent installed: {plist_file}")
|
||||
print("Sites will start automatically on login.")
|
||||
print(f" Disable with: launchctl unload {plist_file}")
|
||||
"""Backward-compatible wrapper for macOS autostart installation."""
|
||||
adapter = get_platform_adapter(cli_module=sys.modules[__name__])
|
||||
adapter.install_autostart()
|
||||
|
||||
|
||||
def _install_windows_autostart():
|
||||
"""Add a startup shortcut or scheduled task on Windows."""
|
||||
voxvera_bin = _find_voxvera_bin()
|
||||
# Use Task Scheduler for a reliable boot-time trigger
|
||||
task_name = "VoxVera Start All"
|
||||
cmd = (
|
||||
f'schtasks /Create /F /SC ONLOGON /TN "{task_name}" '
|
||||
f'/TR "\"{voxvera_bin}\" start-all" /RL HIGHEST'
|
||||
)
|
||||
ret = os.system(cmd)
|
||||
if ret == 0:
|
||||
print(f"Windows scheduled task '{task_name}' created.")
|
||||
print("Sites will start automatically on login.")
|
||||
print(f' Remove with: schtasks /Delete /TN "{task_name}" /F')
|
||||
else:
|
||||
print("Failed to create scheduled task. Try running as Administrator.")
|
||||
"""Backward-compatible wrapper for Windows autostart installation."""
|
||||
adapter = get_platform_adapter(cli_module=sys.modules[__name__])
|
||||
adapter.install_autostart()
|
||||
|
||||
|
||||
def manage_servers():
|
||||
@@ -1696,6 +1813,8 @@ def manage_servers():
|
||||
|
||||
try:
|
||||
interactive_update(str(config_path))
|
||||
if resolve_new_site_folder(str(config_path)) is None:
|
||||
continue
|
||||
build_assets(str(config_path))
|
||||
serve(str(config_path))
|
||||
except (KeyboardInterrupt, Exception) as e:
|
||||
@@ -1888,13 +2007,19 @@ def main(argv=None):
|
||||
help="Skip interactive prompts and use existing config",
|
||||
)
|
||||
sub.add_parser("check", help="Check for required external dependencies")
|
||||
p_doctor = sub.add_parser("doctor", help="Run platform-aware runtime diagnostics")
|
||||
p_doctor.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
|
||||
p_platform_status = sub.add_parser("platform-status", help="Show detected platform support tier and runtime posture")
|
||||
p_platform_status.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
|
||||
sub.add_parser("vendorize", help="Download all dependencies into voxvera/vendor/ for portability")
|
||||
sub.add_parser("build-docs", help="Generate localized documentation from templates")
|
||||
sub.add_parser("build-site", help="Refresh the main site/ directory assets and bundle")
|
||||
sub.add_parser("manage", help="Manage VoxVera servers interactively")
|
||||
sub.add_parser("start-all", help="Start all configured sites (non-interactive)")
|
||||
sub.add_parser("stop-all", help="Stop all running sites (non-interactive)")
|
||||
sub.add_parser("autostart", help="Install/remove a systemd user service to start sites on boot")
|
||||
p_autostart = sub.add_parser("autostart", help="Install or inspect platform autostart support")
|
||||
p_autostart.add_argument("action", nargs="?", choices=["install", "status", "uninstall"], default="install")
|
||||
p_autostart.add_argument("--json", action="store_true", help="Emit machine-readable JSON for status")
|
||||
sub.add_parser("rebuild-all", help="Rebuild all existing sites from the current template (preserves keys and config)")
|
||||
sub.add_parser("_internal_onionshare", help=argparse.SUPPRESS)
|
||||
|
||||
@@ -1918,7 +2043,10 @@ def main(argv=None):
|
||||
pass
|
||||
|
||||
if not current_lang and args.command in ("init", "quickstart", "manage"):
|
||||
current_lang = choose_language()
|
||||
if getattr(args, "non_interactive", False):
|
||||
current_lang = "en"
|
||||
else:
|
||||
current_lang = choose_language()
|
||||
# Save choice to config if possible
|
||||
if config_path.exists():
|
||||
config_data["lang"] = current_lang
|
||||
@@ -1951,7 +2079,13 @@ def main(argv=None):
|
||||
if args.template:
|
||||
copy_template(args.template)
|
||||
return
|
||||
elif not args.non_interactive:
|
||||
ensure_config_exists(config_path)
|
||||
if current_lang:
|
||||
config_data = load_config(str(config_path))
|
||||
if not config_data.get("lang"):
|
||||
config_data["lang"] = current_lang
|
||||
save_config(config_data, str(config_path))
|
||||
if not args.non_interactive:
|
||||
interactive_update(config_path)
|
||||
build_assets(config_path)
|
||||
elif args.command == "build":
|
||||
@@ -1975,8 +2109,19 @@ def main(argv=None):
|
||||
elif args.command == "stop-all":
|
||||
stop_all_servers()
|
||||
elif args.command == "autostart":
|
||||
install_autostart()
|
||||
if args.action == "status":
|
||||
print_autostart_status(json_output=args.json)
|
||||
elif args.action == "uninstall":
|
||||
uninstall_autostart()
|
||||
else:
|
||||
install_autostart()
|
||||
elif args.command == "quickstart":
|
||||
ensure_config_exists(config_path)
|
||||
if current_lang:
|
||||
config_data = load_config(str(config_path))
|
||||
if not config_data.get("lang"):
|
||||
config_data["lang"] = current_lang
|
||||
save_config(config_data, str(config_path))
|
||||
if not args.non_interactive:
|
||||
if not sys.stdin.isatty():
|
||||
print(
|
||||
@@ -1987,8 +2132,13 @@ def main(argv=None):
|
||||
sys.exit(1)
|
||||
interactive_update(config_path)
|
||||
build_assets(config_path)
|
||||
serve(config_path)
|
||||
elif args.command == "check":
|
||||
check_deps()
|
||||
elif args.command == "doctor":
|
||||
print_report("doctor", json_output=args.json)
|
||||
elif args.command == "platform-status":
|
||||
print_report("platform-status", json_output=args.json)
|
||||
elif args.command == "vendorize":
|
||||
vendorize()
|
||||
elif args.command == "build-docs":
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import platform
|
||||
|
||||
from .base import PlatformAdapter
|
||||
from .linux import LinuxPlatformAdapter
|
||||
from .macos import MacOSPlatformAdapter
|
||||
from .windows import WindowsPlatformAdapter
|
||||
|
||||
|
||||
def get_platform_adapter(cli_module=None) -> PlatformAdapter:
|
||||
system = platform.system()
|
||||
if system == "Linux":
|
||||
return LinuxPlatformAdapter(cli_module=cli_module)
|
||||
if system == "Darwin":
|
||||
return MacOSPlatformAdapter(cli_module=cli_module)
|
||||
if system == "Windows":
|
||||
return WindowsPlatformAdapter(cli_module=cli_module)
|
||||
return PlatformAdapter(cli_module=cli_module)
|
||||
@@ -0,0 +1,163 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class PlatformAdapter:
|
||||
platform_id = "generic"
|
||||
tier = "experimental"
|
||||
label = "Generic platform"
|
||||
|
||||
def __init__(self, cli_module=None):
|
||||
self.cli = cli_module
|
||||
|
||||
@property
|
||||
def home_dir(self) -> Path:
|
||||
return Path.home()
|
||||
|
||||
@property
|
||||
def data_dir(self) -> Path:
|
||||
if self.cli is not None:
|
||||
return Path(self.cli.DATA_DIR)
|
||||
return self.home_dir
|
||||
|
||||
@property
|
||||
def repo_root(self) -> Path:
|
||||
if self.cli is not None and hasattr(self.cli, "ROOT"):
|
||||
return Path(self.cli.ROOT).parent
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
@property
|
||||
def support_matrix_path(self) -> Path:
|
||||
return self.repo_root / "support-matrix.json"
|
||||
|
||||
def _run_capture(self, args: list[str]) -> tuple[bool, str]:
|
||||
try:
|
||||
output = subprocess.check_output(args, stderr=subprocess.STDOUT).decode().strip()
|
||||
return True, output
|
||||
except Exception as exc:
|
||||
return False, str(exc)
|
||||
|
||||
def load_support_matrix(self) -> dict:
|
||||
if not self.support_matrix_path.exists():
|
||||
return {"platforms": [], "support_tiers": {}}
|
||||
return json.loads(self.support_matrix_path.read_text(encoding="utf-8"))
|
||||
|
||||
def support_entry(self) -> dict:
|
||||
matrix = self.load_support_matrix()
|
||||
for entry in matrix.get("platforms", []):
|
||||
if entry.get("id") == self.platform_id:
|
||||
return entry
|
||||
return {
|
||||
"id": self.platform_id,
|
||||
"label": self.label,
|
||||
"tier": self.tier,
|
||||
"hosting_model": "",
|
||||
"distribution_surfaces": [],
|
||||
"required_capabilities": [],
|
||||
"current_notes": [],
|
||||
"next_gaps": [],
|
||||
}
|
||||
|
||||
def autostart_status(self) -> dict:
|
||||
return {
|
||||
"platform_id": self.platform_id,
|
||||
"label": self.label,
|
||||
"tier": self.tier,
|
||||
"supported": False,
|
||||
"message": "Autostart status is not implemented for this platform.",
|
||||
"artifacts": [],
|
||||
"checks": [],
|
||||
}
|
||||
|
||||
def install_autostart(self) -> None:
|
||||
raise NotImplementedError("Autostart is not implemented for this platform.")
|
||||
|
||||
def uninstall_autostart(self) -> None:
|
||||
raise NotImplementedError("Autostart uninstall is not implemented for this platform.")
|
||||
|
||||
def platform_status(self) -> dict:
|
||||
entry = self.support_entry()
|
||||
matrix = self.load_support_matrix()
|
||||
return {
|
||||
"platform_id": self.platform_id,
|
||||
"label": entry.get("label", self.label),
|
||||
"tier": entry.get("tier", self.tier),
|
||||
"tier_description": matrix.get("support_tiers", {}).get(entry.get("tier", self.tier), ""),
|
||||
"hosting_model": entry.get("hosting_model", ""),
|
||||
"distribution_surfaces": entry.get("distribution_surfaces", []),
|
||||
"required_capabilities": entry.get("required_capabilities", []),
|
||||
"current_notes": entry.get("current_notes", []),
|
||||
"next_gaps": entry.get("next_gaps", []),
|
||||
}
|
||||
|
||||
def _doctor_section(self, title: str, check_names: list[str], checks: list[dict]) -> dict:
|
||||
section_checks = [check for check in checks if check["name"] in check_names]
|
||||
return {
|
||||
"title": title,
|
||||
"ok": all(check.get("ok") for check in section_checks) if section_checks else True,
|
||||
"checks": section_checks,
|
||||
}
|
||||
|
||||
def build_doctor_sections(self, checks: list[dict]) -> dict:
|
||||
return {
|
||||
"platform_contract": self._doctor_section("Platform contract", ["platform"], checks),
|
||||
"dependencies": self._doctor_section("Dependencies", ["voxvera_cli", "onionshare_cli"], checks),
|
||||
"network": self._doctor_section("Network", [], checks),
|
||||
"autostart": self._doctor_section("Autostart", [], checks),
|
||||
"content_state": self._doctor_section("Content state", [], checks),
|
||||
}
|
||||
|
||||
def doctor_report(self) -> dict:
|
||||
checks = []
|
||||
voxvera_path = shutil.which("voxvera")
|
||||
onionshare_path = shutil.which("onionshare-cli") or shutil.which("onionshare")
|
||||
platform_status = self.platform_status()
|
||||
|
||||
checks.append(
|
||||
{
|
||||
"name": "platform",
|
||||
"ok": True,
|
||||
"detail": f"{platform.system()} ({self.platform_id}, tier={platform_status['tier']})",
|
||||
}
|
||||
)
|
||||
checks.append(
|
||||
{
|
||||
"name": "voxvera_cli",
|
||||
"ok": bool(voxvera_path),
|
||||
"detail": voxvera_path or "voxvera not found on PATH",
|
||||
}
|
||||
)
|
||||
checks.append(
|
||||
{
|
||||
"name": "onionshare_cli",
|
||||
"ok": bool(onionshare_path),
|
||||
"detail": onionshare_path or "onionshare-cli/onionshare not found on PATH",
|
||||
}
|
||||
)
|
||||
|
||||
sections = self.build_doctor_sections(checks)
|
||||
return {
|
||||
"platform_id": self.platform_id,
|
||||
"label": platform_status["label"],
|
||||
"tier": platform_status["tier"],
|
||||
"tier_description": platform_status["tier_description"],
|
||||
"hosting_model": platform_status["hosting_model"],
|
||||
"distribution_surfaces": platform_status["distribution_surfaces"],
|
||||
"required_capabilities": platform_status["required_capabilities"],
|
||||
"current_notes": platform_status["current_notes"],
|
||||
"next_gaps": platform_status["next_gaps"],
|
||||
"summary": {
|
||||
"ok": all(check.get("ok") for check in checks),
|
||||
"failing_checks": [check["name"] for check in checks if not check.get("ok")],
|
||||
},
|
||||
"sections": sections,
|
||||
"checks": checks,
|
||||
}
|
||||
|
||||
def autostart_artifacts(self) -> list[str]:
|
||||
return []
|
||||
@@ -0,0 +1,246 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from .base import PlatformAdapter
|
||||
|
||||
|
||||
class LinuxPlatformAdapter(PlatformAdapter):
|
||||
platform_id = "linux_cli_systemd"
|
||||
tier = "supported"
|
||||
label = "Linux CLI with systemd --user"
|
||||
|
||||
@property
|
||||
def user_systemd_dir(self) -> Path:
|
||||
return self.home_dir / ".config" / "systemd" / "user"
|
||||
|
||||
@property
|
||||
def service_path(self) -> Path:
|
||||
return self.user_systemd_dir / "voxvera-start.service"
|
||||
|
||||
@property
|
||||
def timer_path(self) -> Path:
|
||||
return self.user_systemd_dir / "voxvera-start.timer"
|
||||
|
||||
def autostart_artifacts(self) -> list[str]:
|
||||
return [str(self.service_path), str(self.timer_path)]
|
||||
|
||||
def render_systemd_service(self, voxvera_bin: str) -> str:
|
||||
return f"""\
|
||||
[Unit]
|
||||
Description=VoxVera hidden-service recovery runner
|
||||
After=network-online.target tor.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart={voxvera_bin} start-all
|
||||
"""
|
||||
|
||||
def render_systemd_timer(self) -> str:
|
||||
return """\
|
||||
[Unit]
|
||||
Description=Retry VoxVera hidden-service startup
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnStartupSec=2min
|
||||
OnUnitActiveSec=5min
|
||||
Unit=voxvera-start.service
|
||||
Persistent=true
|
||||
"""
|
||||
|
||||
def autostart_status(self) -> dict:
|
||||
checks = []
|
||||
artifacts = self.autostart_artifacts()
|
||||
for path in [self.service_path, self.timer_path]:
|
||||
checks.append(
|
||||
{
|
||||
"name": path.name,
|
||||
"ok": path.exists(),
|
||||
"detail": str(path) if path.exists() else f"missing: {path}",
|
||||
}
|
||||
)
|
||||
|
||||
if shutil.which("systemctl"):
|
||||
ok, output = self._run_capture(["systemctl", "--user", "is-enabled", "voxvera-start.timer"])
|
||||
checks.append(
|
||||
{
|
||||
"name": "systemd_timer_enabled",
|
||||
"ok": ok and output == "enabled",
|
||||
"detail": output,
|
||||
}
|
||||
)
|
||||
ok, output = self._run_capture(["systemctl", "--user", "is-active", "voxvera-start.timer"])
|
||||
checks.append(
|
||||
{
|
||||
"name": "systemd_timer_active",
|
||||
"ok": ok and output == "active",
|
||||
"detail": output,
|
||||
}
|
||||
)
|
||||
else:
|
||||
checks.append(
|
||||
{
|
||||
"name": "systemctl",
|
||||
"ok": False,
|
||||
"detail": "systemctl not found",
|
||||
}
|
||||
)
|
||||
|
||||
supported = all(check["ok"] for check in checks if check["name"] in {"voxvera-start.service", "voxvera-start.timer"})
|
||||
return {
|
||||
"platform_id": self.platform_id,
|
||||
"label": self.label,
|
||||
"tier": self.tier,
|
||||
"supported": supported,
|
||||
"message": "Linux recovery timer is the supported persistent-host path.",
|
||||
"artifacts": artifacts,
|
||||
"checks": checks,
|
||||
}
|
||||
|
||||
def install_autostart(self) -> None:
|
||||
if self.cli is None:
|
||||
raise RuntimeError("CLI module is required to install Linux autostart.")
|
||||
self.user_systemd_dir.mkdir(parents=True, exist_ok=True)
|
||||
voxvera_bin = self.cli._find_voxvera_bin()
|
||||
|
||||
self.service_path.write_text(self.render_systemd_service(voxvera_bin), encoding="utf-8")
|
||||
self.timer_path.write_text(self.render_systemd_timer(), encoding="utf-8")
|
||||
|
||||
legacy_service = self.user_systemd_dir / "voxvera.service"
|
||||
if legacy_service.exists():
|
||||
legacy_service.unlink()
|
||||
|
||||
commands = [
|
||||
["systemctl", "--user", "daemon-reload"],
|
||||
["systemctl", "--user", "disable", "--now", "voxvera.service"],
|
||||
["systemctl", "--user", "enable", "--now", "voxvera-start.timer"],
|
||||
["systemctl", "--user", "start", "voxvera-start.service"],
|
||||
]
|
||||
for command in commands:
|
||||
try:
|
||||
subprocess.run(
|
||||
command,
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=10,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
linger_output = ""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["loginctl", "show-user", os.environ.get("USER", ""), "--property=Linger", "--value"],
|
||||
check=False,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
linger_output = (result.stdout or "").strip()
|
||||
if linger_output == "no":
|
||||
print("Note: user linger is not enabled. The recovery timer will run while you are logged in.")
|
||||
print(" To enable boot-time user services, run: sudo loginctl enable-linger $USER")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(f"Systemd user service installed: {self.service_path}")
|
||||
print(f"Systemd user timer installed: {self.timer_path}")
|
||||
print("Sites will be retried automatically after boot and every few minutes.")
|
||||
print(" Manage with: systemctl --user [start|stop|status] voxvera-start.service")
|
||||
print(" Timer status: systemctl --user status voxvera-start.timer")
|
||||
|
||||
def uninstall_autostart(self) -> None:
|
||||
commands = [
|
||||
["systemctl", "--user", "disable", "--now", "voxvera-start.timer"],
|
||||
["systemctl", "--user", "stop", "voxvera-start.service"],
|
||||
["systemctl", "--user", "daemon-reload"],
|
||||
]
|
||||
for command in commands:
|
||||
try:
|
||||
subprocess.run(command, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.service_path.unlink(missing_ok=True)
|
||||
self.timer_path.unlink(missing_ok=True)
|
||||
print(f"Removed systemd user service: {self.service_path}")
|
||||
print(f"Removed systemd user timer: {self.timer_path}")
|
||||
|
||||
def doctor_report(self) -> dict:
|
||||
report = super().doctor_report()
|
||||
checks = report["checks"]
|
||||
|
||||
tor_found = bool(shutil.which("tor"))
|
||||
checks.append(
|
||||
{
|
||||
"name": "tor_binary",
|
||||
"ok": tor_found,
|
||||
"detail": shutil.which("tor") or "tor not found on PATH",
|
||||
}
|
||||
)
|
||||
|
||||
socks_port = os.getenv("TOR_SOCKS_PORT")
|
||||
if not socks_port:
|
||||
for candidate in ("9050", "9150"):
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(0.25)
|
||||
result = sock.connect_ex(("127.0.0.1", int(candidate)))
|
||||
sock.close()
|
||||
if result == 0:
|
||||
socks_port = candidate
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
checks.append(
|
||||
{
|
||||
"name": "tor_socks_reachable",
|
||||
"ok": bool(socks_port),
|
||||
"detail": f"reachable on {socks_port}" if socks_port else "no Tor SOCKS port detected",
|
||||
}
|
||||
)
|
||||
|
||||
status = self.autostart_status()
|
||||
timer_check = next((check for check in status["checks"] if check["name"] == "systemd_timer_enabled"), None)
|
||||
if timer_check is not None:
|
||||
checks.append(
|
||||
{
|
||||
"name": "autostart_timer_enabled",
|
||||
"ok": timer_check["ok"],
|
||||
"detail": timer_check["detail"],
|
||||
}
|
||||
)
|
||||
|
||||
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}",
|
||||
}
|
||||
)
|
||||
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"], checks),
|
||||
"dependencies": self._doctor_section("Dependencies", ["voxvera_cli", "onionshare_cli", "tor_binary"], checks),
|
||||
"network": self._doctor_section("Network", ["tor_socks_reachable"], checks),
|
||||
"autostart": self._doctor_section("Autostart", ["autostart_timer_enabled"], checks),
|
||||
"content_state": self._doctor_section("Content state", ["host_root"], checks),
|
||||
}
|
||||
return report
|
||||
@@ -0,0 +1,95 @@
|
||||
from pathlib import Path
|
||||
|
||||
from .base import PlatformAdapter
|
||||
|
||||
|
||||
class MacOSPlatformAdapter(PlatformAdapter):
|
||||
platform_id = "macos_cli"
|
||||
tier = "experimental"
|
||||
label = "macOS CLI"
|
||||
|
||||
@property
|
||||
def launch_agents_dir(self) -> Path:
|
||||
return self.home_dir / "Library" / "LaunchAgents"
|
||||
|
||||
@property
|
||||
def plist_path(self) -> Path:
|
||||
return self.launch_agents_dir / "org.voxvera.start-all.plist"
|
||||
|
||||
def autostart_artifacts(self) -> list[str]:
|
||||
return [str(self.plist_path)]
|
||||
|
||||
def render_launchd_plist(self, voxvera_bin: str) -> str:
|
||||
home = self.home_dir
|
||||
return f"""\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>org.voxvera.start-all</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{voxvera_bin}</string>
|
||||
<string>start-all</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{home}/host/voxvera-autostart.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{home}/host/voxvera-autostart.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
|
||||
def autostart_status(self) -> dict:
|
||||
checks = [
|
||||
{
|
||||
"name": self.plist_path.name,
|
||||
"ok": self.plist_path.exists(),
|
||||
"detail": str(self.plist_path) if self.plist_path.exists() else f"missing: {self.plist_path}",
|
||||
}
|
||||
]
|
||||
if self._run_capture(["launchctl", "list", "org.voxvera.start-all"])[0]:
|
||||
checks.append(
|
||||
{
|
||||
"name": "launchd_loaded",
|
||||
"ok": True,
|
||||
"detail": "org.voxvera.start-all is loaded",
|
||||
}
|
||||
)
|
||||
else:
|
||||
checks.append(
|
||||
{
|
||||
"name": "launchd_loaded",
|
||||
"ok": False,
|
||||
"detail": "org.voxvera.start-all is not loaded",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"platform_id": self.platform_id,
|
||||
"label": self.label,
|
||||
"tier": self.tier,
|
||||
"supported": False,
|
||||
"message": "macOS launchd autostart is experimental and not validated for reliable hidden-service recovery.",
|
||||
"artifacts": self.autostart_artifacts(),
|
||||
"checks": checks,
|
||||
}
|
||||
|
||||
def install_autostart(self) -> None:
|
||||
if self.cli is None:
|
||||
raise RuntimeError("CLI module is required to install macOS autostart.")
|
||||
self.launch_agents_dir.mkdir(parents=True, exist_ok=True)
|
||||
voxvera_bin = self.cli._find_voxvera_bin()
|
||||
self.plist_path.write_text(self.render_launchd_plist(voxvera_bin), encoding="utf-8")
|
||||
self._run_capture(["launchctl", "load", "-w", str(self.plist_path)])
|
||||
print(f"LaunchAgent installed: {self.plist_path}")
|
||||
print("Sites will start automatically on login.")
|
||||
print(f" Disable with: launchctl unload {self.plist_path}")
|
||||
|
||||
def uninstall_autostart(self) -> None:
|
||||
self._run_capture(["launchctl", "unload", str(self.plist_path)])
|
||||
self.plist_path.unlink(missing_ok=True)
|
||||
print(f"Removed LaunchAgent: {self.plist_path}")
|
||||
@@ -0,0 +1,61 @@
|
||||
from .base import PlatformAdapter
|
||||
|
||||
|
||||
class WindowsPlatformAdapter(PlatformAdapter):
|
||||
platform_id = "windows_cli"
|
||||
tier = "experimental"
|
||||
label = "Windows CLI"
|
||||
task_name = "VoxVera Start All"
|
||||
|
||||
def render_schtasks_create_command(self, voxvera_bin: str) -> list[str]:
|
||||
return [
|
||||
"schtasks",
|
||||
"/Create",
|
||||
"/F",
|
||||
"/SC",
|
||||
"ONLOGON",
|
||||
"/TN",
|
||||
self.task_name,
|
||||
"/TR",
|
||||
f'"{voxvera_bin}" start-all',
|
||||
"/RL",
|
||||
"HIGHEST",
|
||||
]
|
||||
|
||||
def autostart_status(self) -> dict:
|
||||
ok, output = self._run_capture(["schtasks", "/Query", "/TN", self.task_name])
|
||||
checks = [
|
||||
{
|
||||
"name": "scheduled_task_present",
|
||||
"ok": ok,
|
||||
"detail": output if ok else f"missing or unreadable: {self.task_name}",
|
||||
}
|
||||
]
|
||||
return {
|
||||
"platform_id": self.platform_id,
|
||||
"label": self.label,
|
||||
"tier": self.tier,
|
||||
"supported": False,
|
||||
"message": "Windows Task Scheduler autostart is experimental and not validated for reliable hidden-service recovery.",
|
||||
"artifacts": [],
|
||||
"checks": checks,
|
||||
}
|
||||
|
||||
def install_autostart(self) -> None:
|
||||
if self.cli is None:
|
||||
raise RuntimeError("CLI module is required to install Windows autostart.")
|
||||
voxvera_bin = self.cli._find_voxvera_bin()
|
||||
ok, _output = self._run_capture(self.render_schtasks_create_command(voxvera_bin))
|
||||
if ok:
|
||||
print(f"Windows scheduled task '{self.task_name}' created.")
|
||||
print("Sites will start automatically on login.")
|
||||
print(f' Remove with: schtasks /Delete /TN "{self.task_name}" /F')
|
||||
return
|
||||
print("Failed to create scheduled task. Try running as Administrator.")
|
||||
|
||||
def uninstall_autostart(self) -> None:
|
||||
ok, _output = self._run_capture(["schtasks", "/Delete", "/TN", self.task_name, "/F"])
|
||||
if ok:
|
||||
print(f"Windows scheduled task '{self.task_name}' removed.")
|
||||
return
|
||||
print(f"Windows scheduled task '{self.task_name}' could not be removed.")
|
||||
Reference in new issue
Block a user