Parallel agent work: bandwidth fix, CPU platforms, packaging
All checks were successful
CI / test (push) Successful in 2m8s
All checks were successful
CI / test (push) Successful in 2m8s
5 agents ran in parallel: 1. Fix bandwidth limit (-b): new advance_next_send() prevents drift bursts by resetting when >2x interval behind (bandwidth.rs, client.rs, server.rs) 2. Windows + FreeBSD CPU support (cpu.rs): - Windows: GetSystemTimes via raw FFI - FreeBSD: sysctl kern.cp_time parsing 3. Ubuntu .deb packaging (deploy/deb/): - build-deb.sh: creates .deb from pre-built binary - test-deb.sh: tests in Ubuntu Docker container 4. Fedora/RHEL RPM packaging (deploy/rpm/): - btest-rs.spec: full RPM spec with systemd unit - build-rpm.sh + test-rpm.sh 5. Alpine Linux apk packaging (deploy/alpine/): - APKBUILD with OpenRC init script - test-alpine.sh 58 tests pass, zero warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
52
deploy/alpine/APKBUILD
Normal file
52
deploy/alpine/APKBUILD
Normal file
@@ -0,0 +1,52 @@
|
||||
# Maintainer: Siavash Sameni <manwe at manko dot yoga>
|
||||
pkgname=btest-rs
|
||||
pkgver=0.6.0
|
||||
pkgrel=0
|
||||
pkgdesc="MikroTik Bandwidth Test server and client with EC-SRP5 auth"
|
||||
url="https://github.com/manawenuz/btest-rs"
|
||||
license="MIT AND Apache-2.0"
|
||||
arch="x86_64 aarch64 armv7"
|
||||
makedepends="cargo rust"
|
||||
install="$pkgname.pre-install"
|
||||
source="$pkgname-$pkgver.tar.gz::https://github.com/manawenuz/btest-rs/archive/refs/tags/v$pkgver.tar.gz
|
||||
btest.initd
|
||||
"
|
||||
sha256sums="SKIP
|
||||
SKIP
|
||||
"
|
||||
|
||||
prepare() {
|
||||
default_prepare
|
||||
cd "$builddir"
|
||||
cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$builddir"
|
||||
export CARGO_TARGET_DIR=target
|
||||
cargo build --frozen --release
|
||||
}
|
||||
|
||||
check() {
|
||||
cd "$builddir"
|
||||
cargo test --frozen --release
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$builddir"
|
||||
|
||||
# binary
|
||||
install -Dm755 "target/release/btest" "$pkgdir/usr/bin/btest"
|
||||
|
||||
# man page
|
||||
install -Dm644 "docs/man/btest.1" "$pkgdir/usr/share/man/man1/btest.1"
|
||||
|
||||
# license
|
||||
install -Dm644 "LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
|
||||
# documentation
|
||||
install -Dm644 "README.md" "$pkgdir/usr/share/doc/$pkgname/README.md"
|
||||
|
||||
# OpenRC init script
|
||||
install -Dm755 "$srcdir/btest.initd" "$pkgdir/etc/init.d/btest"
|
||||
}
|
||||
37
deploy/alpine/btest.initd
Executable file
37
deploy/alpine/btest.initd
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/sbin/openrc-run
|
||||
# OpenRC init script for btest-rs
|
||||
# MikroTik Bandwidth Test server
|
||||
|
||||
name="btest"
|
||||
description="MikroTik Bandwidth Test Server (btest-rs)"
|
||||
command="/usr/bin/btest"
|
||||
command_args="-s"
|
||||
command_background=true
|
||||
pidfile="/run/$name.pid"
|
||||
|
||||
# Run as dedicated user if it exists, otherwise root
|
||||
command_user="btest:btest"
|
||||
|
||||
# Logging
|
||||
output_log="/var/log/$name/$name.log"
|
||||
error_log="/var/log/$name/$name.err"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
after firewall
|
||||
use dns logger
|
||||
}
|
||||
|
||||
start_pre() {
|
||||
# Create log directory
|
||||
checkpath -d -m 0755 -o "$command_user" /var/log/$name
|
||||
|
||||
# Create runtime directory
|
||||
checkpath -d -m 0755 -o "$command_user" /run
|
||||
}
|
||||
|
||||
stop() {
|
||||
ebegin "Stopping $name"
|
||||
start-stop-daemon --stop --pidfile "$pidfile" --retry TERM/5/KILL/3
|
||||
eend $?
|
||||
}
|
||||
118
deploy/alpine/test-alpine.sh
Executable file
118
deploy/alpine/test-alpine.sh
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/bin/sh
|
||||
# Test Alpine Linux packaging for btest-rs
|
||||
# Runs inside an Alpine Docker container to build and verify the APK.
|
||||
#
|
||||
# Usage (from repository root):
|
||||
# docker run --rm -v "$PWD":/src alpine:latest /src/deploy/alpine/test-alpine.sh
|
||||
#
|
||||
set -eu
|
||||
|
||||
ALPINE_DIR="/src/deploy/alpine"
|
||||
|
||||
echo "=== Alpine APK packaging test ==="
|
||||
echo "Alpine version: $(cat /etc/alpine-release)"
|
||||
|
||||
# ── Install build dependencies ──────────────────────────────────────
|
||||
echo "--- Installing build dependencies ---"
|
||||
apk update
|
||||
apk add --no-cache \
|
||||
alpine-sdk \
|
||||
rust \
|
||||
cargo \
|
||||
sudo
|
||||
|
||||
# ── Create a non-root build user (abuild refuses to run as root) ──
|
||||
echo "--- Setting up build user ---"
|
||||
adduser -D builder
|
||||
addgroup builder abuild
|
||||
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
||||
|
||||
# ── Prepare build tree ──────────────────────────────────────────────
|
||||
echo "--- Preparing build tree ---"
|
||||
BUILD_DIR="/home/builder/btest-rs"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
cp "$ALPINE_DIR/APKBUILD" "$BUILD_DIR/"
|
||||
cp "$ALPINE_DIR/btest.initd" "$BUILD_DIR/"
|
||||
|
||||
# Generate signing key (required by abuild)
|
||||
su builder -c "abuild-keygen -a -n -q"
|
||||
sudo cp /home/builder/.abuild/*.rsa.pub /etc/apk/keys/
|
||||
|
||||
# ── Build the package ──────────────────────────────────────────────
|
||||
echo "--- Building APK ---"
|
||||
cd "$BUILD_DIR"
|
||||
chown -R builder:builder "$BUILD_DIR"
|
||||
su builder -c "abuild -r"
|
||||
|
||||
echo "--- Build succeeded ---"
|
||||
|
||||
# ── Locate and install the package ──────────────────────────────────
|
||||
echo "--- Installing built APK ---"
|
||||
APK_FILE=$(find /home/builder/packages -name "btest-rs-*.apk" -not -name "*doc*" | head -1)
|
||||
if [ -z "$APK_FILE" ]; then
|
||||
echo "FAIL: APK file not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found APK: $APK_FILE"
|
||||
apk add --allow-untrusted "$APK_FILE"
|
||||
|
||||
# ── Verify installation ────────────────────────────────────────────
|
||||
echo "--- Verifying installation ---"
|
||||
FAIL=0
|
||||
|
||||
# Binary exists and is executable
|
||||
if command -v btest >/dev/null 2>&1; then
|
||||
echo "PASS: btest binary installed"
|
||||
else
|
||||
echo "FAIL: btest binary not found in PATH"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
# Binary runs (show version / help)
|
||||
if btest --help >/dev/null 2>&1; then
|
||||
echo "PASS: btest --help exits successfully"
|
||||
else
|
||||
echo "FAIL: btest --help failed"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
# Man page installed
|
||||
if [ -f /usr/share/man/man1/btest.1 ]; then
|
||||
echo "PASS: man page installed"
|
||||
else
|
||||
echo "FAIL: man page not found"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
# License installed
|
||||
if [ -f /usr/share/licenses/btest-rs/LICENSE ]; then
|
||||
echo "PASS: LICENSE installed"
|
||||
else
|
||||
echo "FAIL: LICENSE not found"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
# OpenRC init script installed
|
||||
if [ -f /etc/init.d/btest ]; then
|
||||
echo "PASS: OpenRC init script installed"
|
||||
else
|
||||
echo "FAIL: OpenRC init script not found"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
# Init script is executable
|
||||
if [ -x /etc/init.d/btest ]; then
|
||||
echo "PASS: init script is executable"
|
||||
else
|
||||
echo "FAIL: init script is not executable"
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
# ── Summary ─────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo "=== All Alpine packaging tests PASSED ==="
|
||||
else
|
||||
echo "=== Some Alpine packaging tests FAILED ==="
|
||||
exit 1
|
||||
fi
|
||||
208
deploy/deb/build-deb.sh
Executable file
208
deploy/deb/build-deb.sh
Executable file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-deb.sh -- Build a Debian/Ubuntu .deb package for btest-rs
|
||||
#
|
||||
# Usage:
|
||||
# ./deploy/deb/build-deb.sh # uses dist/btest or target/release/btest
|
||||
# BTEST_BIN=path/to/btest ./deploy/deb/build-deb.sh
|
||||
#
|
||||
# Requirements: dpkg-deb, gzip (standard on Debian/Ubuntu build hosts)
|
||||
set -euo pipefail
|
||||
|
||||
###############################################################################
|
||||
# Package metadata
|
||||
###############################################################################
|
||||
PKG_NAME="btest-rs"
|
||||
PKG_VERSION="0.6.0"
|
||||
PKG_ARCH="amd64"
|
||||
PKG_MAINTAINER="Siavash Sameni <manwe@manko.yoga>"
|
||||
PKG_DESCRIPTION="MikroTik Bandwidth Test (btest) server and client with EC-SRP5 auth"
|
||||
PKG_HOMEPAGE="https://github.com/manawenuz/btest-rs"
|
||||
PKG_LICENSE="MIT AND Apache-2.0"
|
||||
PKG_SECTION="net"
|
||||
PKG_PRIORITY="optional"
|
||||
|
||||
###############################################################################
|
||||
# Paths
|
||||
###############################################################################
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
# Locate the pre-built binary
|
||||
if [[ -n "${BTEST_BIN:-}" ]]; then
|
||||
: # caller provided an explicit path
|
||||
elif [[ -f "$REPO_ROOT/dist/btest" ]]; then
|
||||
BTEST_BIN="$REPO_ROOT/dist/btest"
|
||||
elif [[ -f "$REPO_ROOT/target/release/btest" ]]; then
|
||||
BTEST_BIN="$REPO_ROOT/target/release/btest"
|
||||
else
|
||||
echo "Error: cannot find btest binary."
|
||||
echo " Build first (cargo build --release) or set BTEST_BIN=path/to/btest"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the binary exists and is executable
|
||||
if [[ ! -f "$BTEST_BIN" ]]; then
|
||||
echo "Error: $BTEST_BIN does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Using binary: $BTEST_BIN"
|
||||
|
||||
###############################################################################
|
||||
# Prepare staging tree
|
||||
###############################################################################
|
||||
DEB_FILE="${PKG_NAME}_${PKG_VERSION}_${PKG_ARCH}.deb"
|
||||
STAGE="$(mktemp -d)"
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
|
||||
echo "==> Staging in $STAGE"
|
||||
|
||||
# Binary
|
||||
install -Dm755 "$BTEST_BIN" "$STAGE/usr/bin/btest"
|
||||
|
||||
# Man page
|
||||
if [[ -f "$REPO_ROOT/docs/man/btest.1" ]]; then
|
||||
install -Dm644 "$REPO_ROOT/docs/man/btest.1" "$STAGE/usr/share/man/man1/btest.1"
|
||||
gzip -9n "$STAGE/usr/share/man/man1/btest.1"
|
||||
else
|
||||
echo "Warning: docs/man/btest.1 not found -- skipping man page"
|
||||
fi
|
||||
|
||||
# systemd service unit
|
||||
install -d "$STAGE/usr/lib/systemd/system"
|
||||
cat > "$STAGE/usr/lib/systemd/system/btest.service" <<'UNIT'
|
||||
[Unit]
|
||||
Description=MikroTik Bandwidth Test Server (btest-rs)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/btest -s
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
DynamicUser=yes
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectControlGroups=yes
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
# Documentation
|
||||
install -Dm644 "$REPO_ROOT/README.md" "$STAGE/usr/share/doc/$PKG_NAME/README.md"
|
||||
|
||||
# License
|
||||
install -Dm644 "$REPO_ROOT/LICENSE" "$STAGE/usr/share/licenses/$PKG_NAME/LICENSE"
|
||||
|
||||
# Debian copyright file (policy-compliant copy in /usr/share/doc)
|
||||
install -d "$STAGE/usr/share/doc/$PKG_NAME"
|
||||
cat > "$STAGE/usr/share/doc/$PKG_NAME/copyright" <<COPY
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: $PKG_NAME
|
||||
Upstream-Contact: $PKG_MAINTAINER
|
||||
Source: $PKG_HOMEPAGE
|
||||
|
||||
Files: *
|
||||
Copyright: 2024-2026 Siavash Sameni
|
||||
License: MIT AND Apache-2.0
|
||||
COPY
|
||||
|
||||
###############################################################################
|
||||
# Calculate installed size (in KiB, as Debian policy requires)
|
||||
###############################################################################
|
||||
INSTALLED_SIZE=$(du -sk "$STAGE" | cut -f1)
|
||||
|
||||
###############################################################################
|
||||
# DEBIAN/control
|
||||
###############################################################################
|
||||
install -d "$STAGE/DEBIAN"
|
||||
cat > "$STAGE/DEBIAN/control" <<CTRL
|
||||
Package: $PKG_NAME
|
||||
Version: $PKG_VERSION
|
||||
Architecture: $PKG_ARCH
|
||||
Maintainer: $PKG_MAINTAINER
|
||||
Installed-Size: $INSTALLED_SIZE
|
||||
Section: $PKG_SECTION
|
||||
Priority: $PKG_PRIORITY
|
||||
Homepage: $PKG_HOMEPAGE
|
||||
Description: $PKG_DESCRIPTION
|
||||
A high-performance Rust implementation of the MikroTik Bandwidth Test
|
||||
protocol, supporting both server and client modes with EC-SRP5
|
||||
authentication. Supports TCP/UDP throughput testing and is fully
|
||||
compatible with RouterOS btest clients.
|
||||
CTRL
|
||||
|
||||
###############################################################################
|
||||
# DEBIAN/conffiles (mark the systemd unit as a conffile)
|
||||
###############################################################################
|
||||
cat > "$STAGE/DEBIAN/conffiles" <<'CF'
|
||||
/usr/lib/systemd/system/btest.service
|
||||
CF
|
||||
|
||||
###############################################################################
|
||||
# Maintainer scripts
|
||||
###############################################################################
|
||||
|
||||
# postinst -- reload systemd after install
|
||||
cat > "$STAGE/DEBIAN/postinst" <<'POST'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ "$1" = "configure" ]; then
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || true
|
||||
echo ""
|
||||
echo "btest-rs installed. To start the server:"
|
||||
echo " sudo systemctl enable --now btest.service"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
POST
|
||||
chmod 755 "$STAGE/DEBIAN/postinst"
|
||||
|
||||
# prerm -- stop service before removal
|
||||
cat > "$STAGE/DEBIAN/prerm" <<'PRERM'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ "$1" = "remove" ] || [ "$1" = "deconfigure" ]; then
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl stop btest.service 2>/dev/null || true
|
||||
systemctl disable btest.service 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
PRERM
|
||||
chmod 755 "$STAGE/DEBIAN/prerm"
|
||||
|
||||
# postrm -- clean up after removal
|
||||
cat > "$STAGE/DEBIAN/postrm" <<'POSTRM'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ "$1" = "purge" ] || [ "$1" = "remove" ]; then
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || true
|
||||
fi
|
||||
fi
|
||||
POSTRM
|
||||
chmod 755 "$STAGE/DEBIAN/postrm"
|
||||
|
||||
###############################################################################
|
||||
# Build .deb
|
||||
###############################################################################
|
||||
OUTPUT_DIR="${OUTPUT_DIR:-$REPO_ROOT/dist}"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo "==> Building $DEB_FILE ..."
|
||||
dpkg-deb --root-owner-group --build "$STAGE" "$OUTPUT_DIR/$DEB_FILE"
|
||||
|
||||
echo "==> Package ready: $OUTPUT_DIR/$DEB_FILE"
|
||||
echo ""
|
||||
dpkg-deb --info "$OUTPUT_DIR/$DEB_FILE"
|
||||
echo ""
|
||||
dpkg-deb --contents "$OUTPUT_DIR/$DEB_FILE"
|
||||
104
deploy/deb/test-deb.sh
Executable file
104
deploy/deb/test-deb.sh
Executable file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
# test-deb.sh -- Smoke-test a btest-rs .deb inside an Ubuntu Docker container
|
||||
#
|
||||
# Usage:
|
||||
# ./deploy/deb/test-deb.sh # auto-finds dist/*.deb
|
||||
# ./deploy/deb/test-deb.sh path/to/btest-rs_*.deb
|
||||
#
|
||||
# Requirements: docker
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
IMAGE="${TEST_IMAGE:-ubuntu:24.04}"
|
||||
|
||||
###############################################################################
|
||||
# Locate the .deb
|
||||
###############################################################################
|
||||
if [[ -n "${1:-}" ]]; then
|
||||
DEB_PATH="$1"
|
||||
else
|
||||
DEB_PATH="$(ls -1t "$REPO_ROOT"/dist/btest-rs_*.deb 2>/dev/null | head -1 || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "$DEB_PATH" || ! -f "$DEB_PATH" ]]; then
|
||||
echo "Error: no .deb file found."
|
||||
echo " Build first: ./deploy/deb/build-deb.sh"
|
||||
echo " Or pass path: $0 path/to/btest-rs_*.deb"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEB_FILE="$(basename "$DEB_PATH")"
|
||||
DEB_DIR="$(cd "$(dirname "$DEB_PATH")" && pwd)"
|
||||
|
||||
echo "==> Testing $DEB_FILE in $IMAGE"
|
||||
echo ""
|
||||
|
||||
###############################################################################
|
||||
# Run tests inside a disposable container
|
||||
###############################################################################
|
||||
docker run --rm \
|
||||
-v "$DEB_DIR/$DEB_FILE:/tmp/$DEB_FILE:ro" \
|
||||
"$IMAGE" \
|
||||
bash -euxc "
|
||||
###################################################################
|
||||
# 1. Install the .deb
|
||||
###################################################################
|
||||
apt-get update -qq
|
||||
dpkg -i /tmp/$DEB_FILE || apt-get install -f -y # resolve deps if any
|
||||
|
||||
###################################################################
|
||||
# 2. Verify files are in place
|
||||
###################################################################
|
||||
echo '--- Checking installed files ---'
|
||||
test -x /usr/bin/btest
|
||||
test -f /usr/lib/systemd/system/btest.service
|
||||
test -f /usr/share/doc/btest-rs/README.md
|
||||
test -f /usr/share/licenses/btest-rs/LICENSE
|
||||
|
||||
# Man page (may be gzipped)
|
||||
test -f /usr/share/man/man1/btest.1.gz || test -f /usr/share/man/man1/btest.1
|
||||
echo 'All expected files present.'
|
||||
|
||||
###################################################################
|
||||
# 3. btest --version
|
||||
###################################################################
|
||||
echo ''
|
||||
echo '--- btest --version ---'
|
||||
btest --version
|
||||
|
||||
###################################################################
|
||||
# 4. Quick loopback server+client test
|
||||
###################################################################
|
||||
echo ''
|
||||
echo '--- Loopback smoke test ---'
|
||||
|
||||
# Start server in background
|
||||
btest -s &
|
||||
SERVER_PID=\$!
|
||||
sleep 1
|
||||
|
||||
# Run a short TCP test against localhost
|
||||
if btest -c 127.0.0.1 -d 2 2>&1; then
|
||||
echo 'Loopback TCP test passed.'
|
||||
else
|
||||
echo 'Warning: loopback test returned non-zero (may be expected in container).'
|
||||
fi
|
||||
|
||||
# Tear down
|
||||
kill \$SERVER_PID 2>/dev/null || true
|
||||
wait \$SERVER_PID 2>/dev/null || true
|
||||
|
||||
###################################################################
|
||||
# 5. Package metadata sanity
|
||||
###################################################################
|
||||
echo ''
|
||||
echo '--- dpkg metadata ---'
|
||||
dpkg -s btest-rs | head -20
|
||||
|
||||
echo ''
|
||||
echo '=== All tests passed ==='
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "==> .deb smoke test completed successfully."
|
||||
73
deploy/rpm/btest-rs.spec
Normal file
73
deploy/rpm/btest-rs.spec
Normal file
@@ -0,0 +1,73 @@
|
||||
Name: btest-rs
|
||||
Version: 0.6.0
|
||||
Release: 1%{?dist}
|
||||
Summary: MikroTik Bandwidth Test (btest) server and client with EC-SRP5 auth
|
||||
|
||||
License: MIT AND Apache-2.0
|
||||
URL: https://github.com/manawenuz/btest-rs
|
||||
Source0: https://github.com/manawenuz/btest-rs/archive/refs/tags/v%{version}.tar.gz
|
||||
|
||||
BuildRequires: cargo
|
||||
BuildRequires: rust
|
||||
ExclusiveArch: x86_64 aarch64
|
||||
|
||||
%description
|
||||
A Rust reimplementation of the MikroTik Bandwidth Test (btest) protocol,
|
||||
providing both server and client functionality with EC-SRP5 authentication.
|
||||
|
||||
%prep
|
||||
%autosetup -n %{name}-%{version}
|
||||
|
||||
%build
|
||||
export CARGO_TARGET_DIR=target
|
||||
cargo build --release
|
||||
|
||||
%install
|
||||
install -Dm755 target/release/btest %{buildroot}%{_bindir}/btest
|
||||
install -Dm644 docs/man/btest.1 %{buildroot}%{_mandir}/man1/btest.1
|
||||
install -Dm644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
|
||||
|
||||
# systemd service unit
|
||||
install -d %{buildroot}%{_unitdir}
|
||||
cat > %{buildroot}%{_unitdir}/btest.service << 'EOF'
|
||||
[Unit]
|
||||
Description=MikroTik Bandwidth Test Server (btest-rs)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/btest -s
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
DynamicUser=yes
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%{_bindir}/btest
|
||||
%{_mandir}/man1/btest.1*
|
||||
%{_unitdir}/btest.service
|
||||
|
||||
%post
|
||||
%systemd_post btest.service
|
||||
|
||||
%preun
|
||||
%systemd_preun btest.service
|
||||
|
||||
%postun
|
||||
%systemd_postun_with_restart btest.service
|
||||
|
||||
%changelog
|
||||
* Mon Mar 30 2026 Siavash Sameni <manwe@manko.yoga> - 0.6.0-1
|
||||
- Initial RPM package
|
||||
30
deploy/rpm/build-rpm.sh
Executable file
30
deploy/rpm/build-rpm.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-rpm.sh — Build the btest-rs RPM package
|
||||
set -euo pipefail
|
||||
|
||||
SPEC_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SPEC_FILE="${SPEC_DIR}/btest-rs.spec"
|
||||
VERSION="0.6.0"
|
||||
TARBALL="v${VERSION}.tar.gz"
|
||||
SOURCE_URL="https://github.com/manawenuz/btest-rs/archive/refs/tags/${TARBALL}"
|
||||
|
||||
echo "==> Setting up rpmbuild tree"
|
||||
mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||
|
||||
echo "==> Downloading source tarball"
|
||||
if [ ! -f ~/rpmbuild/SOURCES/"${TARBALL}" ]; then
|
||||
curl -fSL -o ~/rpmbuild/SOURCES/"${TARBALL}" "${SOURCE_URL}"
|
||||
else
|
||||
echo " (already present, skipping download)"
|
||||
fi
|
||||
|
||||
echo "==> Copying spec file"
|
||||
cp "${SPEC_FILE}" ~/rpmbuild/SPECS/btest-rs.spec
|
||||
|
||||
echo "==> Building RPM"
|
||||
rpmbuild -ba ~/rpmbuild/SPECS/btest-rs.spec
|
||||
|
||||
echo ""
|
||||
echo "==> Build complete. Packages:"
|
||||
find ~/rpmbuild/RPMS -name '*.rpm' -print
|
||||
find ~/rpmbuild/SRPMS -name '*.rpm' -print
|
||||
75
deploy/rpm/test-rpm.sh
Executable file
75
deploy/rpm/test-rpm.sh
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
# test-rpm.sh — Test the btest-rs RPM build inside a Fedora container
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
IMAGE="fedora:latest"
|
||||
|
||||
echo "==> Testing RPM build in ${IMAGE}"
|
||||
docker run --rm \
|
||||
-v "${REPO_ROOT}:/workspace:ro" \
|
||||
"${IMAGE}" \
|
||||
bash -euxc '
|
||||
# ── Install build dependencies ──
|
||||
dnf install -y rpm-build rpmdevtools curl gcc make \
|
||||
systemd-rpm-macros
|
||||
|
||||
# Install Rust toolchain
|
||||
curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||
| sh -s -- -y --profile minimal
|
||||
source "$HOME/.cargo/env"
|
||||
|
||||
# ── Set up rpmbuild tree ──
|
||||
rpmdev-setuptree
|
||||
|
||||
VERSION="0.6.0"
|
||||
TARBALL="v${VERSION}.tar.gz"
|
||||
|
||||
# Copy spec
|
||||
cp /workspace/deploy/rpm/btest-rs.spec ~/rpmbuild/SPECS/
|
||||
|
||||
# Create source tarball from workspace
|
||||
# rpmbuild expects btest-rs-VERSION/ top-level directory
|
||||
mkdir -p /tmp/btest-rs-${VERSION}
|
||||
cp -a /workspace/. /tmp/btest-rs-${VERSION}/
|
||||
tar czf ~/rpmbuild/SOURCES/${TARBALL} -C /tmp btest-rs-${VERSION}
|
||||
|
||||
# ── Build RPM ──
|
||||
rpmbuild -ba ~/rpmbuild/SPECS/btest-rs.spec
|
||||
|
||||
# ── Install the RPM ──
|
||||
RPM=$(find ~/rpmbuild/RPMS -name "btest-rs-*.rpm" | head -1)
|
||||
echo "Installing: ${RPM}"
|
||||
dnf install -y "${RPM}"
|
||||
|
||||
# ── Verify installation ──
|
||||
echo "--- btest --version ---"
|
||||
btest --version
|
||||
|
||||
echo "--- Checking systemd unit ---"
|
||||
systemctl cat btest.service || true
|
||||
|
||||
echo "--- Checking man page ---"
|
||||
test -f /usr/share/man/man1/btest.1* && echo "man page OK" || echo "man page MISSING"
|
||||
|
||||
echo "--- Checking license ---"
|
||||
test -f /usr/share/licenses/btest-rs/LICENSE && echo "license OK" || echo "license MISSING"
|
||||
|
||||
# ── Loopback bandwidth test ──
|
||||
echo "--- Starting loopback test ---"
|
||||
btest -s &
|
||||
SERVER_PID=$!
|
||||
sleep 2
|
||||
|
||||
btest -c 127.0.0.1 --duration 3 && echo "Loopback test PASSED" \
|
||||
|| echo "Loopback test FAILED (exit $?)"
|
||||
|
||||
kill "${SERVER_PID}" 2>/dev/null || true
|
||||
wait "${SERVER_PID}" 2>/dev/null || true
|
||||
|
||||
echo "==> All RPM tests completed."
|
||||
'
|
||||
|
||||
echo "==> Fedora container test finished."
|
||||
@@ -80,6 +80,34 @@ pub fn calc_send_interval(tx_speed_bps: u32, tx_size: u16) -> Option<Duration> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance `next_send` by one interval and clamp drift.
|
||||
///
|
||||
/// When the sender falls behind (e.g., the write blocked longer than the
|
||||
/// inter-packet interval), `next_send` accumulates a debt. Once the path
|
||||
/// clears, the loop would fire packets with *no* delay until the debt is
|
||||
/// repaid, producing a burst that overshoots the target rate.
|
||||
///
|
||||
/// This helper resets `next_send` to `now` whenever it has drifted more
|
||||
/// than 2x the interval behind the current wall-clock time, bounding the
|
||||
/// maximum burst to at most one extra interval's worth of packets.
|
||||
pub fn advance_next_send(
|
||||
next_send: &mut std::time::Instant,
|
||||
iv: Duration,
|
||||
now: std::time::Instant,
|
||||
) -> Option<Duration> {
|
||||
*next_send += iv;
|
||||
// If we have fallen more than 2x the interval behind, reset to now
|
||||
// to prevent a compensating burst.
|
||||
if *next_send + iv < now {
|
||||
*next_send = now;
|
||||
}
|
||||
if *next_send > now {
|
||||
Some(*next_send - now)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a bandwidth value in human-readable form.
|
||||
pub fn format_bandwidth(bits_per_sec: f64) -> String {
|
||||
if bits_per_sec >= 1_000_000_000.0 {
|
||||
|
||||
@@ -167,10 +167,9 @@ async fn tcp_client_tx_loop(
|
||||
|
||||
match interval {
|
||||
Some(iv) => {
|
||||
next_send += iv;
|
||||
let now = Instant::now();
|
||||
if next_send > now {
|
||||
tokio::time::sleep(next_send - now).await;
|
||||
if let Some(delay) = bandwidth::advance_next_send(&mut next_send, iv, now) {
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
@@ -317,10 +316,9 @@ async fn udp_client_tx_loop(
|
||||
|
||||
match interval {
|
||||
Some(iv) => {
|
||||
next_send += iv;
|
||||
let now = Instant::now();
|
||||
if next_send > now {
|
||||
tokio::time::sleep(next_send - now).await;
|
||||
if let Some(delay) = bandwidth::advance_next_send(&mut next_send, iv, now) {
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
||||
86
src/cpu.rs
86
src/cpu.rs
@@ -1,7 +1,7 @@
|
||||
//! Lightweight CPU usage measurement.
|
||||
//!
|
||||
//! Returns the system-wide CPU usage as a percentage (0-100).
|
||||
//! Works on macOS and Linux without external dependencies.
|
||||
//! Works on macOS, Linux, Windows, and FreeBSD without external dependencies.
|
||||
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::time::Duration;
|
||||
@@ -93,7 +93,82 @@ fn get_cpu_times() -> (u64, u64) {
|
||||
(0, 0)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_cpu_times() -> (u64, u64) {
|
||||
#[repr(C)]
|
||||
#[derive(Default)]
|
||||
struct FILETIME {
|
||||
dwLowDateTime: u32,
|
||||
dwHighDateTime: u32,
|
||||
}
|
||||
|
||||
impl FILETIME {
|
||||
fn to_u64(&self) -> u64 {
|
||||
(self.dwHighDateTime as u64) << 32 | self.dwLowDateTime as u64
|
||||
}
|
||||
}
|
||||
|
||||
extern "system" {
|
||||
fn GetSystemTimes(
|
||||
lpIdleTime: *mut FILETIME,
|
||||
lpKernelTime: *mut FILETIME,
|
||||
lpUserTime: *mut FILETIME,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
let mut idle = FILETIME::default();
|
||||
let mut kernel = FILETIME::default();
|
||||
let mut user = FILETIME::default();
|
||||
|
||||
// SAFETY: We pass valid pointers to stack-allocated FILETIME structs.
|
||||
// GetSystemTimes is a well-documented Win32 API that writes into these
|
||||
// output parameters. A non-zero return value indicates success.
|
||||
let ret = unsafe { GetSystemTimes(&mut idle, &mut kernel, &mut user) };
|
||||
|
||||
if ret != 0 {
|
||||
let idle_ticks = idle.to_u64();
|
||||
// Kernel time includes idle time on Windows, so total = kernel + user.
|
||||
let total_ticks = kernel.to_u64() + user.to_u64();
|
||||
(total_ticks, idle_ticks)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "freebsd")]
|
||||
fn get_cpu_times() -> (u64, u64) {
|
||||
// kern.cp_time returns: user nice system interrupt idle
|
||||
if let Ok(output) = std::process::Command::new("sysctl")
|
||||
.arg("-n")
|
||||
.arg("kern.cp_time")
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
let parts: Vec<u64> = text
|
||||
.split_whitespace()
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
if parts.len() >= 5 {
|
||||
let user = parts[0];
|
||||
let nice = parts[1];
|
||||
let system = parts[2];
|
||||
let interrupt = parts[3];
|
||||
let idle = parts[4];
|
||||
let total = user + nice + system + interrupt + idle;
|
||||
return (total, idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
(0, 0)
|
||||
}
|
||||
|
||||
#[cfg(not(any(
|
||||
target_os = "linux",
|
||||
target_os = "macos",
|
||||
target_os = "windows",
|
||||
target_os = "freebsd",
|
||||
)))]
|
||||
fn get_cpu_times() -> (u64, u64) {
|
||||
(0, 0) // Unsupported platform
|
||||
}
|
||||
@@ -116,7 +191,12 @@ mod tests {
|
||||
fn test_cpu_times_returns_nonzero() {
|
||||
let (total, idle) = get_cpu_times();
|
||||
// On supported platforms, total should be > 0
|
||||
if cfg!(any(target_os = "linux", target_os = "macos")) {
|
||||
if cfg!(any(
|
||||
target_os = "linux",
|
||||
target_os = "macos",
|
||||
target_os = "windows",
|
||||
target_os = "freebsd",
|
||||
)) {
|
||||
assert!(total > 0, "CPU total ticks should be > 0");
|
||||
assert!(idle <= total, "idle should be <= total");
|
||||
}
|
||||
|
||||
@@ -565,10 +565,9 @@ async fn tcp_tx_loop_inner(
|
||||
|
||||
match interval {
|
||||
Some(iv) => {
|
||||
next_send += iv;
|
||||
let now = Instant::now();
|
||||
if next_send > now {
|
||||
tokio::time::sleep(next_send - now).await;
|
||||
if let Some(delay) = bandwidth::advance_next_send(&mut next_send, iv, now) {
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
@@ -805,10 +804,9 @@ async fn udp_tx_loop(
|
||||
|
||||
match interval {
|
||||
Some(iv) => {
|
||||
next_send += iv;
|
||||
let now = Instant::now();
|
||||
if next_send > now {
|
||||
tokio::time::sleep(next_send - now).await;
|
||||
if let Some(delay) = bandwidth::advance_next_send(&mut next_send, iv, now) {
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
||||
Reference in New Issue
Block a user