Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,57 @@ jobs:
path: composeApp/build/reports/tests/desktopTest/
retention-days: 7

cli-tests:
name: CLI Tests & Packaging
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- name: Setup
uses: ./.github/actions/setup

- name: CLI + core unit tests
run: ./gradlew :cliApp:test :probeCore:desktopTest

- name: Dependency boundary checks
run: |
fail=0
if grep -REn "org\.ooni\.probe\.ui\.|androidx\.compose|org\.jetbrains\.compose\.resources|androidx\.navigation|ooniprobe\.composeapp\.generated\.resources" probeCore/src; then
echo "::error::probeCore/src contains forbidden UI/Compose/navigation imports"; fail=1
fi
if find probeCore/src -path '*composeResources*' -print -quit | grep -q .; then
echo "::error::probeCore/src contains a composeResources directory"; fail=1
fi
if grep -REn "androidx\.compose|org\.jetbrains\.compose|navigation-compose|composeResources|ooniprobe\.composeapp\.generated\.resources" cliApp/src cliApp/build.gradle.kts; then
echo "::error::cliApp contains forbidden Compose references"; fail=1
fi
if grep -REn "project\(\":composeApp\"\)" cliApp/build.gradle.kts probeCore/build.gradle.kts; then
echo "::error::cliApp/probeCore must not depend on :composeApp"; fail=1
fi
exit $fail

- name: Install CLI distribution
run: ./gradlew :cliApp:installDist

- name: Installed-script smoke (temp home, no network)
run: |
set -e
BIN=cliApp/build/install/ooniprobe/bin/ooniprobe
"$BIN" version
"$BIN" help
HOME_DIR=$(mktemp -d)
OONI_HOME="$HOME_DIR" "$BIN" list --json
OONI_HOME="$HOME_DIR" "$BIN" internal descriptor-load --json

- name: Uploads test reports
uses: actions/upload-artifact@v7
if: failure()
with:
name: cli-tests-report
path: cliApp/build/reports/tests/
retention-days: 7

build-android:
name: Build Android
runs-on: ubuntu-latest
Expand Down
106 changes: 106 additions & 0 deletions cliApp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# ooniprobe CLI (`:cliApp`)

