The earlier script installed a systemd *user* service, which conflated two separate decisions. What matters in host mode is which account jobs run as, because there is no container boundary and the service account is the only sandbox. A system service with User= set gets unprivileged jobs *and* a root-managed lifecycle that starts at boot with no lingering session. Jobs now run as a dedicated `gitea-runner` account in the flutterdev group, with its own HOME so its Gradle and pub caches are its own. The alternative — a human account — would hand every workflow that user's home directory and SSH keys, which is a sharper edge than usual here because a workflow file is code and much of this repo is agent-authored. Running as root would be worse still, and would also miss the warm caches in a human HOME and re-download dependencies every run. flutterdev is what grants write access to /opt/flutter and /opt/android-sdk. Group membership alone does not prove it, since both carry ACLs, so the script tests writability as the service account and fails with the getfacl command to run. Tool lookups use systemd's default PATH rather than an interactive shell's, because that is what the service will actually have. Adds --check, which runs pre-flight and exits without creating anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
iperf3-gui
A Flutter-based Android GUI wrapper for iperf3, the popular network performance measurement tool.
Features
- Client-mode network throughput testing
- Configurable server, port, duration and parallel streams
- Live iperf3 output, streamed line by line while the test runs
- Stop a running test early
- Copy the transcript to the clipboard
- iperf3 3.19 compiled into the app as a native library — no external binary
Architecture
lib/
├── domain/
│ ├── models/ # TestConfiguration, Iperf3Result
├── data/
│ └── services/ # Iperf3Service (MethodChannel → JNI)
├── ui/
│ └── features/
│ └── test_run/ # TestRunScreen, TestRunViewModel
└── main.dart # App entry point
android/app/src/main/
├── kotlin/…/IperfRunner.kt # MethodChannel handler, runs tests off the platform thread
└── cpp/iperf_jni.c # JNI bridge into the iperf3 API
See docs/README.md for the data flow, the native API surface and the design decisions behind them.
Why JNI instead of executing a binary
Android SELinux in Enforcing mode blocks execve() from the untrusted_app
domain, so shipping an iperf3 ELF in assets and running it via Process.start()
fails with "Permission denied". iperf3 is therefore compiled as a shared library
(libiperf3jni.so) and called in-process over JNI.
assets/iperf3/ still holds the pre-compiled binaries from that earlier
approach. They are not declared in pubspec.yaml, so they are not packaged into
the APK, and nothing in the app reads them.
Building
Prerequisites:
- Flutter 3.47+
- Android SDK with platform android-36
- JDK 17
- NDK r28+ and CMake 3.22.1 (for the native build)
flutter pub get
flutter analyze
flutter test
flutter build apk --debug
adb install -r build/app/outputs/flutter-apk/app-debug.apk
Testing against a local server
iperf3 -s -p 5201 # on a machine on the same network
ip -4 addr show # note its address
Enter that address in the app, then press GO.
Dependencies
- Dart:
provider - Native: iperf3 3.19 + cJSON, built by CMake for
armeabi-v7aandarm64-v8a
License
MIT — bundled iperf3 3.19 is BSD-3-Clause licensed.