Merge pull request #3 from 0xddy/codex/jni-boundary-cleanup-20260730
fix: harden JNI boundary and remove legacy example
This commit is contained in:
@@ -12,38 +12,6 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Java fat JAR(平台无关,只需构建一次)
|
||||
# 运行方式:java -Djava.library.path=<so/dll 目录> -jar dht-crawler-jni-example-<ver>.jar
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
build-java-jar:
|
||||
name: Build Java Example JAR
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
|
||||
- name: Build fat JAR
|
||||
working-directory: examples-jni
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew shadowJar -PprojectVersion=${{ github.event.inputs.version }} --no-daemon
|
||||
shell: bash
|
||||
|
||||
- name: Upload JAR to Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.version }}
|
||||
files: examples-jni/build/libs/dht-crawler-jni-example-${{ github.event.inputs.version }}.jar
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# 各平台 Rust 构建(example 可执行 + JNI so/dll/dylib)
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -59,6 +59,7 @@ jobs:
|
||||
- "mimalloc"
|
||||
- "metrics"
|
||||
- "mimalloc,metrics"
|
||||
- "jni"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
@@ -88,4 +89,4 @@ jobs:
|
||||
cargo build --examples --verbose
|
||||
else
|
||||
cargo build --examples --verbose --features "${{ matrix.features }}"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -18,7 +18,3 @@ torrents/
|
||||
|
||||
# 个人脚本(不提交到仓库)
|
||||
scripts/
|
||||
|
||||
# Java / Gradle(examples-jni)
|
||||
examples-jni/build/
|
||||
examples-jni/.gradle/
|
||||
|
||||
@@ -244,7 +244,8 @@ dht-crawler = { version = "0.2", features = ["metrics"] }
|
||||
| `jni` | 构建 Java JNI 接口和 `cdylib` |
|
||||
| `mimalloc` | 将 mimalloc 注册为全局分配器 |
|
||||
|
||||
JNI 的构建方式和 Java 示例见 [examples-jni/README.md](examples-jni/README.md)。
|
||||
Java/Kotlin 封装、平台 native JAR 和示例由独立项目
|
||||
[`dht-crawler-java`](https://github.com/0xddy/dht-crawler-java) 维护。
|
||||
启用 `mimalloc` 前,请确认最终二进制没有注册其他全局分配器。
|
||||
|
||||
## 开发
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
# dht-crawler JNI Java 示例
|
||||
|
||||
Gradle 项目,演示通过 JNI 调用 `dht-crawler`(需启用 Cargo feature `jni`)。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
examples-jni/
|
||||
├── build.gradle
|
||||
├── settings.gradle
|
||||
└── src/main/java/cn/lmcw/dht/
|
||||
├── model/ # DHTOptions, TorrentInfo, FileInfo
|
||||
├── DhtCrawler.java # 面向对象入口(推荐)
|
||||
├── DhtCrawlerJni.java # native 方法声明
|
||||
├── DhtListener.java # 回调接口
|
||||
└── DhtCrawlerExample.java
|
||||
```
|
||||
|
||||
Rust JNI 实现位于 **`dht-crawler/jni/`**(`lib` crate-type 含 `cdylib`)。
|
||||
|
||||
## 编译 native 库
|
||||
|
||||
在 **`dht-crawler`** 目录(本 README 的上一级)执行:
|
||||
|
||||
```bash
|
||||
cargo build --release --features jni
|
||||
```
|
||||
|
||||
产物路径(因平台而异):
|
||||
|
||||
- Linux: `target/release/libdht_crawler.so`
|
||||
- Windows: `target/release/dht_crawler.dll`
|
||||
- macOS: `target/release/libdht_crawler.dylib`
|
||||
|
||||
## 运行示例
|
||||
|
||||
### 方式一:Release 预编译包(推荐)
|
||||
|
||||
下载 Release 中的 fat JAR 与对应平台 native 库 zip,放在同一目录:
|
||||
|
||||
```bash
|
||||
# Linux / macOS
|
||||
java -Djava.library.path=. -jar dht-crawler-jni-example-<version>.jar
|
||||
|
||||
# Windows
|
||||
java "-Djava.library.path=." -jar dht-crawler-jni-example-<version>.jar
|
||||
```
|
||||
|
||||
### 方式二:源码 + Gradle
|
||||
|
||||
```bash
|
||||
cd examples-jni
|
||||
gradle run
|
||||
# 指定 native 库目录(默认为 ../target/release)
|
||||
gradle run -Plib.path=/path/to/lib
|
||||
```
|
||||
|
||||
构建 fat JAR:
|
||||
|
||||
```bash
|
||||
gradle shadowJar
|
||||
```
|
||||
|
||||
## 集成到自己的 Java 项目
|
||||
|
||||
1. 复制 `cn/lmcw/dht/` 包(含 `model/`、`DhtCrawler`、`DhtCrawlerJni`、`DhtListener`)。
|
||||
2. 将对应平台的 native 库加入 `java.library.path`。
|
||||
3. 使用与示例相同的 `dht-crawler` JNI 版本构建 `cdylib`。
|
||||
|
||||
## API(面向对象)
|
||||
|
||||
```java
|
||||
DhtCrawler crawler = DhtCrawler.createServer(options, listener);
|
||||
crawler.start(); // 后台启动,不阻塞调用线程
|
||||
// ...
|
||||
crawler.stop(); // 或 try-with-resources
|
||||
```
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createServer(options, listener)` | 创建 `ServerHandle`(含独立 tokio `Runtime` + `DHTServer`) |
|
||||
| `start()` | 在 runtime 内 spawn `server.start()`;同一会话多次调用仅首次生效 |
|
||||
| `stop()` / `close()` | `shutdown()` 后在后台线程 drop Runtime,避免阻塞 JNI 线程 |
|
||||
| `getNodePoolSize()` | 节点池大小(`DHTServer::get_node_pool_size`) |
|
||||
|
||||
Native 导出类:`cn.lmcw.dht.DhtCrawlerJni`(`createServer` / `startServer` / `stopServer` / `getNodePoolSize`)。
|
||||
|
||||
## 回调与线程
|
||||
|
||||
- `onTorrent` / `onError`:在 Rust 工作线程触发,Java 实现须线程安全。
|
||||
- `onMetadataFetch`:在阻塞线程池中调用,应尽快返回 boolean。
|
||||
|
||||
行为与 Rust 库一致:InfoHash 来自 **`announce_peer`**;`start()` 在 Rust 侧仍阻塞至 `shutdown()`,JNI 通过单独 runtime + `spawn` 避免卡住 Java 主流程。
|
||||
|
||||
当前 JNI listener 暴露 `onTorrent`、`onMetadataFetch` 和 `onError`,不暴露 Rust
|
||||
`on_torrent_with_ack` / `on_metadata_fetch_complete`。因此 Java `onTorrent` 返回后始终按
|
||||
`Accepted` 处理;需要交付确认和最终状态的应用应扩展 JNI callback contract。
|
||||
|
||||
## JNI 配置映射
|
||||
|
||||
Java `DHTOptions` 是 Rust 配置的扁平化子集,不是全部 Rust 字段的一一镜像:
|
||||
|
||||
| Java 字段组 | Rust 目标 |
|
||||
|---|---|
|
||||
| port / netMode / hashQueueCapacity | `DHTOptions` 顶层 |
|
||||
| metadataTimeout / maxMetadataQueueSize / maxMetadataWorkerCount | `metadata.*` |
|
||||
| poolCapacity / recentProbeTtlSeconds / responsive* / poolLowWatermark | `crawl.pool.*` |
|
||||
| findNode* / requestTimeout* / response* / pressure / replacements / subnet | `crawl.rate_limit.*` |
|
||||
|
||||
以下配置未通过当前 JNI 暴露,使用 Rust `Default`:
|
||||
|
||||
- `metadata.peer_failure_cache_capacity`、`metadata.peer_failure_ttl_secs`
|
||||
- `crawl.bootstrap.*`
|
||||
- `crawl.target.*`
|
||||
- `crawl.scheduler.*`
|
||||
|
||||
Java 字段默认值与当前 Rust 0.2 默认值保持一致;传入 `null` options 时直接使用完整的
|
||||
`DHTOptions::default()`。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- Java 11+(见 `build.gradle`)。
|
||||
- Java/Rust 版本必须一致,避免 JNI 按字段名和签名读取时失败。
|
||||
- 停止后勿再使用同一 `long` 句柄。
|
||||
@@ -1,39 +0,0 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'application'
|
||||
id 'com.gradleup.shadow' version '9.0.2'
|
||||
}
|
||||
|
||||
group = 'cn.lmcw.dht'
|
||||
version = project.findProperty('projectVersion') ?: '0.1.0'
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = 'cn.lmcw.dht.DhtCrawlerExample'
|
||||
def libPath = project.findProperty('lib.path') ?: '.'
|
||||
applicationDefaultJvmArgs = ["-Djava.library.path=${libPath}"]
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
// 运行时将 native 库路径传入 JVM(默认指向仓库根 target/release)
|
||||
tasks.withType(JavaExec).configureEach {
|
||||
def libPath = project.findProperty('lib.path') ?: '../target/release'
|
||||
jvmArgs "-Djava.library.path=${libPath}"
|
||||
}
|
||||
|
||||
// Fat JAR(含所有依赖 + Main-Class):执行 `gradle shadowJar` 生成
|
||||
shadowJar {
|
||||
archiveBaseName = 'dht-crawler-jni-example'
|
||||
archiveClassifier = ''
|
||||
archiveVersion = version
|
||||
manifest {
|
||||
attributes 'Main-Class': 'cn.lmcw.dht.DhtCrawlerExample'
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -1,7 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
Vendored
-248
@@ -1,248 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
-84
@@ -1,84 +0,0 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem having the script exit with the same return code as the Gradle build
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -1 +0,0 @@
|
||||
rootProject.name = 'dht-crawler-jni'
|
||||
@@ -1,100 +0,0 @@
|
||||
package cn.lmcw.dht;
|
||||
|
||||
import cn.lmcw.dht.model.DHTOptions;
|
||||
|
||||
/**
|
||||
* DHT 爬虫会话:封装 native 句柄与生命周期,推荐使用的面向对象入口。
|
||||
*
|
||||
* <pre>{@code
|
||||
* try (DhtCrawler crawler = DhtCrawler.createServer(options, listener)) {
|
||||
* crawler.start();
|
||||
* // ... 运行 ...
|
||||
* } // close → stop,释放 Rust 资源
|
||||
*
|
||||
* // 或手动:
|
||||
* DhtCrawler c = DhtCrawler.createServer(options, listener);
|
||||
* c.start();
|
||||
* // ...
|
||||
* c.stop();
|
||||
* }</pre>
|
||||
*/
|
||||
public final class DhtCrawler implements AutoCloseable {
|
||||
|
||||
private long handle;
|
||||
private volatile boolean started;
|
||||
|
||||
private DhtCrawler(long handle) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 DHT 服务器会话(尚未 {@link #start()},与 native createServer 对应)。
|
||||
* Java 中方法不能命名为 {@code new},故用 {@code createServer}。
|
||||
*
|
||||
* @param options 配置,{@code null} 使用 Rust 默认
|
||||
* @param listener 回调,{@code null} 不注册回调
|
||||
* @return 已绑定 native 资源的会话
|
||||
* @throws IllegalStateException 创建失败(句柄为 0,通常伴随 JVM 异常)
|
||||
*/
|
||||
public static DhtCrawler createServer(DHTOptions options, DhtListener listener) {
|
||||
long h = DhtCrawlerJni.createServer(options, listener);
|
||||
if (h == 0) {
|
||||
throw new IllegalStateException("DHT 服务器创建失败(createServer 返回 0)");
|
||||
}
|
||||
return new DhtCrawler(h);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在后台启动 DHT(非阻塞)。可多次调用,仅首次生效。
|
||||
*
|
||||
* @throws IllegalStateException 已 {@link #stop()} 关闭后不能再启动
|
||||
*/
|
||||
public synchronized void start() {
|
||||
if (handle == 0) {
|
||||
throw new IllegalStateException("会话已关闭,无法 start");
|
||||
}
|
||||
if (started) {
|
||||
return;
|
||||
}
|
||||
DhtCrawlerJni.startServer(handle);
|
||||
started = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止并释放全部 Rust 资源(tokio runtime 等)。幂等:重复调用安全。
|
||||
*/
|
||||
public synchronized void stop() {
|
||||
if (handle == 0) {
|
||||
return;
|
||||
}
|
||||
DhtCrawlerJni.stopServer(handle);
|
||||
handle = 0;
|
||||
started = false;
|
||||
}
|
||||
|
||||
/** 是否已调用过 {@link #start()} 且尚未 {@link #stop()}。 */
|
||||
public synchronized boolean isStarted() {
|
||||
return started && handle != 0;
|
||||
}
|
||||
|
||||
/** 会话仍有效(未 stop)时为 true。 */
|
||||
public synchronized boolean isOpen() {
|
||||
return handle != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前 routing table 节点数;已关闭时返回 0。
|
||||
*/
|
||||
public int getNodePoolSize() {
|
||||
long h = handle;
|
||||
if (h == 0) {
|
||||
return 0;
|
||||
}
|
||||
return DhtCrawlerJni.getNodePoolSize(h);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package cn.lmcw.dht;
|
||||
|
||||
import cn.lmcw.dht.model.DHTOptions;
|
||||
import cn.lmcw.dht.model.TorrentInfo;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* DHT-Crawler JNI 使用示例。
|
||||
*
|
||||
* <h3>运行前准备</h3>
|
||||
* <ol>
|
||||
* <li>在仓库根目录编译 Rust JNI 产物:
|
||||
* <pre>cargo build --release --features jni</pre>
|
||||
* </li>
|
||||
* <li>在 {@code examples-jni/} 目录下运行({@code lib.path} 指向 so/dll/dylib 所在目录):
|
||||
* <pre>gradle run -Plib.path=../target/release</pre>
|
||||
* 或直接用默认路径({@code ../target/release}):
|
||||
* <pre>gradle run</pre>
|
||||
* </li>
|
||||
* </ol>
|
||||
*/
|
||||
public class DhtCrawlerExample {
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
DHTOptions options = new DHTOptions()
|
||||
.setPort(6881)
|
||||
.setNetMode(0)
|
||||
.setMetadataTimeout(5L)
|
||||
.setMaxMetadataQueueSize(50_000)
|
||||
.setMaxMetadataWorkerCount(500);
|
||||
|
||||
AtomicLong torrentCount = new AtomicLong();
|
||||
CountDownLatch shutdown = new CountDownLatch(1);
|
||||
|
||||
DhtListener listener = new DhtListener() {
|
||||
@Override
|
||||
public void onTorrent(TorrentInfo info) {
|
||||
long n = torrentCount.incrementAndGet();
|
||||
System.out.printf("[#%d] %s (%s) files=%d%n",
|
||||
n, info.getName(), info.getInfoHash(),
|
||||
info.getFiles() != null ? info.getFiles().size() : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String message) {
|
||||
System.err.println("[ERROR] " + message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMetadataFetch(String infoHash) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
final DhtCrawler crawler = DhtCrawler.createServer(options, listener);
|
||||
crawler.start();
|
||||
System.out.println("DHT 已启动,端口 " + options.getPort());
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
System.out.println("\n收到退出信号,正在停止...");
|
||||
crawler.stop();
|
||||
System.out.println("已关闭,共发现种子:" + torrentCount.get());
|
||||
shutdown.countDown();
|
||||
}));
|
||||
|
||||
System.out.println("按 Ctrl+C 退出。每 10 秒打印一次节点池大小...");
|
||||
while (true) {
|
||||
Thread.sleep(10_000);
|
||||
System.out.println("节点池: " + crawler.getNodePoolSize()
|
||||
+ " 已发现种子: " + torrentCount.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package cn.lmcw.dht;
|
||||
|
||||
import cn.lmcw.dht.model.DHTOptions;
|
||||
|
||||
/**
|
||||
* 包内 JNI 绑定(由 {@link DhtCrawler} 使用)。业务代码请用 {@link DhtCrawler}。
|
||||
*/
|
||||
final class DhtCrawlerJni {
|
||||
|
||||
static {
|
||||
System.loadLibrary("dht_crawler");
|
||||
}
|
||||
|
||||
private DhtCrawlerJni() {}
|
||||
|
||||
static native long createServer(DHTOptions options, DhtListener listener);
|
||||
|
||||
static native void startServer(long handle);
|
||||
|
||||
static native void stopServer(long handle);
|
||||
|
||||
static native int getNodePoolSize(long handle);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package cn.lmcw.dht;
|
||||
|
||||
import cn.lmcw.dht.model.TorrentInfo;
|
||||
|
||||
/**
|
||||
* DHT 爬虫事件回调接口。
|
||||
* <p>实现此接口并传入 {@link DhtCrawlerJni#createServer} 即可接收事件通知。
|
||||
* 回调在 Rust 内部的 tokio 工作线程触发,实现方需自行保证线程安全。</p>
|
||||
*/
|
||||
public interface DhtListener {
|
||||
|
||||
/**
|
||||
* 成功获取到 torrent metadata 时触发。
|
||||
*
|
||||
* @param info 完整的 torrent 信息对象,由 Rust JNI 层映射构造
|
||||
*/
|
||||
void onTorrent(TorrentInfo info);
|
||||
|
||||
/**
|
||||
* 内部发生错误时触发。
|
||||
*
|
||||
* @param message Rust 侧错误的文本描述
|
||||
*/
|
||||
void onError(String message);
|
||||
|
||||
/**
|
||||
* 在即将对某 info_hash 拉取 BitTorrent metadata 之前调用。
|
||||
* 与 Rust {@code DHTServer::on_metadata_fetch} 对应:返回 {@code true} 才会真正去拉取;
|
||||
* 返回 {@code false} 则跳过该 hash,不会触发 {@link #onTorrent}。
|
||||
* <p>在 Rust 的 metadata 工作线程中通过阻塞线程池调用,应快速返回,避免长时间阻塞。</p>
|
||||
*
|
||||
* @param infoHash 40 字符小写十六进制 info_hash
|
||||
* @return 是否允许拉取 metadata
|
||||
*/
|
||||
default boolean onMetadataFetch(String infoHash) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package cn.lmcw.dht.model;
|
||||
|
||||
/**
|
||||
* Rust {@code DHTOptions} 的 JNI 扁平化子集。
|
||||
* <p>通过 JNI 传入 Rust 侧;未暴露的 Metadata failure cache、Bootstrap、Target 和
|
||||
* Scheduler 字段采用 Rust 0.2 默认值。Java 与 native 库版本必须保持一致。</p>
|
||||
*
|
||||
* <h3>netMode 取值</h3>
|
||||
* <ul>
|
||||
* <li>0 - IPv4 Only</li>
|
||||
* <li>1 - IPv6 Only</li>
|
||||
* <li>2 - Dual Stack(默认)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class DHTOptions {
|
||||
|
||||
/** 监听端口,默认 6881 */
|
||||
private int port = 6881;
|
||||
|
||||
/** 获取 metadata 超时(秒),默认 3 */
|
||||
private long metadataTimeout = 4L;
|
||||
|
||||
/** metadata 队列最大容量,默认 100000 */
|
||||
private int maxMetadataQueueSize = 10_000;
|
||||
|
||||
/** 并发 metadata 拉取 worker 数量,默认 1000 */
|
||||
private int maxMetadataWorkerCount = 256;
|
||||
|
||||
/** 节点队列容量,默认 100000 */
|
||||
private int poolCapacity = 100_000;
|
||||
|
||||
private int findNodeRatePerSecond = 200;
|
||||
|
||||
private int findNodeBurst = 40;
|
||||
|
||||
private int maxFindNodeInFlight = 512;
|
||||
|
||||
private int maxNewDestinationsPerMinute = 10_000;
|
||||
|
||||
private int maxReplacementsPerMinute = 25_000;
|
||||
|
||||
private long requestTimeoutSeconds = 2L;
|
||||
private int maxResponseRatePerSecond = 500;
|
||||
private long maxResponseBytesPerSecond = 1_048_576L;
|
||||
private int maxResponseRatePerSource = 40;
|
||||
private int metadataPressureFloorPercent = 25;
|
||||
private long recentProbeTtlSeconds = 600L;
|
||||
private int responsiveCapacity = 16_384;
|
||||
private long responsiveTtlSeconds = 900L;
|
||||
private int poolLowWatermark = 10_000;
|
||||
private int maxInFlightPerSubnet = 8;
|
||||
|
||||
/** hash 队列容量,默认 10000 */
|
||||
private int hashQueueCapacity = 10_000;
|
||||
|
||||
/**
|
||||
* 网络模式:0=IPv4Only, 1=IPv6Only, 2=DualStack(默认 0 / IPv4Only,
|
||||
* 与 Rust 侧 {@code NetMode::Ipv4Only} 对应)
|
||||
*/
|
||||
private int netMode = 0;
|
||||
|
||||
public DHTOptions() {}
|
||||
|
||||
public int getPort() { return port; }
|
||||
public DHTOptions setPort(int port) { this.port = port; return this; }
|
||||
|
||||
public long getMetadataTimeout() { return metadataTimeout; }
|
||||
public DHTOptions setMetadataTimeout(long metadataTimeout) {
|
||||
this.metadataTimeout = metadataTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getMaxMetadataQueueSize() { return maxMetadataQueueSize; }
|
||||
public DHTOptions setMaxMetadataQueueSize(int maxMetadataQueueSize) {
|
||||
this.maxMetadataQueueSize = maxMetadataQueueSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getMaxMetadataWorkerCount() { return maxMetadataWorkerCount; }
|
||||
public DHTOptions setMaxMetadataWorkerCount(int maxMetadataWorkerCount) {
|
||||
this.maxMetadataWorkerCount = maxMetadataWorkerCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getPoolCapacity() { return poolCapacity; }
|
||||
public DHTOptions setPoolCapacity(int poolCapacity) {
|
||||
this.poolCapacity = poolCapacity;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getFindNodeRatePerSecond() { return findNodeRatePerSecond; }
|
||||
public DHTOptions setFindNodeRatePerSecond(int value) { this.findNodeRatePerSecond = value; return this; }
|
||||
|
||||
public int getFindNodeBurst() { return findNodeBurst; }
|
||||
public DHTOptions setFindNodeBurst(int value) { this.findNodeBurst = value; return this; }
|
||||
|
||||
public int getMaxFindNodeInFlight() { return maxFindNodeInFlight; }
|
||||
public DHTOptions setMaxFindNodeInFlight(int value) { this.maxFindNodeInFlight = value; return this; }
|
||||
|
||||
public int getMaxNewDestinationsPerMinute() { return maxNewDestinationsPerMinute; }
|
||||
public DHTOptions setMaxNewDestinationsPerMinute(int value) { this.maxNewDestinationsPerMinute = value; return this; }
|
||||
|
||||
public int getMaxReplacementsPerMinute() { return maxReplacementsPerMinute; }
|
||||
public DHTOptions setMaxReplacementsPerMinute(int value) { this.maxReplacementsPerMinute = value; return this; }
|
||||
|
||||
public long getRequestTimeoutSeconds() { return requestTimeoutSeconds; }
|
||||
public DHTOptions setRequestTimeoutSeconds(long value) { this.requestTimeoutSeconds = value; return this; }
|
||||
public int getMaxResponseRatePerSecond() { return maxResponseRatePerSecond; }
|
||||
public DHTOptions setMaxResponseRatePerSecond(int value) { this.maxResponseRatePerSecond = value; return this; }
|
||||
public long getMaxResponseBytesPerSecond() { return maxResponseBytesPerSecond; }
|
||||
public DHTOptions setMaxResponseBytesPerSecond(long value) { this.maxResponseBytesPerSecond = value; return this; }
|
||||
public int getMaxResponseRatePerSource() { return maxResponseRatePerSource; }
|
||||
public DHTOptions setMaxResponseRatePerSource(int value) { this.maxResponseRatePerSource = value; return this; }
|
||||
public int getMetadataPressureFloorPercent() { return metadataPressureFloorPercent; }
|
||||
public DHTOptions setMetadataPressureFloorPercent(int value) { this.metadataPressureFloorPercent = value; return this; }
|
||||
public long getRecentProbeTtlSeconds() { return recentProbeTtlSeconds; }
|
||||
public DHTOptions setRecentProbeTtlSeconds(long value) { this.recentProbeTtlSeconds = value; return this; }
|
||||
public int getResponsiveCapacity() { return responsiveCapacity; }
|
||||
public DHTOptions setResponsiveCapacity(int value) { this.responsiveCapacity = value; return this; }
|
||||
public long getResponsiveTtlSeconds() { return responsiveTtlSeconds; }
|
||||
public DHTOptions setResponsiveTtlSeconds(long value) { this.responsiveTtlSeconds = value; return this; }
|
||||
public int getPoolLowWatermark() { return poolLowWatermark; }
|
||||
public DHTOptions setPoolLowWatermark(int value) { this.poolLowWatermark = value; return this; }
|
||||
public int getMaxInFlightPerSubnet() { return maxInFlightPerSubnet; }
|
||||
public DHTOptions setMaxInFlightPerSubnet(int value) { this.maxInFlightPerSubnet = value; return this; }
|
||||
|
||||
public int getHashQueueCapacity() { return hashQueueCapacity; }
|
||||
public DHTOptions setHashQueueCapacity(int hashQueueCapacity) {
|
||||
this.hashQueueCapacity = hashQueueCapacity;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getNetMode() { return netMode; }
|
||||
public DHTOptions setNetMode(int netMode) { this.netMode = netMode; return this; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DHTOptions{"
|
||||
+ "port=" + port
|
||||
+ ", metadataTimeout=" + metadataTimeout
|
||||
+ ", netMode=" + netMode
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package cn.lmcw.dht.model;
|
||||
|
||||
/**
|
||||
* 与 Rust {@code FileInfo} 一一对应的 POJO。
|
||||
* <p>表示一个种子内单个文件的路径与大小。</p>
|
||||
*/
|
||||
public final class FileInfo {
|
||||
|
||||
private final String path;
|
||||
private final long size;
|
||||
|
||||
/**
|
||||
* 供 JNI 层调用的全参构造器。
|
||||
*
|
||||
* @param path 文件相对路径
|
||||
* @param size 文件字节数
|
||||
*/
|
||||
public FileInfo(String path, long size) {
|
||||
this.path = path;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public String getPath() { return path; }
|
||||
public long getSize() { return size; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FileInfo{path='" + path + "', size=" + size + '}';
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package cn.lmcw.dht.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 与 Rust {@code TorrentInfo} 一一对应的 POJO。
|
||||
* <p>由 Rust JNI 层通过 {@code NewObject} / {@code SetField} 构造并填充,
|
||||
* 通过 {@link cn.lmcw.dht.DhtListener#onTorrent(TorrentInfo)} 回调给 Java 层。</p>
|
||||
*/
|
||||
public final class TorrentInfo {
|
||||
|
||||
private final String infoHash;
|
||||
private final String magnetLink;
|
||||
private final String name;
|
||||
private final long totalSize;
|
||||
private final List<FileInfo> files;
|
||||
private final long pieceLength;
|
||||
private final List<String> peers;
|
||||
private final long timestamp;
|
||||
|
||||
/**
|
||||
* 供 JNI 层调用的全参构造器。
|
||||
*
|
||||
* @param infoHash 十六进制 info-hash 字符串
|
||||
* @param magnetLink magnet 链接
|
||||
* @param name 种子名称
|
||||
* @param totalSize 总字节数
|
||||
* @param files 文件列表
|
||||
* @param pieceLength 分片长度(字节)
|
||||
* @param peers 来源节点地址列表
|
||||
* @param timestamp 发现时间戳(Unix 秒)
|
||||
*/
|
||||
public TorrentInfo(
|
||||
String infoHash,
|
||||
String magnetLink,
|
||||
String name,
|
||||
long totalSize,
|
||||
List<FileInfo> files,
|
||||
long pieceLength,
|
||||
List<String> peers,
|
||||
long timestamp) {
|
||||
this.infoHash = infoHash;
|
||||
this.magnetLink = magnetLink;
|
||||
this.name = name;
|
||||
this.totalSize = totalSize;
|
||||
this.files = files;
|
||||
this.pieceLength = pieceLength;
|
||||
this.peers = peers;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public String getInfoHash() { return infoHash; }
|
||||
public String getMagnetLink() { return magnetLink; }
|
||||
public String getName() { return name; }
|
||||
public long getTotalSize() { return totalSize; }
|
||||
public List<FileInfo> getFiles() { return files; }
|
||||
public long getPieceLength() { return pieceLength; }
|
||||
public List<String> getPeers() { return peers; }
|
||||
public long getTimestamp() { return timestamp; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TorrentInfo{"
|
||||
+ "infoHash='" + infoHash + '\''
|
||||
+ ", name='" + name + '\''
|
||||
+ ", totalSize=" + totalSize
|
||||
+ ", files=" + (files != null ? files.size() : 0)
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -58,6 +58,7 @@ fn register_on_error(server: &Arc<DHTServer>, callback: JavaCallback) {
|
||||
|
||||
/// 注册 on_metadata_fetch:在拉取 metadata 前询问 Java `onMetadataFetch(infoHash)`。
|
||||
/// 返回 `true` 才继续拉取;在阻塞线程池中执行 JNI,避免阻塞 tokio worker。
|
||||
/// JNI 调用或阻塞任务失败时按拒绝处理。
|
||||
fn register_on_metadata_fetch(server: &Arc<DHTServer>, callback: JavaCallback) {
|
||||
server.on_metadata_fetch(move |info_hash| {
|
||||
let cb = callback.clone();
|
||||
@@ -78,12 +79,12 @@ fn register_on_metadata_fetch(server: &Arc<DHTServer>, callback: JavaCallback) {
|
||||
match r {
|
||||
Ok(Ok(allow)) => allow,
|
||||
Ok(Err(e)) => {
|
||||
log::error!("回调 onMetadataFetch 失败: {e},默认允许拉取");
|
||||
true
|
||||
log::error!("回调 onMetadataFetch 失败: {e},拒绝拉取");
|
||||
false
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("onMetadataFetch spawn_blocking 失败: {e},默认允许拉取");
|
||||
true
|
||||
log::error!("onMetadataFetch spawn_blocking 失败: {e},拒绝拉取");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -12,10 +12,10 @@ use jni::sys::{jint, jlong};
|
||||
|
||||
/// 创建 DHTServer 并返回句柄(jlong)。
|
||||
///
|
||||
/// Java 签名:`native long createServer(DHTOptions options, DhtListener listener);`
|
||||
/// JVM 签名:`native long createServer(Object options, Object listener);`
|
||||
///
|
||||
/// - `options`:`cn.lmcw.dht.model.DHTOptions` 对象,或 null 则使用默认选项。
|
||||
/// - `listener`:`cn.lmcw.dht.DhtListener` 实现,或 null 则不注册回调。
|
||||
/// - `options`:包含 native 配置字段的 DTO,或 null 则使用默认选项。
|
||||
/// - `listener`:实现 `onTorrent`、`onError` 和 `onMetadataFetch` 的对象,或 null。
|
||||
/// - 返回:服务器句柄(成功)或 0(失败)。
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_cn_lmcw_dht_DhtCrawlerJni_createServer(
|
||||
|
||||
+249
-50
@@ -3,6 +3,76 @@ use jni::JNIEnv;
|
||||
use jni::objects::{JObject, JString, JValue};
|
||||
use jni::sys::jlong;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DhtOptionsConversionError {
|
||||
#[error(transparent)]
|
||||
Jni(#[from] jni::errors::Error),
|
||||
#[error("invalid DHTOptions.{field}: expected {expected}, got {value}")]
|
||||
InvalidValue {
|
||||
field: &'static str,
|
||||
expected: &'static str,
|
||||
value: i64,
|
||||
},
|
||||
}
|
||||
|
||||
type DhtOptionsResult<T> = Result<T, DhtOptionsConversionError>;
|
||||
|
||||
fn invalid_value(
|
||||
field: &'static str,
|
||||
expected: &'static str,
|
||||
value: impl Into<i64>,
|
||||
) -> DhtOptionsConversionError {
|
||||
DhtOptionsConversionError::InvalidValue {
|
||||
field,
|
||||
expected,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn checked_port(value: i32) -> DhtOptionsResult<u16> {
|
||||
u16::try_from(value).map_err(|_| invalid_value("port", "an integer in 0..=65535", value))
|
||||
}
|
||||
|
||||
fn checked_non_negative_u32(field: &'static str, value: i32) -> DhtOptionsResult<u32> {
|
||||
u32::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value))
|
||||
}
|
||||
|
||||
fn checked_non_negative_u64(field: &'static str, value: i64) -> DhtOptionsResult<u64> {
|
||||
u64::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value))
|
||||
}
|
||||
|
||||
fn checked_non_negative_usize(field: &'static str, value: i32) -> DhtOptionsResult<usize> {
|
||||
usize::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value))
|
||||
}
|
||||
|
||||
fn checked_positive_usize(field: &'static str, value: i32) -> DhtOptionsResult<usize> {
|
||||
let value = checked_non_negative_usize(field, value)?;
|
||||
if value == 0 {
|
||||
return Err(invalid_value(
|
||||
field,
|
||||
"an integer greater than or equal to 1",
|
||||
0,
|
||||
));
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn checked_percentage(field: &'static str, value: i32) -> DhtOptionsResult<u8> {
|
||||
if !(0..=100).contains(&value) {
|
||||
return Err(invalid_value(field, "an integer in 0..=100", value));
|
||||
}
|
||||
Ok(u8::try_from(value).expect("0..=100 always fits in u8"))
|
||||
}
|
||||
|
||||
fn checked_netmode(value: i32) -> DhtOptionsResult<NetMode> {
|
||||
match value {
|
||||
0 => Ok(NetMode::Ipv4Only),
|
||||
1 => Ok(NetMode::Ipv6Only),
|
||||
2 => Ok(NetMode::DualStack),
|
||||
_ => Err(invalid_value("netMode", "one of 0, 1, or 2", value)),
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// 常量:Java 类全限定名
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -32,14 +102,15 @@ pub fn file_info_to_java<'local>(
|
||||
env: &mut JNIEnv<'local>,
|
||||
fi: &FileInfo,
|
||||
) -> jni::errors::Result<JObject<'local>> {
|
||||
let cls = env.find_class(CLASS_FILE_INFO)?;
|
||||
let path = rust_str_to_jstring(env, &fi.path)?;
|
||||
let obj = env.new_object(
|
||||
&cls,
|
||||
"(Ljava/lang/String;J)V",
|
||||
&[JValue::Object(&path), JValue::Long(fi.size as jlong)],
|
||||
)?;
|
||||
Ok(obj)
|
||||
env.with_local_frame_returning_local(4, |env| {
|
||||
let cls = env.find_class(CLASS_FILE_INFO)?;
|
||||
let path = rust_str_to_jstring(env, &fi.path)?;
|
||||
env.new_object(
|
||||
&cls,
|
||||
"(Ljava/lang/String;J)V",
|
||||
&[JValue::Object(&path), JValue::Long(fi.size as jlong)],
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -125,40 +196,99 @@ fn build_string_list<'local>(
|
||||
// DHTOptions: Java → Rust
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 从 Java `cn.lmcw.dht.model.DHTOptions` 对象读取字段,构造 Rust `DHTOptions`。
|
||||
pub fn java_to_dht_options(env: &mut JNIEnv, obj: &JObject) -> jni::errors::Result<DHTOptions> {
|
||||
let port = env.get_field(obj, "port", "I")?.i()? as u16;
|
||||
let metadata_timeout_secs = env.get_field(obj, "metadataTimeout", "J")?.j()? as u64;
|
||||
let metadata_max_queue_size = env.get_field(obj, "maxMetadataQueueSize", "I")?.i()? as usize;
|
||||
let metadata_max_worker_count =
|
||||
env.get_field(obj, "maxMetadataWorkerCount", "I")?.i()? as usize;
|
||||
let pool_capacity = env.get_field(obj, "poolCapacity", "I")?.i()? as usize;
|
||||
let find_node_rate = env.get_field(obj, "findNodeRatePerSecond", "I")?.i()? as u32;
|
||||
let find_node_burst = env.get_field(obj, "findNodeBurst", "I")?.i()? as u32;
|
||||
let max_find_node_in_flight = env.get_field(obj, "maxFindNodeInFlight", "I")?.i()? as usize;
|
||||
let max_new_destinations = env
|
||||
.get_field(obj, "maxNewDestinationsPerMinute", "I")?
|
||||
.i()? as u32;
|
||||
let max_replacements = env.get_field(obj, "maxReplacementsPerMinute", "I")?.i()? as u32;
|
||||
let request_timeout_secs = env.get_field(obj, "requestTimeoutSeconds", "J")?.j()? as u64;
|
||||
let max_response_rate = env.get_field(obj, "maxResponseRatePerSecond", "I")?.i()? as u32;
|
||||
let max_response_bytes = env.get_field(obj, "maxResponseBytesPerSecond", "J")?.j()? as u64;
|
||||
let max_response_per_source = env.get_field(obj, "maxResponseRatePerSource", "I")?.i()? as u32;
|
||||
let pressure_floor = env
|
||||
.get_field(obj, "metadataPressureFloorPercent", "I")?
|
||||
.i()? as u8;
|
||||
let recent_probe_ttl = env.get_field(obj, "recentProbeTtlSeconds", "J")?.j()? as u64;
|
||||
let responsive_capacity = env.get_field(obj, "responsiveCapacity", "I")?.i()? as usize;
|
||||
let responsive_ttl = env.get_field(obj, "responsiveTtlSeconds", "J")?.j()? as u64;
|
||||
let low_watermark = env.get_field(obj, "poolLowWatermark", "I")?.i()? as usize;
|
||||
let subnet_in_flight = env.get_field(obj, "maxInFlightPerSubnet", "I")?.i()? as usize;
|
||||
let hash_queue_capacity = env.get_field(obj, "hashQueueCapacity", "I")?.i()? as usize;
|
||||
let netmode_ord = env.get_field(obj, "netMode", "I")?.i()?;
|
||||
let netmode = match netmode_ord {
|
||||
0 => NetMode::Ipv4Only,
|
||||
1 => NetMode::Ipv6Only,
|
||||
_ => NetMode::DualStack,
|
||||
};
|
||||
/// 从 JVM bindings 的 options DTO 读取字段,构造 Rust `DHTOptions`。
|
||||
pub fn java_to_dht_options(env: &mut JNIEnv, obj: &JObject) -> DhtOptionsResult<DHTOptions> {
|
||||
let port = checked_port(env.get_field(obj, "port", "I")?.i()?)?;
|
||||
let metadata_timeout_secs = checked_non_negative_u64(
|
||||
"metadataTimeout",
|
||||
env.get_field(obj, "metadataTimeout", "J")?.j()?,
|
||||
)?;
|
||||
let metadata_max_queue_size = checked_positive_usize(
|
||||
"maxMetadataQueueSize",
|
||||
env.get_field(obj, "maxMetadataQueueSize", "I")?.i()?,
|
||||
)?;
|
||||
let metadata_max_worker_count = checked_positive_usize(
|
||||
"maxMetadataWorkerCount",
|
||||
env.get_field(obj, "maxMetadataWorkerCount", "I")?.i()?,
|
||||
)?;
|
||||
let pool_capacity = checked_positive_usize(
|
||||
"poolCapacity",
|
||||
env.get_field(obj, "poolCapacity", "I")?.i()?,
|
||||
)?;
|
||||
let find_node_rate = checked_non_negative_u32(
|
||||
"findNodeRatePerSecond",
|
||||
env.get_field(obj, "findNodeRatePerSecond", "I")?.i()?,
|
||||
)?;
|
||||
let find_node_burst = checked_non_negative_u32(
|
||||
"findNodeBurst",
|
||||
env.get_field(obj, "findNodeBurst", "I")?.i()?,
|
||||
)?;
|
||||
let max_find_node_in_flight = checked_positive_usize(
|
||||
"maxFindNodeInFlight",
|
||||
env.get_field(obj, "maxFindNodeInFlight", "I")?.i()?,
|
||||
)?;
|
||||
let max_new_destinations = checked_non_negative_u32(
|
||||
"maxNewDestinationsPerMinute",
|
||||
env.get_field(obj, "maxNewDestinationsPerMinute", "I")?
|
||||
.i()?,
|
||||
)?;
|
||||
let max_replacements = checked_non_negative_u32(
|
||||
"maxReplacementsPerMinute",
|
||||
env.get_field(obj, "maxReplacementsPerMinute", "I")?.i()?,
|
||||
)?;
|
||||
let request_timeout_secs = checked_non_negative_u64(
|
||||
"requestTimeoutSeconds",
|
||||
env.get_field(obj, "requestTimeoutSeconds", "J")?.j()?,
|
||||
)?;
|
||||
let max_response_rate = checked_non_negative_u32(
|
||||
"maxResponseRatePerSecond",
|
||||
env.get_field(obj, "maxResponseRatePerSecond", "I")?.i()?,
|
||||
)?;
|
||||
let max_response_bytes = checked_non_negative_u64(
|
||||
"maxResponseBytesPerSecond",
|
||||
env.get_field(obj, "maxResponseBytesPerSecond", "J")?.j()?,
|
||||
)?;
|
||||
let max_response_per_source = checked_non_negative_u32(
|
||||
"maxResponseRatePerSource",
|
||||
env.get_field(obj, "maxResponseRatePerSource", "I")?.i()?,
|
||||
)?;
|
||||
let pressure_floor = checked_percentage(
|
||||
"metadataPressureFloorPercent",
|
||||
env.get_field(obj, "metadataPressureFloorPercent", "I")?
|
||||
.i()?,
|
||||
)?;
|
||||
let recent_probe_ttl = checked_non_negative_u64(
|
||||
"recentProbeTtlSeconds",
|
||||
env.get_field(obj, "recentProbeTtlSeconds", "J")?.j()?,
|
||||
)?;
|
||||
let responsive_capacity = checked_positive_usize(
|
||||
"responsiveCapacity",
|
||||
env.get_field(obj, "responsiveCapacity", "I")?.i()?,
|
||||
)?;
|
||||
let responsive_ttl = checked_non_negative_u64(
|
||||
"responsiveTtlSeconds",
|
||||
env.get_field(obj, "responsiveTtlSeconds", "J")?.j()?,
|
||||
)?;
|
||||
let low_watermark = checked_non_negative_usize(
|
||||
"poolLowWatermark",
|
||||
env.get_field(obj, "poolLowWatermark", "I")?.i()?,
|
||||
)?;
|
||||
if low_watermark > pool_capacity {
|
||||
return Err(invalid_value(
|
||||
"poolLowWatermark",
|
||||
"an integer no greater than poolCapacity",
|
||||
i64::try_from(low_watermark).expect("Java int always fits in i64"),
|
||||
));
|
||||
}
|
||||
let subnet_in_flight = checked_positive_usize(
|
||||
"maxInFlightPerSubnet",
|
||||
env.get_field(obj, "maxInFlightPerSubnet", "I")?.i()?,
|
||||
)?;
|
||||
let hash_queue_capacity = checked_positive_usize(
|
||||
"hashQueueCapacity",
|
||||
env.get_field(obj, "hashQueueCapacity", "I")?.i()?,
|
||||
)?;
|
||||
let netmode = checked_netmode(env.get_field(obj, "netMode", "I")?.i()?)?;
|
||||
|
||||
let mut options = DHTOptions {
|
||||
port,
|
||||
@@ -172,33 +302,102 @@ pub fn java_to_dht_options(env: &mut JNIEnv, obj: &JObject) -> jni::errors::Resu
|
||||
},
|
||||
..DHTOptions::default()
|
||||
};
|
||||
options.crawl.pool.capacity = pool_capacity.max(1);
|
||||
options.crawl.pool.capacity = pool_capacity;
|
||||
options.crawl.pool.recent_probe_ttl_secs = recent_probe_ttl;
|
||||
options.crawl.pool.responsive_capacity = responsive_capacity.max(1);
|
||||
options.crawl.pool.responsive_capacity = responsive_capacity;
|
||||
options.crawl.pool.responsive_ttl_secs = responsive_ttl;
|
||||
options.crawl.pool.low_watermark = low_watermark.min(pool_capacity);
|
||||
options.crawl.pool.low_watermark = low_watermark;
|
||||
options.crawl.rate_limit.max_find_node_rate_per_sec = find_node_rate;
|
||||
options.crawl.rate_limit.burst = find_node_burst;
|
||||
options.crawl.rate_limit.max_in_flight = max_find_node_in_flight.max(1);
|
||||
options.crawl.rate_limit.max_in_flight = max_find_node_in_flight;
|
||||
options.crawl.rate_limit.max_new_destinations_per_minute = max_new_destinations;
|
||||
options.crawl.rate_limit.request_timeout_secs = request_timeout_secs;
|
||||
options.crawl.rate_limit.max_response_rate_per_sec = max_response_rate;
|
||||
options.crawl.rate_limit.max_response_bytes_per_sec = max_response_bytes;
|
||||
options.crawl.rate_limit.max_response_rate_per_source = max_response_per_source;
|
||||
options.crawl.rate_limit.metadata_pressure_floor_percent = pressure_floor.min(100);
|
||||
options.crawl.rate_limit.metadata_pressure_floor_percent = pressure_floor;
|
||||
options.crawl.rate_limit.max_replacements_per_minute = max_replacements;
|
||||
options.crawl.rate_limit.max_in_flight_per_subnet = subnet_in_flight.max(1);
|
||||
options.crawl.rate_limit.max_in_flight_per_subnet = subnet_in_flight;
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
/// 从 Java `cn.lmcw.dht.model.DHTOptions` 对象读取,或若为 null 则返回默认选项。
|
||||
/// 从 JVM bindings 的 options DTO 读取,或若为 null 则返回默认选项。
|
||||
pub fn java_to_dht_options_or_default(
|
||||
env: &mut JNIEnv,
|
||||
obj: &JObject,
|
||||
) -> jni::errors::Result<DHTOptions> {
|
||||
) -> DhtOptionsResult<DHTOptions> {
|
||||
if obj.is_null() {
|
||||
Ok(DHTOptions::default())
|
||||
} else {
|
||||
java_to_dht_options(env, obj)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_invalid<T>(result: DhtOptionsResult<T>, field: &'static str, value: i64) {
|
||||
match result {
|
||||
Err(DhtOptionsConversionError::InvalidValue {
|
||||
field: actual_field,
|
||||
value: actual_value,
|
||||
..
|
||||
}) => {
|
||||
assert_eq!(actual_field, field);
|
||||
assert_eq!(actual_value, value);
|
||||
}
|
||||
_ => panic!("expected an invalid-value error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn port_accepts_java_boundaries_and_rejects_out_of_range_values() {
|
||||
assert_eq!(checked_port(0).unwrap(), 0);
|
||||
assert_eq!(checked_port(i32::from(u16::MAX)).unwrap(), u16::MAX);
|
||||
assert_invalid(checked_port(-1), "port", -1);
|
||||
assert_invalid(checked_port(i32::from(u16::MAX) + 1), "port", 65_536);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_values_are_checked_before_unsigned_conversion() {
|
||||
assert_eq!(checked_non_negative_u32("rate", 0).unwrap(), 0);
|
||||
assert_eq!(
|
||||
checked_non_negative_u32("rate", i32::MAX).unwrap(),
|
||||
i32::MAX as u32
|
||||
);
|
||||
assert_invalid(checked_non_negative_u32("rate", -1), "rate", -1);
|
||||
|
||||
assert_eq!(checked_non_negative_u64("timeout", 0).unwrap(), 0);
|
||||
assert_eq!(
|
||||
checked_non_negative_u64("timeout", i64::MAX).unwrap(),
|
||||
i64::MAX as u64
|
||||
);
|
||||
assert_invalid(checked_non_negative_u64("timeout", -1), "timeout", -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capacities_and_in_flight_limits_must_be_positive() {
|
||||
assert_eq!(checked_positive_usize("capacity", 1).unwrap(), 1);
|
||||
assert_eq!(
|
||||
checked_positive_usize("capacity", i32::MAX).unwrap(),
|
||||
i32::MAX as usize
|
||||
);
|
||||
assert_invalid(checked_positive_usize("capacity", 0), "capacity", 0);
|
||||
assert_invalid(checked_positive_usize("capacity", -1), "capacity", -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage_and_netmode_only_accept_documented_values() {
|
||||
assert_eq!(checked_percentage("percent", 0).unwrap(), 0);
|
||||
assert_eq!(checked_percentage("percent", 100).unwrap(), 100);
|
||||
assert_invalid(checked_percentage("percent", -1), "percent", -1);
|
||||
assert_invalid(checked_percentage("percent", 101), "percent", 101);
|
||||
|
||||
assert_eq!(checked_netmode(0).unwrap(), NetMode::Ipv4Only);
|
||||
assert_eq!(checked_netmode(1).unwrap(), NetMode::Ipv6Only);
|
||||
assert_eq!(checked_netmode(2).unwrap(), NetMode::DualStack);
|
||||
assert_invalid(checked_netmode(-1), "netMode", -1);
|
||||
assert_invalid(checked_netmode(3), "netMode", 3);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -58,5 +58,5 @@ pub mod prelude {
|
||||
|
||||
#[cfg(feature = "jni")]
|
||||
#[path = "../jni/mod.rs"]
|
||||
/// JNI entry points used by the bundled Java wrapper.
|
||||
/// JNI entry points consumed by the external JVM bindings.
|
||||
pub mod jni_bindings;
|
||||
|
||||
Reference in New Issue
Block a user