A JVM command-line front-end for OONI Probe, aiming for parity with the Go
[`ooni/probe-cli`](https://github.com/ooni/probe-cli) `ooniprobe` command.

## Architecture

`:cliApp` is a thin Kotlin/JVM application built with [Clikt](https://ajalt.github.io/clikt/). It
depends **only on `:probeCore`** — never on `:composeApp` — so it carries no Compose UI, resources,
navigation, or view models. Measurement, upload, storage, onboarding, geolocation, and autorun all
run through the shared engine and orchestration in `:probeCore`, the same code the mobile and desktop
apps use.

For packaged behavior, verify with the installed start script (`installDist`) rather than
`runDebug`/`run`, so the real distribution classpath (including the bundled descriptor assets) is
exercised.

## Commands

| Command | Notes |
|---|---|
| `version` | Print the CLI version. |
| `info` | Print resolved paths and runtime settings (`--json`). |
| `list [resultId]` | List results, or the measurements of a result (`--json`). |
| `show <measurementId>` | Print a stored measurement record as JSON. |
| `rm <resultId>` / `rm --all` | Delete results (`--yes` or interactive confirmation). |
| `reset --force` | Delete all local OONI Probe data (database, files, and stored credentials). |
| `onboard [--yes]` | Run the informed-consent onboarding, or auto-accept. |
| `upload [all\|result <id>\|measurement <id>]` | Upload measurements not yet submitted. |
| `geoip` | Print the probe's IP, ASN, network name, and country (`--json`). |
| `run [group]` | Run measurement groups (see below). |
| `autorun status \| log show \| log stream` | Inspect autorun readiness and logs. |
| `help` | Print usage. |

### `run` groups

Groups: `websites`, `im`, `performance`, `circumvention`, `middlebox`, `experimental`, `unattended`,
`all`. Bare `run` is equivalent to `run all`.

- `run websites` accepts repeatable `--input <url>` and `--input-file <path>`. Inputs are used in
order (all `--input` values first, then each file's non-blank lines); any invalid URL aborts the
run with a file/line diagnostic before measuring.
- `--no-collector` skips uploading results; `--no-creds` skips credential preparation and uses the
anonymous submission path.
- Running requires completed onboarding; in `--batch` mode an incomplete onboarding fails with a
message to run `ooniprobe onboard --yes`.

## Root flags

`--config/-c <file>`, `--verbose/-v`, `--batch`, `--log-handler={cli,batch,syslog}`,
`--software-name`, `--software-version`, `--proxy`, `--json`. Global flags may appear before or after
the subcommand.

## Software name

The identity reported to the OONI backend (measurements, check-in, geolocation) is `ooniprobe-cli`,
distinct from the desktop app's `ooniprobe-desktop`. Auto-run measurements report
`ooniprobe-cli-unattended`.

## Paths

OONI home resolution: the `OONI_HOME` environment variable, else `${user.home}/.ooniprobe`.
`--config <file>` selects the config file only; it does not redirect the home. Data lives under
`<ooniHome>/data`, logs under `<ooniHome>/logs`.

## Build, run, test

```bash
# Unit tests (fast; no device or simulator)
./gradlew :cliApp:test :probeCore:desktopTest

# Install and run the real executable (recommended for smoke checks)
./gradlew :cliApp:installDist
BIN=cliApp/build/install/ooniprobe/bin/ooniprobe
"$BIN" version
"$BIN" help
OONI_HOME=$(mktemp -d) "$BIN" list --json
OONI_HOME=$(mktemp -d) "$BIN" onboard --yes
```

Tests never touch the real `~/.ooniprobe`; they inject a temporary `OONI_HOME` and temp paths.

## Exit codes

- `0` — success.
- `2` — usage/validation errors (bad option, missing/invalid argument, `reset` without `--force`,
a destructive command without `--yes` in `--batch`).
- `1` — runtime errors (result/measurement not found, secure-storage clear failure during `reset`,
an unsupported operation, or an unexpected failure).
- `130` — interrupted by SIGINT/SIGTERM; the in-flight run or upload is cancelled cleanly before exit.

## Deviations from Go probe-cli

- `show` prints the **stored measurement record** (metadata + test keys) as JSON, because the raw
report file is deleted after upload; Go probe-cli prints the raw measurement JSON.
- `reset --force` clears the local OONI home (database, cache, logs, tunnel, assets) and the stored
OONI credentials, and preserves an external `--config` file located outside the home.
- `--help`/`-h` on a subcommand exits `2` (help is handled by the root command); `help` and the root
`--help` exit `0`.

## Known limitations

- `run experimental` currently runs STUN Reachability, OpenVPN, and Vanilla Tor. DNS Check, ECH
Check, and Tor Snowflake are not yet available in the bundled descriptors/engine.
- `autorun start` and `autorun stop` (OS service supervision) are not implemented and return a
deterministic "unsupported on this platform" error; `autorun status` and `autorun log` work.
67 changes: 67 additions & 0 deletions cliApp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.kotlinJvm)
application
id("ooni.common")
}

val appVersionName = libs.versions.app.versionName.get()
val appVersionCode = libs.versions.app.versionCode.get().toInt()

val generateCliBuildConfig = tasks.register("generateCliBuildConfig") {
val outputDir = layout.buildDirectory.dir("generated/cliBuildConfig/kotlin")
inputs.property("versionName", appVersionName)
inputs.property("versionCode", appVersionCode)
outputs.dir(outputDir)
doLast {
val dir = outputDir.get().asFile.resolve("org/ooni/probe/cli")
dir.mkdirs()
dir.resolve("CliBuildConfig.kt").writeText(
"""
|package org.ooni.probe.cli
|
|object CliBuildConfig {
| const val VERSION_NAME = "$appVersionName"
| const val VERSION_CODE = $appVersionCode
|}
""".trimMargin(),
)
}
}

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_25)
}
sourceSets {
main {
kotlin.srcDir(generateCliBuildConfig)
}
}
}

dependencies {
implementation(project(":probeCore"))
// DesktopNetworkTypeFinder (real network type detection instead of an "unknown" stub).
implementation(project(":desktopShared"))
implementation(libs.clikt)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlin.datetime)
// The Cli*Gateway desktop implementations (moved from probeCore) construct SqlDriver/Database,
// DataStore preferences, Json, and okio.FileSystem directly.
implementation(libs.sqldelight.jvm)
implementation(libs.androidx.datastore.preferences.core)
implementation(libs.kotlin.serialization.json)
implementation(libs.okio)

testImplementation(kotlin("test-junit"))
testImplementation(libs.kotlinx.coroutines.test)
}

application {
mainClass = "org.ooni.probe.cli.MainKt"
applicationName = "ooniprobe"
}

version = appVersionName
Loading