mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-02 17:15:43 +00:00
feat(ffi): add config server client bindings (#2320)
Add config server client support for the C FFI and Android JNI bindings. Reuse the existing easytier::web_client::run_web_client path and NetworkInstanceManager; OHOS is unchanged. Report successful remote config apply/delete operations through a callback, with one JSON event per affected instance. Keep the config server client and FFI data plane mutually exclusive: once either side is in use, the other side returns an error instead of sharing lifecycle state.
This commit is contained in:
Generated
+5
@@ -2385,6 +2385,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"android_logger",
|
"android_logger",
|
||||||
"easytier",
|
"easytier",
|
||||||
|
"easytier-ffi",
|
||||||
"jni",
|
"jni",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -2396,13 +2397,17 @@ dependencies = [
|
|||||||
name = "easytier-ffi"
|
name = "easytier-ffi"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"easytier",
|
"easytier",
|
||||||
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ android_logger = "0.13"
|
|||||||
serde = { version = "1.0.220", features = ["derive"] }
|
serde = { version = "1.0.220", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
easytier = { path = "../../easytier" }
|
easytier = { path = "../../easytier" }
|
||||||
|
easytier-ffi = { path = "../easytier-ffi", default-features = false }
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
if !matches!(target_os.as_str(), "android" | "linux") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let exports = manifest_dir.join("exports.map");
|
||||||
|
println!("cargo:rerun-if-changed={}", exports.display());
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-cdylib-link-arg=-Wl,--version-script={}",
|
||||||
|
exports.display()
|
||||||
|
);
|
||||||
|
println!("cargo:rustc-cdylib-link-arg=-Wl,--exclude-libs,ALL");
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
global:
|
||||||
|
Java_com_easytier_jni_EasyTierJNI_*;
|
||||||
|
local:
|
||||||
|
*;
|
||||||
|
};
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
package com.easytier.jni
|
package com.easytier.jni
|
||||||
|
|
||||||
|
fun interface ConfigServerEventCallback {
|
||||||
|
fun onEvent(eventJson: String)
|
||||||
|
}
|
||||||
|
|
||||||
/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 网络功能的接口 */
|
/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 网络功能的接口 */
|
||||||
object EasyTierJNI {
|
object EasyTierJNI {
|
||||||
|
|
||||||
@@ -33,6 +37,35 @@ object EasyTierJNI {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic external fun runNetworkInstance(config: String): Int
|
@JvmStatic external fun runNetworkInstance(config: String): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动配置服务器客户端
|
||||||
|
* @param url 配置服务器 URL
|
||||||
|
* @param hostname 主机名,传入 null 使用系统主机名
|
||||||
|
* @param machineId 稳定机器 ID,由调用方负责持久化
|
||||||
|
* @param secureMode 是否启用 secure mode
|
||||||
|
* @param callback 远程配置应用/删除事件回调
|
||||||
|
* @return 0 表示成功,-1 表示失败
|
||||||
|
* @throws RuntimeException 当客户端启动失败时抛出异常
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
external fun startConfigServerClient(
|
||||||
|
url: String,
|
||||||
|
hostname: String?,
|
||||||
|
machineId: String,
|
||||||
|
secureMode: Boolean,
|
||||||
|
callback: ConfigServerEventCallback?
|
||||||
|
): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止配置服务器客户端
|
||||||
|
* @return 0 表示成功,-1 表示失败
|
||||||
|
* @throws RuntimeException 当客户端停止失败时抛出异常
|
||||||
|
*/
|
||||||
|
@JvmStatic external fun stopConfigServerClient(): Int
|
||||||
|
|
||||||
|
/** 查询配置服务器客户端是否已连接 */
|
||||||
|
@JvmStatic external fun isConfigServerClientConnected(): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保留指定的网络实例,停止其他实例
|
* 保留指定的网络实例,停止其他实例
|
||||||
* @param instanceNames 要保留的实例名称数组,传入 null 或空数组将停止所有实例
|
* @param instanceNames 要保留的实例名称数组,传入 null 或空数组将停止所有实例
|
||||||
@@ -44,7 +77,7 @@ object EasyTierJNI {
|
|||||||
/**
|
/**
|
||||||
* 收集网络信息
|
* 收集网络信息
|
||||||
* @param maxLength 最大返回条目数
|
* @param maxLength 最大返回条目数
|
||||||
* @return 包含网络信息的字符串数组,每个元素格式为 "key=value"
|
* @return 包含网络信息的 JSON 字符串
|
||||||
* @throws RuntimeException 当操作失败时抛出异常
|
* @throws RuntimeException 当操作失败时抛出异常
|
||||||
*/
|
*/
|
||||||
@JvmStatic external fun collectNetworkInfos(maxLength: Int): String?
|
@JvmStatic external fun collectNetworkInfos(maxLength: Int): String?
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
use std::{
|
||||||
|
ffi::{CStr, c_char, c_void},
|
||||||
|
sync::{Arc, Mutex, MutexGuard},
|
||||||
|
};
|
||||||
|
|
||||||
|
use easytier_ffi::ConfigServerEventCallback;
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::{GlobalRef, JObject, JValue};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::error;
|
||||||
|
|
||||||
|
pub(crate) struct JniConfigServerCallback {
|
||||||
|
java_vm: jni::JavaVM,
|
||||||
|
callback: GlobalRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
static CONFIG_SERVER_CALLBACK: Lazy<Mutex<Option<Arc<JniConfigServerCallback>>>> =
|
||||||
|
Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
pub(crate) fn lock_callback_storage()
|
||||||
|
-> Result<MutexGuard<'static, Option<Arc<JniConfigServerCallback>>>, String> {
|
||||||
|
CONFIG_SERVER_CALLBACK
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("Failed to lock config server callback: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new_callback(
|
||||||
|
env: &mut JNIEnv,
|
||||||
|
callback: &JObject,
|
||||||
|
) -> Result<Arc<JniConfigServerCallback>, String> {
|
||||||
|
let java_vm = env
|
||||||
|
.get_java_vm()
|
||||||
|
.map_err(|e| format!("Failed to get JavaVM: {:?}", e))?;
|
||||||
|
let callback = env
|
||||||
|
.new_global_ref(callback)
|
||||||
|
.map_err(|e| format!("Failed to create callback global ref: {:?}", e))?;
|
||||||
|
Ok(Arc::new(JniConfigServerCallback { java_vm, callback }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn callback_fn(
|
||||||
|
callback: &Option<Arc<JniConfigServerCallback>>,
|
||||||
|
) -> ConfigServerEventCallback {
|
||||||
|
callback
|
||||||
|
.as_ref()
|
||||||
|
.map(|_| config_server_event_callback as unsafe extern "C" fn(*const c_char, *mut c_void))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn user_data(callback: &Option<Arc<JniConfigServerCallback>>) -> *mut c_void {
|
||||||
|
callback
|
||||||
|
.as_ref()
|
||||||
|
.map(|callback| Arc::as_ptr(callback) as *mut c_void)
|
||||||
|
.unwrap_or(std::ptr::null_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JniConfigServerCallback {
|
||||||
|
fn clear_pending_exception(
|
||||||
|
env: &mut JNIEnv,
|
||||||
|
context: &str,
|
||||||
|
error: &dyn std::fmt::Debug,
|
||||||
|
) -> String {
|
||||||
|
match env.exception_check() {
|
||||||
|
Ok(true) => {
|
||||||
|
if let Err(clear_err) = env.exception_clear() {
|
||||||
|
return format!(
|
||||||
|
"{}: {:?}; failed to clear pending Java exception: {:?}",
|
||||||
|
context, error, clear_err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(check_err) => {
|
||||||
|
return format!(
|
||||||
|
"{}: {:?}; failed to check pending Java exception: {:?}",
|
||||||
|
context, error, check_err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("{}: {:?}", context, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(&self, event_json: *const c_char) -> Result<(), String> {
|
||||||
|
let event_json = unsafe { CStr::from_ptr(event_json) }
|
||||||
|
.to_str()
|
||||||
|
.map_err(|e| format!("Invalid config server event JSON: {:?}", e))?;
|
||||||
|
let mut env = self
|
||||||
|
.java_vm
|
||||||
|
.attach_current_thread()
|
||||||
|
.map_err(|e| format!("Failed to attach callback thread: {:?}", e))?;
|
||||||
|
let event_json = env.new_string(event_json).map_err(|e| {
|
||||||
|
Self::clear_pending_exception(&mut env, "Failed to create event string", &e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Err(e) = env.call_method(
|
||||||
|
self.callback.as_obj(),
|
||||||
|
"onEvent",
|
||||||
|
"(Ljava/lang/String;)V",
|
||||||
|
&[JValue::from(&event_json)],
|
||||||
|
) {
|
||||||
|
return Err(Self::clear_pending_exception(
|
||||||
|
&mut env,
|
||||||
|
"Failed to call config server callback",
|
||||||
|
&e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn config_server_event_callback(
|
||||||
|
event_json: *const c_char,
|
||||||
|
user_data: *mut c_void,
|
||||||
|
) {
|
||||||
|
if event_json.is_null() || user_data.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let callback = unsafe { &*(user_data as *const JniConfigServerCallback) };
|
||||||
|
|
||||||
|
if let Err(error) = callback.on_event(event_json) {
|
||||||
|
error::set_callback_error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
use easytier_ffi::{
|
||||||
|
in_config_server_callback, is_config_server_client_connected, start_config_server_client,
|
||||||
|
stop_config_server_client,
|
||||||
|
};
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::{JClass, JObject, JString};
|
||||||
|
use jni::sys::{JNI_FALSE, JNI_TRUE, jboolean, jint};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
callback, error,
|
||||||
|
strings::{jstring_to_cstring, optional_jstring_to_cstring},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn start_config_server_client_jni(
|
||||||
|
env: &mut JNIEnv,
|
||||||
|
config_server_url: JString,
|
||||||
|
hostname: JString,
|
||||||
|
machine_id: JString,
|
||||||
|
secure_mode: jboolean,
|
||||||
|
callback_obj: JObject,
|
||||||
|
) -> jint {
|
||||||
|
if in_config_server_callback() {
|
||||||
|
error::throw_exception(
|
||||||
|
env,
|
||||||
|
"Cannot start config server client from config server callback",
|
||||||
|
);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_server_url = match jstring_to_cstring(env, &config_server_url) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
error::throw_exception(env, &format!("Invalid config server URL: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let hostname = match optional_jstring_to_cstring(env, &hostname) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
error::throw_exception(env, &format!("Invalid hostname: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let machine_id = match jstring_to_cstring(env, &machine_id) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
error::throw_exception(env, &format!("Invalid machine ID: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let callback_ref = if callback_obj.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
match callback::new_callback(env, &callback_obj) {
|
||||||
|
Ok(state) => Some(state),
|
||||||
|
Err(e) => {
|
||||||
|
error::throw_exception(env, &e);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut callback_guard = match callback::lock_callback_storage() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(e) => {
|
||||||
|
error::throw_exception(env, &e);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if callback_guard.is_none() {
|
||||||
|
error::clear_callback_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
let callback_fn = callback::callback_fn(&callback_ref);
|
||||||
|
let user_data = callback::user_data(&callback_ref);
|
||||||
|
let result = unsafe {
|
||||||
|
start_config_server_client(
|
||||||
|
config_server_url.as_ptr(),
|
||||||
|
hostname
|
||||||
|
.as_ref()
|
||||||
|
.map(|value| value.as_ptr())
|
||||||
|
.unwrap_or(ptr::null()),
|
||||||
|
machine_id.as_ptr(),
|
||||||
|
secure_mode == JNI_TRUE,
|
||||||
|
callback_fn,
|
||||||
|
user_data,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if result != 0 {
|
||||||
|
if let Some(error_msg) = error::get_last_error() {
|
||||||
|
error::throw_exception(env, &error_msg);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
*callback_guard = callback_ref;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stop_config_server_client_jni(mut env: JNIEnv, _class: JClass) -> jint {
|
||||||
|
if in_config_server_callback() {
|
||||||
|
let result = stop_config_server_client();
|
||||||
|
if result != 0
|
||||||
|
&& let Some(error_msg) = error::get_last_error()
|
||||||
|
{
|
||||||
|
error::throw_exception(&mut env, &error_msg);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut callback_guard = match callback::lock_callback_storage() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(e) => {
|
||||||
|
error::throw_exception(&mut env, &e);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = stop_config_server_client();
|
||||||
|
if result != 0 {
|
||||||
|
if let Some(error_msg) = error::get_last_error() {
|
||||||
|
error::throw_exception(&mut env, &error_msg);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
*callback_guard = None;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_config_server_client_connected_jni(_env: JNIEnv, _class: JClass) -> jboolean {
|
||||||
|
if is_config_server_client_connected() != 0 {
|
||||||
|
JNI_TRUE
|
||||||
|
} else {
|
||||||
|
JNI_FALSE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
use std::{
|
||||||
|
ffi::{CStr, c_char},
|
||||||
|
ptr,
|
||||||
|
sync::Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
|
use easytier_ffi::{free_string, get_error_msg};
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::JClass;
|
||||||
|
use jni::sys::jstring;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
static JNI_CALLBACK_ERROR: Lazy<Mutex<Option<String>>> = Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
pub(crate) fn set_callback_error(error: String) {
|
||||||
|
log::error!("{}", error);
|
||||||
|
if let Ok(mut guard) = JNI_CALLBACK_ERROR.lock() {
|
||||||
|
*guard = Some(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear_callback_error() {
|
||||||
|
if let Ok(mut guard) = JNI_CALLBACK_ERROR.lock() {
|
||||||
|
*guard = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_callback_error() -> Option<String> {
|
||||||
|
JNI_CALLBACK_ERROR
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|mut guard| guard.take())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ffi_last_error() -> Option<String> {
|
||||||
|
unsafe {
|
||||||
|
let mut error_ptr: *const c_char = ptr::null();
|
||||||
|
get_error_msg(&mut error_ptr);
|
||||||
|
if error_ptr.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let error_cstr = CStr::from_ptr(error_ptr);
|
||||||
|
let error_str = error_cstr.to_string_lossy().into_owned();
|
||||||
|
free_string(error_ptr);
|
||||||
|
Some(error_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_last_error() -> Option<String> {
|
||||||
|
match (get_ffi_last_error(), take_callback_error()) {
|
||||||
|
(Some(ffi_error), Some(callback_error)) => Some(format!(
|
||||||
|
"{}; config server callback error: {}",
|
||||||
|
ffi_error, callback_error
|
||||||
|
)),
|
||||||
|
(Some(ffi_error), None) => Some(ffi_error),
|
||||||
|
(None, Some(callback_error)) => Some(callback_error),
|
||||||
|
(None, None) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn throw_exception(env: &mut JNIEnv, message: &str) {
|
||||||
|
let _ = env.throw_new("java/lang/RuntimeException", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_last_error_jni(env: JNIEnv, _class: JClass) -> jstring {
|
||||||
|
match get_last_error() {
|
||||||
|
Some(error) => match env.new_string(&error) {
|
||||||
|
Ok(jstr) => jstr.into_raw(),
|
||||||
|
Err(_) => ptr::null_mut(),
|
||||||
|
},
|
||||||
|
None => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,319 +1,203 @@
|
|||||||
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
//! JNI facade for Android callers of EasyTier.
|
||||||
|
//!
|
||||||
|
//! This file intentionally lists every Java-visible native method exported by
|
||||||
|
//! `libeasytier_android_jni.so`. The implementation details live in sibling
|
||||||
|
//! modules so this facade stays readable as an API map.
|
||||||
|
//!
|
||||||
|
//! Network management APIs:
|
||||||
|
//! - `setTunFd(instanceName, fd)`: attach an Android TUN fd to an instance.
|
||||||
|
//! - `parseConfig(config)`: validate TOML config text.
|
||||||
|
//! - `runNetworkInstance(config)`: start a local network instance.
|
||||||
|
//! - `retainNetworkInstance(instanceNames)`: retain named instances and stop the rest.
|
||||||
|
//! - `collectNetworkInfos()`: return running instance info as a JSON string.
|
||||||
|
//!
|
||||||
|
//! Config server client APIs:
|
||||||
|
//! - `startConfigServerClient(url, hostname, machineId, secureMode, callback)`:
|
||||||
|
//! start the managed remote config client.
|
||||||
|
//! - `stopConfigServerClient()`: stop the managed client and release its Java callback.
|
||||||
|
//! - `isConfigServerClientConnected()`: return whether the managed client is connected.
|
||||||
|
//!
|
||||||
|
//! Error API:
|
||||||
|
//! - `getLastError()`: return the latest FFI/JNI error string for the calling thread.
|
||||||
|
|
||||||
|
mod callback;
|
||||||
|
mod config_server_api;
|
||||||
|
mod error;
|
||||||
|
mod logger;
|
||||||
|
mod network_api;
|
||||||
|
mod strings;
|
||||||
|
|
||||||
use jni::JNIEnv;
|
use jni::JNIEnv;
|
||||||
use jni::objects::{JClass, JObjectArray, JString};
|
use jni::objects::{JClass, JObject, JObjectArray, JString};
|
||||||
use jni::sys::{jint, jstring};
|
use jni::sys::{jboolean, jint, jstring};
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use std::ffi::{CStr, CString};
|
|
||||||
use std::ptr;
|
|
||||||
|
|
||||||
// 定义 KeyValuePair 结构体
|
/// Attach a TUN file descriptor to an EasyTier network instance.
|
||||||
#[repr(C)]
|
///
|
||||||
#[derive(Clone, Copy)]
|
/// Java signature:
|
||||||
pub struct KeyValuePair {
|
/// `EasyTierJNI.setTunFd(instanceName: String, fd: Int): Int`
|
||||||
pub key: *const std::ffi::c_char,
|
///
|
||||||
pub value: *const std::ffi::c_char,
|
/// `instanceName` must name an instance known to the shared FFI instance cache.
|
||||||
}
|
/// The `fd` must be a valid Android TUN file descriptor. On failure this
|
||||||
|
/// returns `-1` and throws `RuntimeException` with the FFI error message when
|
||||||
// 声明外部 C 函数
|
/// one is available.
|
||||||
unsafe extern "C" {
|
|
||||||
fn set_tun_fd(inst_name: *const std::ffi::c_char, fd: std::ffi::c_int) -> std::ffi::c_int;
|
|
||||||
fn get_error_msg(out: *mut *const std::ffi::c_char);
|
|
||||||
fn free_string(s: *const std::ffi::c_char);
|
|
||||||
fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int;
|
|
||||||
fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int;
|
|
||||||
fn retain_network_instance(
|
|
||||||
inst_names: *const *const std::ffi::c_char,
|
|
||||||
length: usize,
|
|
||||||
) -> std::ffi::c_int;
|
|
||||||
fn collect_network_infos(infos: *mut KeyValuePair, max_length: usize) -> std::ffi::c_int;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化 Android 日志
|
|
||||||
static LOGGER_INIT: Lazy<()> = Lazy::new(|| {
|
|
||||||
android_logger::init_once(
|
|
||||||
android_logger::Config::default()
|
|
||||||
.with_max_level(log::LevelFilter::Debug)
|
|
||||||
.with_tag("EasyTier-JNI"),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 辅助函数:从 Java String 转换为 CString
|
|
||||||
fn jstring_to_cstring(env: &mut JNIEnv, jstr: &JString) -> Result<CString, String> {
|
|
||||||
let java_str = env
|
|
||||||
.get_string(jstr)
|
|
||||||
.map_err(|e| format!("Failed to get string: {:?}", e))?;
|
|
||||||
let rust_str = java_str.to_str().map_err(|_| "Invalid UTF-8".to_string())?;
|
|
||||||
CString::new(rust_str).map_err(|_| "String contains null byte".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数:获取错误消息
|
|
||||||
fn get_last_error() -> Option<String> {
|
|
||||||
unsafe {
|
|
||||||
let mut error_ptr: *const std::ffi::c_char = ptr::null();
|
|
||||||
get_error_msg(&mut error_ptr);
|
|
||||||
if error_ptr.is_null() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let error_cstr = CStr::from_ptr(error_ptr);
|
|
||||||
let error_str = error_cstr.to_string_lossy().into_owned();
|
|
||||||
free_string(error_ptr);
|
|
||||||
Some(error_str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数:抛出 Java 异常
|
|
||||||
fn throw_exception(env: &mut JNIEnv, message: &str) {
|
|
||||||
let _ = env.throw_new("java/lang/RuntimeException", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 设置 TUN 文件描述符
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
|
||||||
mut env: JNIEnv,
|
env: JNIEnv,
|
||||||
_class: JClass,
|
class: JClass,
|
||||||
inst_name: JString,
|
inst_name: JString,
|
||||||
fd: jint,
|
fd: jint,
|
||||||
) -> jint {
|
) -> jint {
|
||||||
Lazy::force(&LOGGER_INIT);
|
logger::init();
|
||||||
|
network_api::set_tun_fd_jni(env, class, inst_name, fd)
|
||||||
let inst_name_cstr = match jstring_to_cstring(&mut env, &inst_name) {
|
|
||||||
Ok(cstr) => cstr,
|
|
||||||
Err(e) => {
|
|
||||||
throw_exception(&mut env, &format!("Invalid instance name: {}", e));
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
|
|
||||||
if result != 0
|
|
||||||
&& let Some(error) = get_last_error()
|
|
||||||
{
|
|
||||||
throw_exception(&mut env, &error);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 解析配置
|
/// Validate a TOML network config string.
|
||||||
|
///
|
||||||
|
/// Java signature:
|
||||||
|
/// `EasyTierJNI.parseConfig(config: String): Int`
|
||||||
|
///
|
||||||
|
/// This only validates the config text; it does not start or mutate any
|
||||||
|
/// instance. On failure this returns `-1` and throws `RuntimeException`.
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
|
||||||
mut env: JNIEnv,
|
env: JNIEnv,
|
||||||
_class: JClass,
|
class: JClass,
|
||||||
config: JString,
|
config: JString,
|
||||||
) -> jint {
|
) -> jint {
|
||||||
Lazy::force(&LOGGER_INIT);
|
logger::init();
|
||||||
|
network_api::parse_config_jni(env, class, config)
|
||||||
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
|
||||||
Ok(cstr) => cstr,
|
|
||||||
Err(e) => {
|
|
||||||
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let result = parse_config(config_cstr.as_ptr());
|
|
||||||
if result != 0
|
|
||||||
&& let Some(error) = get_last_error()
|
|
||||||
{
|
|
||||||
throw_exception(&mut env, &error);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 运行网络实例
|
/// Start one local EasyTier network instance from TOML config text.
|
||||||
|
///
|
||||||
|
/// Java signature:
|
||||||
|
/// `EasyTierJNI.runNetworkInstance(config: String): Int`
|
||||||
|
///
|
||||||
|
/// The instance name in the config must be unique in the FFI instance cache.
|
||||||
|
/// On failure this returns `-1` and throws `RuntimeException`.
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
|
||||||
mut env: JNIEnv,
|
env: JNIEnv,
|
||||||
_class: JClass,
|
class: JClass,
|
||||||
config: JString,
|
config: JString,
|
||||||
) -> jint {
|
) -> jint {
|
||||||
Lazy::force(&LOGGER_INIT);
|
logger::init();
|
||||||
|
network_api::run_network_instance_jni(env, class, config)
|
||||||
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
|
||||||
Ok(cstr) => cstr,
|
|
||||||
Err(e) => {
|
|
||||||
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let result = run_network_instance(config_cstr.as_ptr());
|
|
||||||
if result != 0
|
|
||||||
&& let Some(error) = get_last_error()
|
|
||||||
{
|
|
||||||
throw_exception(&mut env, &error);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 保持网络实例
|
/// Retain the named network instances and stop all other instances.
|
||||||
|
///
|
||||||
|
/// Java signature:
|
||||||
|
/// `EasyTierJNI.retainNetworkInstance(instanceNames: Array<String>?): Int`
|
||||||
|
///
|
||||||
|
/// Passing `null` or an empty array stops all instances. Null elements inside a
|
||||||
|
/// non-empty array are skipped. On failure this returns `-1` and throws
|
||||||
|
/// `RuntimeException`.
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||||
mut env: JNIEnv,
|
env: JNIEnv,
|
||||||
_class: JClass,
|
class: JClass,
|
||||||
instance_names: JObjectArray,
|
instance_names: JObjectArray,
|
||||||
) -> jint {
|
) -> jint {
|
||||||
Lazy::force(&LOGGER_INIT);
|
logger::init();
|
||||||
|
network_api::retain_network_instance_jni(env, class, instance_names)
|
||||||
// 处理 null 数组的情况
|
|
||||||
if instance_names.is_null() {
|
|
||||||
unsafe {
|
|
||||||
let result = retain_network_instance(ptr::null(), 0);
|
|
||||||
if result != 0
|
|
||||||
&& let Some(error) = get_last_error()
|
|
||||||
{
|
|
||||||
throw_exception(&mut env, &error);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取数组长度
|
/// Collect running network instance information.
|
||||||
let array_length = match env.get_array_length(&instance_names) {
|
///
|
||||||
Ok(len) => len as usize,
|
/// Java signature:
|
||||||
Err(e) => {
|
/// `EasyTierJNI.collectNetworkInfos(maxLength: Int): String?`
|
||||||
throw_exception(&mut env, &format!("Failed to get array length: {:?}", e));
|
///
|
||||||
return -1;
|
/// Returns a JSON string containing `NetworkInstanceRunningInfoMap`, or null if
|
||||||
}
|
/// collection fails. `maxLength` limits how many FFI entries are collected. On
|
||||||
};
|
/// failure this throws `RuntimeException` when an error message is available.
|
||||||
|
|
||||||
// 如果数组为空,停止所有实例
|
|
||||||
if array_length == 0 {
|
|
||||||
unsafe {
|
|
||||||
let result = retain_network_instance(ptr::null(), 0);
|
|
||||||
if result != 0
|
|
||||||
&& let Some(error) = get_last_error()
|
|
||||||
{
|
|
||||||
throw_exception(&mut env, &error);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换 Java 字符串数组为 C 字符串数组
|
|
||||||
let mut c_strings = Vec::with_capacity(array_length);
|
|
||||||
let mut c_string_ptrs = Vec::with_capacity(array_length);
|
|
||||||
|
|
||||||
for i in 0..array_length {
|
|
||||||
let java_string = match env.get_object_array_element(&instance_names, i as i32) {
|
|
||||||
Ok(obj) => obj,
|
|
||||||
Err(e) => {
|
|
||||||
throw_exception(
|
|
||||||
&mut env,
|
|
||||||
&format!("Failed to get array element {}: {:?}", i, e),
|
|
||||||
);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if java_string.is_null() {
|
|
||||||
continue; // 跳过 null 元素
|
|
||||||
}
|
|
||||||
|
|
||||||
let jstring = JString::from(java_string);
|
|
||||||
let c_string = match jstring_to_cstring(&mut env, &jstring) {
|
|
||||||
Ok(cstr) => cstr,
|
|
||||||
Err(e) => {
|
|
||||||
throw_exception(
|
|
||||||
&mut env,
|
|
||||||
&format!("Invalid instance name at index {}: {}", i, e),
|
|
||||||
);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
c_string_ptrs.push(c_string.as_ptr());
|
|
||||||
c_strings.push(c_string); // 保持 CString 的所有权
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
|
|
||||||
if result != 0
|
|
||||||
&& let Some(error) = get_last_error()
|
|
||||||
{
|
|
||||||
throw_exception(&mut env, &error);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 收集网络信息
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
||||||
mut env: JNIEnv,
|
env: JNIEnv,
|
||||||
_class: JClass,
|
class: JClass,
|
||||||
|
max_length: jint,
|
||||||
) -> jstring {
|
) -> jstring {
|
||||||
Lazy::force(&LOGGER_INIT);
|
logger::init();
|
||||||
|
network_api::collect_network_infos_jni(env, class, max_length)
|
||||||
const MAX_INFOS: usize = 100;
|
|
||||||
let mut infos = vec![
|
|
||||||
KeyValuePair {
|
|
||||||
key: ptr::null(),
|
|
||||||
value: ptr::null(),
|
|
||||||
};
|
|
||||||
MAX_INFOS
|
|
||||||
];
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let count = collect_network_infos(infos.as_mut_ptr(), MAX_INFOS);
|
|
||||||
if count < 0 {
|
|
||||||
if let Some(error) = get_last_error() {
|
|
||||||
throw_exception(&mut env, &error);
|
|
||||||
}
|
|
||||||
return ptr::null_mut();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ret = NetworkInstanceRunningInfoMap::default();
|
/// Return the latest FFI/JNI error string for the calling thread.
|
||||||
|
///
|
||||||
// 使用 serde_json 构建 JSON
|
/// Java signature:
|
||||||
for info in infos.iter().take(count as usize) {
|
/// `EasyTierJNI.getLastError(): String?`
|
||||||
let key_ptr = info.key;
|
///
|
||||||
let val_ptr = info.value;
|
/// This combines the FFI thread-local error with any pending config-server Java
|
||||||
if key_ptr.is_null() || val_ptr.is_null() {
|
/// callback error. It returns null when no error is available.
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = CStr::from_ptr(key_ptr).to_string_lossy();
|
|
||||||
let val = CStr::from_ptr(val_ptr).to_string_lossy();
|
|
||||||
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(val.as_ref()) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => {
|
|
||||||
throw_exception(&mut env, "Failed to parse JSON");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ret.map.insert(key.to_string(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
|
|
||||||
|
|
||||||
match env.new_string(&json_str) {
|
|
||||||
Ok(jstr) => jstr.into_raw(),
|
|
||||||
Err(_) => {
|
|
||||||
throw_exception(&mut env, "Failed to create JSON string");
|
|
||||||
ptr::null_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取最后的错误信息
|
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError(
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError(
|
||||||
env: JNIEnv,
|
env: JNIEnv,
|
||||||
_class: JClass,
|
class: JClass,
|
||||||
) -> jstring {
|
) -> jstring {
|
||||||
match get_last_error() {
|
error::get_last_error_jni(env, class)
|
||||||
Some(error) => match env.new_string(&error) {
|
|
||||||
Ok(jstr) => jstr.into_raw(),
|
|
||||||
Err(_) => ptr::null_mut(),
|
|
||||||
},
|
|
||||||
None => ptr::null_mut(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start the managed config-server client.
|
||||||
|
///
|
||||||
|
/// Java signature:
|
||||||
|
/// `EasyTierJNI.startConfigServerClient(url, hostname, machineId, secureMode, callback): Int`
|
||||||
|
///
|
||||||
|
/// JNI only converts Java values and keeps the Java callback alive. The FFI
|
||||||
|
/// layer owns singleton lifecycle, config-server/data-plane mutual exclusion,
|
||||||
|
/// remote instance tracking, and callback event timing. If `callback` is
|
||||||
|
/// non-null, each remote apply/delete event is delivered to
|
||||||
|
/// `ConfigServerEventCallback.onEvent(eventJson)`.
|
||||||
|
///
|
||||||
|
/// On failure this returns `-1` and throws `RuntimeException`.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_startConfigServerClient(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
config_server_url: JString,
|
||||||
|
hostname: JString,
|
||||||
|
machine_id: JString,
|
||||||
|
secure_mode: jboolean,
|
||||||
|
callback: JObject,
|
||||||
|
) -> jint {
|
||||||
|
logger::init();
|
||||||
|
config_server_api::start_config_server_client_jni(
|
||||||
|
&mut env,
|
||||||
|
config_server_url,
|
||||||
|
hostname,
|
||||||
|
machine_id,
|
||||||
|
secure_mode,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the managed config-server client.
|
||||||
|
///
|
||||||
|
/// Java signature:
|
||||||
|
/// `EasyTierJNI.stopConfigServerClient(): Int`
|
||||||
|
///
|
||||||
|
/// The FFI layer performs the actual stop and managed instance cleanup. JNI
|
||||||
|
/// releases the Java callback reference after FFI stop succeeds. On failure
|
||||||
|
/// this returns `-1` and throws `RuntimeException`.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_stopConfigServerClient(
|
||||||
|
env: JNIEnv,
|
||||||
|
class: JClass,
|
||||||
|
) -> jint {
|
||||||
|
logger::init();
|
||||||
|
config_server_api::stop_config_server_client_jni(env, class)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Report whether the managed config-server client is connected.
|
||||||
|
///
|
||||||
|
/// Java signature:
|
||||||
|
/// `EasyTierJNI.isConfigServerClientConnected(): Boolean`
|
||||||
|
///
|
||||||
|
/// Returns `JNI_TRUE` only when the FFI config-server client exists and reports
|
||||||
|
/// connected.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_isConfigServerClientConnected(
|
||||||
|
env: JNIEnv,
|
||||||
|
class: JClass,
|
||||||
|
) -> jboolean {
|
||||||
|
logger::init();
|
||||||
|
config_server_api::is_config_server_client_connected_jni(env, class)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
static LOGGER_INIT: Lazy<()> = Lazy::new(|| {
|
||||||
|
android_logger::init_once(
|
||||||
|
android_logger::Config::default()
|
||||||
|
.with_max_level(log::LevelFilter::Debug)
|
||||||
|
.with_tag("EasyTier-JNI"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
pub(crate) fn init() {
|
||||||
|
Lazy::force(&LOGGER_INIT);
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
use std::{ffi::CStr, ptr};
|
||||||
|
|
||||||
|
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||||
|
use easytier_ffi::{
|
||||||
|
KeyValuePair, collect_network_infos, parse_config, retain_network_instance,
|
||||||
|
run_network_instance, set_tun_fd,
|
||||||
|
};
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::{JClass, JObjectArray, JString};
|
||||||
|
use jni::sys::{jint, jstring};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::{get_last_error, throw_exception},
|
||||||
|
strings::jstring_to_cstring,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn set_tun_fd_jni(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
inst_name: JString,
|
||||||
|
fd: jint,
|
||||||
|
) -> jint {
|
||||||
|
let inst_name_cstr = match jstring_to_cstring(&mut env, &inst_name) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid instance name: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
|
||||||
|
if result != 0
|
||||||
|
&& let Some(error) = get_last_error()
|
||||||
|
{
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_config_jni(mut env: JNIEnv, _class: JClass, config: JString) -> jint {
|
||||||
|
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
let result = parse_config(config_cstr.as_ptr());
|
||||||
|
if result != 0
|
||||||
|
&& let Some(error) = get_last_error()
|
||||||
|
{
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run_network_instance_jni(mut env: JNIEnv, _class: JClass, config: JString) -> jint {
|
||||||
|
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
let result = run_network_instance(config_cstr.as_ptr());
|
||||||
|
if result != 0
|
||||||
|
&& let Some(error) = get_last_error()
|
||||||
|
{
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn retain_network_instance_jni(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
instance_names: JObjectArray,
|
||||||
|
) -> jint {
|
||||||
|
if instance_names.is_null() {
|
||||||
|
return retain_all(&mut env);
|
||||||
|
}
|
||||||
|
|
||||||
|
let array_length = match env.get_array_length(&instance_names) {
|
||||||
|
Ok(len) => len as usize,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Failed to get array length: {:?}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if array_length == 0 {
|
||||||
|
return retain_all(&mut env);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut c_strings = Vec::with_capacity(array_length);
|
||||||
|
let mut c_string_ptrs = Vec::with_capacity(array_length);
|
||||||
|
|
||||||
|
for i in 0..array_length {
|
||||||
|
let java_string = match env.get_object_array_element(&instance_names, i as i32) {
|
||||||
|
Ok(obj) => obj,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(
|
||||||
|
&mut env,
|
||||||
|
&format!("Failed to get array element {}: {:?}", i, e),
|
||||||
|
);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if java_string.is_null() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let jstring = JString::from(java_string);
|
||||||
|
let c_string = match jstring_to_cstring(&mut env, &jstring) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(
|
||||||
|
&mut env,
|
||||||
|
&format!("Invalid instance name at index {}: {}", i, e),
|
||||||
|
);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
c_string_ptrs.push(c_string.as_ptr());
|
||||||
|
c_strings.push(c_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
|
||||||
|
if result != 0
|
||||||
|
&& let Some(error) = get_last_error()
|
||||||
|
{
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn retain_all(env: &mut JNIEnv) -> jint {
|
||||||
|
unsafe {
|
||||||
|
let result = retain_network_instance(ptr::null(), 0);
|
||||||
|
if result != 0
|
||||||
|
&& let Some(error) = get_last_error()
|
||||||
|
{
|
||||||
|
throw_exception(env, &error);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn collect_network_infos_jni(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
max_length: jint,
|
||||||
|
) -> jstring {
|
||||||
|
let max_length = max_length.max(0) as usize;
|
||||||
|
let mut infos = vec![
|
||||||
|
KeyValuePair {
|
||||||
|
key: ptr::null(),
|
||||||
|
value: ptr::null(),
|
||||||
|
};
|
||||||
|
max_length
|
||||||
|
];
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let count = collect_network_infos(infos.as_mut_ptr(), max_length);
|
||||||
|
if count < 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ret = NetworkInstanceRunningInfoMap::default();
|
||||||
|
for info in infos.iter().take(count as usize) {
|
||||||
|
let key_ptr = info.key;
|
||||||
|
let val_ptr = info.value;
|
||||||
|
if key_ptr.is_null() || val_ptr.is_null() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = CStr::from_ptr(key_ptr).to_string_lossy();
|
||||||
|
let val = CStr::from_ptr(val_ptr).to_string_lossy();
|
||||||
|
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(val.as_ref()) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => {
|
||||||
|
throw_exception(&mut env, "Failed to parse JSON");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ret.map.insert(key.to_string(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
|
||||||
|
match env.new_string(&json_str) {
|
||||||
|
Ok(jstr) => jstr.into_raw(),
|
||||||
|
Err(_) => {
|
||||||
|
throw_exception(&mut env, "Failed to create JSON string");
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
use std::ffi::CString;
|
||||||
|
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::JString;
|
||||||
|
|
||||||
|
pub(crate) fn jstring_to_cstring(env: &mut JNIEnv, jstr: &JString) -> Result<CString, String> {
|
||||||
|
let java_str = env
|
||||||
|
.get_string(jstr)
|
||||||
|
.map_err(|e| format!("Failed to get string: {:?}", e))?;
|
||||||
|
let rust_str = java_str.to_str().map_err(|_| "Invalid UTF-8".to_string())?;
|
||||||
|
CString::new(rust_str).map_err(|_| "String contains null byte".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn optional_jstring_to_cstring(
|
||||||
|
env: &mut JNIEnv,
|
||||||
|
jstr: &JString,
|
||||||
|
) -> Result<Option<CString>, String> {
|
||||||
|
if jstr.is_null() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring_to_cstring(env, jstr).map(Some)
|
||||||
|
}
|
||||||
@@ -4,10 +4,11 @@ version = "0.1.0"
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["ffi-dataplane"]
|
default = ["c-abi", "ffi-dataplane"]
|
||||||
|
c-abi = []
|
||||||
ffi-dataplane = ["easytier/ffi-dataplane"]
|
ffi-dataplane = ["easytier/ffi-dataplane"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -15,9 +16,13 @@ easytier = { path = "../../easytier" }
|
|||||||
|
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
dashmap = "6.0"
|
dashmap = "6.0"
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "io-util", "time", "sync", "macros"] }
|
||||||
|
async-trait = "0.1"
|
||||||
|
log = "0.4"
|
||||||
|
percent-encoding = "2.3"
|
||||||
|
url = "2"
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
uuid = "1.17.0"
|
uuid = "1.17.0"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "io-util", "time", "sync", "macros"] }
|
|
||||||
tokio-util = "0.7"
|
tokio-util = "0.7"
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <unistd.h> // for sleep
|
||||||
|
|
||||||
|
// FFI struct and function declarations
|
||||||
|
typedef struct {
|
||||||
|
const char* key;
|
||||||
|
const char* value;
|
||||||
|
} KeyValuePair;
|
||||||
|
|
||||||
|
typedef void (*config_server_event_callback)(
|
||||||
|
const char* event_json,
|
||||||
|
void* user_data
|
||||||
|
);
|
||||||
|
|
||||||
|
extern int parse_config(const char* cfg_str);
|
||||||
|
extern int run_network_instance(const char* cfg_str);
|
||||||
|
extern void get_error_msg(const char** out);
|
||||||
|
extern void free_string(const char* s);
|
||||||
|
extern int collect_network_infos(KeyValuePair* infos, size_t max_length);
|
||||||
|
extern int start_config_server_client(
|
||||||
|
const char* config_server_url,
|
||||||
|
const char* hostname,
|
||||||
|
const char* machine_id,
|
||||||
|
bool secure_mode,
|
||||||
|
config_server_event_callback callback,
|
||||||
|
void* user_data
|
||||||
|
);
|
||||||
|
extern int stop_config_server_client(void);
|
||||||
|
extern int is_config_server_client_connected(void);
|
||||||
|
|
||||||
|
static void on_config_server_event(const char* event_json, void* user_data) {
|
||||||
|
(void)user_data;
|
||||||
|
printf("config server event: %s\n", event_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
const char* config = "inst_name = \"test\"\nnetwork = \"test_network\"\n";
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
// 调用 parse_config
|
||||||
|
ret = parse_config(config);
|
||||||
|
if (ret != 0) {
|
||||||
|
const char* err = NULL;
|
||||||
|
get_error_msg(&err);
|
||||||
|
if (err) {
|
||||||
|
printf("parse_config error: %s\n", err);
|
||||||
|
free_string(err);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("parse_config success\n");
|
||||||
|
|
||||||
|
// 调用 run_network_instance
|
||||||
|
ret = run_network_instance(config);
|
||||||
|
if (ret != 0) {
|
||||||
|
const char* err = NULL;
|
||||||
|
get_error_msg(&err);
|
||||||
|
if (err) {
|
||||||
|
printf("run_network_instance error: %s\n", err);
|
||||||
|
free_string(err);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("run_network_instance success\n");
|
||||||
|
|
||||||
|
// 周期性调用 collect_network_infos 并打印
|
||||||
|
const size_t max_infos = 8;
|
||||||
|
KeyValuePair* infos = (KeyValuePair*)malloc(sizeof(KeyValuePair) * max_infos);
|
||||||
|
if (!infos) {
|
||||||
|
fprintf(stderr, "malloc failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; ++i) { // 循环5次作为示例
|
||||||
|
memset(infos, 0, sizeof(KeyValuePair) * max_infos);
|
||||||
|
int count = collect_network_infos(infos, max_infos);
|
||||||
|
if (count < 0) {
|
||||||
|
const char* err = NULL;
|
||||||
|
get_error_msg(&err);
|
||||||
|
if (err) {
|
||||||
|
printf("collect_network_infos error: %s\n", err);
|
||||||
|
free_string(err);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
printf("collect_network_infos: %d instance(s)\n", count);
|
||||||
|
for (int j = 0; j < count; ++j) {
|
||||||
|
printf(" [%d] key: %s\n value: %s\n", j, infos[j].key, infos[j].value);
|
||||||
|
free_string(infos[j].key);
|
||||||
|
free_string(infos[j].value);
|
||||||
|
}
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
free(infos);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,565 @@
|
|||||||
|
use std::{
|
||||||
|
cell::Cell,
|
||||||
|
collections::HashSet,
|
||||||
|
ffi::{CString, c_char, c_int, c_void},
|
||||||
|
sync::{
|
||||||
|
Arc, Mutex,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use easytier::{
|
||||||
|
common::{
|
||||||
|
MachineIdOptions,
|
||||||
|
config::{ConfigLoader as _, TomlConfigLoader},
|
||||||
|
},
|
||||||
|
tunnel::TunnelScheme,
|
||||||
|
web_client::{WebClient, WebClientHooks, run_web_client},
|
||||||
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
data_plane::remove_data_plane_handles_by_instance_ids,
|
||||||
|
error::set_error_msg,
|
||||||
|
state::{
|
||||||
|
ASYNC_RUNTIME, INSTANCE_MANAGER, INSTANCE_MUTATION_LOCK, INSTANCE_NAME_ID_MAP,
|
||||||
|
lock_remote_instance_mutation, remove_instance_name_ids,
|
||||||
|
},
|
||||||
|
strings::{c_str_to_string, optional_c_str_to_string},
|
||||||
|
types::ConfigServerEventCallback,
|
||||||
|
};
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static IN_CONFIG_SERVER_CALLBACK: Cell<bool> = const { Cell::new(false) };
|
||||||
|
}
|
||||||
|
|
||||||
|
static CONFIG_SERVER_CLIENT: once_cell::sync::Lazy<Mutex<Option<ManagedConfigServerClient>>> =
|
||||||
|
once_cell::sync::Lazy::new(|| Mutex::new(None));
|
||||||
|
static CONFIG_SERVER_CLIENT_ACTIVE: once_cell::sync::Lazy<AtomicBool> =
|
||||||
|
once_cell::sync::Lazy::new(|| AtomicBool::new(false));
|
||||||
|
static CONFIG_SERVER_CLIENT_STOPPING: once_cell::sync::Lazy<AtomicBool> =
|
||||||
|
once_cell::sync::Lazy::new(|| AtomicBool::new(false));
|
||||||
|
static LAST_CONFIG_SERVER_CALLBACK_ERROR: once_cell::sync::Lazy<Mutex<Option<String>>> =
|
||||||
|
once_cell::sync::Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
pub(crate) struct ConfigServerCallbackScope;
|
||||||
|
|
||||||
|
impl ConfigServerCallbackScope {
|
||||||
|
pub(crate) fn enter() -> Self {
|
||||||
|
IN_CONFIG_SERVER_CALLBACK.with(|in_callback| in_callback.set(true));
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ConfigServerCallbackScope {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
IN_CONFIG_SERVER_CALLBACK.with(|in_callback| in_callback.set(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn in_config_server_callback() -> bool {
|
||||||
|
IN_CONFIG_SERVER_CALLBACK.with(Cell::get)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_server_machine_id_options(machine_id: String) -> MachineIdOptions {
|
||||||
|
MachineIdOptions {
|
||||||
|
explicit_machine_id: Some(machine_id),
|
||||||
|
state_dir: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_config_server_client_options(
|
||||||
|
config_server_url_s: &str,
|
||||||
|
machine_id: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if machine_id.trim().is_empty() {
|
||||||
|
return Err("machine_id is empty".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_server_url = match url::Url::parse(config_server_url_s) {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(_) => format!(
|
||||||
|
"udp://config-server.easytier.cn:22020/{}",
|
||||||
|
config_server_url_s
|
||||||
|
)
|
||||||
|
.parse()
|
||||||
|
.map_err(|err| format!("failed to parse config server URL: {}", err))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
TunnelScheme::try_from(&config_server_url).map_err(|_| {
|
||||||
|
format!(
|
||||||
|
"unsupported config server scheme: {}",
|
||||||
|
config_server_url.scheme()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let token = config_server_url
|
||||||
|
.path_segments()
|
||||||
|
.and_then(|mut segments| segments.next_back())
|
||||||
|
.map(|segment| percent_encoding::percent_decode_str(segment).decode_utf8())
|
||||||
|
.transpose()
|
||||||
|
.map_err(|err| format!("failed to decode config server token: {}", err))?
|
||||||
|
.map(|token| token.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if token.is_empty() {
|
||||||
|
return Err("empty token".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ManagedConfigServerClient {
|
||||||
|
client: WebClient,
|
||||||
|
hooks: Arc<ManagedConfigServerClientHooks>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ManagedConfigServerClientHooks {
|
||||||
|
pub(crate) instance_ids: Mutex<HashSet<Uuid>>,
|
||||||
|
callback_delivery: Mutex<()>,
|
||||||
|
stopping: AtomicBool,
|
||||||
|
callback: ConfigServerEventCallback,
|
||||||
|
user_data: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManagedConfigServerClientHooks {
|
||||||
|
pub(crate) fn new(callback: ConfigServerEventCallback, user_data: *mut c_void) -> Self {
|
||||||
|
Self {
|
||||||
|
instance_ids: Mutex::new(HashSet::new()),
|
||||||
|
callback_delivery: Mutex::new(()),
|
||||||
|
stopping: AtomicBool::new(false),
|
||||||
|
callback,
|
||||||
|
user_data: user_data as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn tracked_instance_ids(&self) -> Vec<Uuid> {
|
||||||
|
self.instance_ids
|
||||||
|
.lock()
|
||||||
|
.map(|guard| guard.iter().copied().collect())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_tracked_instance_ids(&self, ids: &[Uuid]) -> Result<Vec<Uuid>, String> {
|
||||||
|
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
|
||||||
|
Ok(ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|id| guard.remove(id).then_some(*id))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_instance_name(&self, inst_name: &str, inst_id: Uuid) -> Result<(), String> {
|
||||||
|
if let Some(existing_id) = INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id)
|
||||||
|
&& existing_id != inst_id
|
||||||
|
{
|
||||||
|
return Err(format!("instance name {} already exists", inst_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit_instance_name(&self, inst_name: String, inst_id: Uuid) -> Result<(), String> {
|
||||||
|
INSTANCE_NAME_ID_MAP.retain(|_, existing_id| *existing_id != inst_id);
|
||||||
|
self.validate_instance_name(&inst_name, inst_id)?;
|
||||||
|
INSTANCE_NAME_ID_MAP.insert(inst_name, inst_id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn start_stopping(&self) -> Vec<Uuid> {
|
||||||
|
let _delivery_guard = if in_config_server_callback() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.callback_delivery.lock().ok()
|
||||||
|
};
|
||||||
|
let mut guard = match self.instance_ids.lock() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(_) => return Vec::new(),
|
||||||
|
};
|
||||||
|
self.stopping.store(true, Ordering::Release);
|
||||||
|
guard.drain().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn note_callback_error(&self, error: String) {
|
||||||
|
log::warn!("config server event callback failed: {}", error);
|
||||||
|
if let Ok(mut guard) = LAST_CONFIG_SERVER_CALLBACK_ERROR.lock() {
|
||||||
|
*guard = Some(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_event_with_delivery_locked(
|
||||||
|
&self,
|
||||||
|
event: &str,
|
||||||
|
instance_id: Uuid,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if self.stopping.load(Ordering::Acquire) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(callback) = self.callback else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let event_json = serde_json::json!({
|
||||||
|
"event": event,
|
||||||
|
"success": true,
|
||||||
|
"instance_id": instance_id.to_string(),
|
||||||
|
"error": null,
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
let event_json = CString::new(event_json).map_err(|err| err.to_string())?;
|
||||||
|
let _callback_scope = ConfigServerCallbackScope::enter();
|
||||||
|
unsafe {
|
||||||
|
callback(event_json.as_ptr(), self.user_data as *mut c_void);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_event(&self, event: &str, instance_id: Uuid) -> Result<(), String> {
|
||||||
|
let _delivery_guard = self
|
||||||
|
.callback_delivery
|
||||||
|
.lock()
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
self.emit_event_with_delivery_locked(event, instance_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_callback_delivery(&self) {
|
||||||
|
if in_config_server_callback() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(guard) = self.callback_delivery.lock() {
|
||||||
|
drop(guard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl WebClientHooks for ManagedConfigServerClientHooks {
|
||||||
|
fn manages_remote_config_instances(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pre_run_network_instance(&self, cfg: &TomlConfigLoader) -> Result<(), String> {
|
||||||
|
if self.stopping.load(Ordering::Acquire) {
|
||||||
|
return Err("config server client is stopping".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let inst_name = cfg.get_inst_name();
|
||||||
|
let inst_id = cfg.get_id();
|
||||||
|
|
||||||
|
self.validate_instance_name(&inst_name, inst_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_run_network_instance(&self, id: &Uuid) -> Result<(), String> {
|
||||||
|
let _delivery_guard = self
|
||||||
|
.callback_delivery
|
||||||
|
.lock()
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
let Some(inst_name) = INSTANCE_MANAGER.get_instance_name(id) else {
|
||||||
|
if !self.stopping.load(Ordering::Acquire) {
|
||||||
|
return Err(format!("instance {} not found after start", id));
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let _mutation_guard = INSTANCE_MUTATION_LOCK
|
||||||
|
.lock()
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
if INSTANCE_MANAGER.get_instance_name(id).is_none() {
|
||||||
|
if !self.stopping.load(Ordering::Acquire) {
|
||||||
|
return Err(format!("instance {} not found after start", id));
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let should_delete = {
|
||||||
|
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
|
||||||
|
if self.stopping.load(Ordering::Acquire) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
guard.insert(*id);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_delete {
|
||||||
|
if let Err(err) = INSTANCE_MANAGER.delete_network_instance(vec![*id]) {
|
||||||
|
return Err(err.to_string());
|
||||||
|
}
|
||||||
|
remove_instance_name_ids(&[*id]);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.stopping.load(Ordering::Acquire) {
|
||||||
|
self.remove_tracked_instance_ids(&[*id])?;
|
||||||
|
remove_instance_name_ids(&[*id]);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = self.commit_instance_name(inst_name.clone(), *id) {
|
||||||
|
self.remove_tracked_instance_ids(&[*id])?;
|
||||||
|
if let Err(delete_err) = INSTANCE_MANAGER.delete_network_instance(vec![*id]) {
|
||||||
|
return Err(format!(
|
||||||
|
"{}; failed to delete duplicate instance: {}",
|
||||||
|
err, delete_err
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.stopping.load(Ordering::Acquire) {
|
||||||
|
self.remove_tracked_instance_ids(&[*id])?;
|
||||||
|
remove_instance_name_ids(&[*id]);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if INSTANCE_MANAGER.get_instance_name(id).is_none() {
|
||||||
|
self.remove_tracked_instance_ids(&[*id])?;
|
||||||
|
remove_instance_name_ids(&[*id]);
|
||||||
|
return Err(format!(
|
||||||
|
"instance {} was removed before post-run completed",
|
||||||
|
id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_data_plane_handles_by_instance_ids(&[*id]);
|
||||||
|
|
||||||
|
if let Err(err) = self.emit_event_with_delivery_locked("run_network_instance", *id) {
|
||||||
|
self.note_callback_error(err);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
|
||||||
|
let removed_ids = {
|
||||||
|
let _mutation_guard = INSTANCE_MUTATION_LOCK
|
||||||
|
.lock()
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
let removed_ids = self.remove_tracked_instance_ids(ids)?;
|
||||||
|
remove_instance_name_ids(ids);
|
||||||
|
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||||
|
removed_ids
|
||||||
|
};
|
||||||
|
|
||||||
|
for id in removed_ids {
|
||||||
|
if let Err(err) = self.emit_event("delete_network_instance", id) {
|
||||||
|
self.note_callback_error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_config_server_tracked_instance_ids(ids: &[Uuid]) {
|
||||||
|
if ids.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(guard) = CONFIG_SERVER_CLIENT.lock()
|
||||||
|
&& let Some(managed) = guard.as_ref()
|
||||||
|
&& let Err(err) = managed.hooks.remove_tracked_instance_ids(ids)
|
||||||
|
{
|
||||||
|
log::warn!("failed to remove config server tracked ids: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn wait_for_config_server_delivery() {
|
||||||
|
let hooks = CONFIG_SERVER_CLIENT
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|guard| guard.as_ref().map(|managed| managed.hooks.clone()));
|
||||||
|
if let Some(hooks) = hooks {
|
||||||
|
hooks.wait_for_callback_delivery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn last_callback_error() -> Option<String> {
|
||||||
|
LAST_CONFIG_SERVER_CALLBACK_ERROR
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|guard| guard.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear_last_callback_error() {
|
||||||
|
if let Ok(mut guard) = LAST_CONFIG_SERVER_CALLBACK_ERROR.lock() {
|
||||||
|
*guard = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) fn is_config_server_active_or_stopping() -> bool {
|
||||||
|
CONFIG_SERVER_CLIENT_ACTIVE.load(Ordering::Acquire)
|
||||||
|
|| CONFIG_SERVER_CLIENT_STOPPING.load(Ordering::Acquire)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn set_active_for_test(active: bool) {
|
||||||
|
CONFIG_SERVER_CLIENT_ACTIVE.store(active, Ordering::Release);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Start the config server client.
|
||||||
|
///
|
||||||
|
/// `config_server_url` must be a valid null-terminated UTF-8 string.
|
||||||
|
/// `hostname` may be null; if non-null it must be a valid null-terminated UTF-8 string.
|
||||||
|
/// `machine_id` must be a valid null-terminated UTF-8 string.
|
||||||
|
/// `event_json` passed to `callback` is valid only during that callback invocation.
|
||||||
|
pub(crate) unsafe fn start_config_server_client(
|
||||||
|
config_server_url: *const c_char,
|
||||||
|
hostname: *const c_char,
|
||||||
|
machine_id: *const c_char,
|
||||||
|
secure_mode: bool,
|
||||||
|
callback: ConfigServerEventCallback,
|
||||||
|
user_data: *mut c_void,
|
||||||
|
) -> c_int {
|
||||||
|
if in_config_server_callback() {
|
||||||
|
set_error_msg("cannot start config server client from config server callback");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_server_url = match unsafe { c_str_to_string(config_server_url, "config_server_url") }
|
||||||
|
{
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let hostname = match unsafe { optional_c_str_to_string(hostname, "hostname") } {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let machine_id = match unsafe { c_str_to_string(machine_id, "machine_id") } {
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
Ok(value) => value,
|
||||||
|
};
|
||||||
|
if let Err(err) = validate_config_server_client_options(&config_server_url, &machine_id) {
|
||||||
|
set_error_msg(&err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut guard = match CONFIG_SERVER_CLIENT.lock() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&format!("failed to lock config server client: {}", err));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if guard.is_some() {
|
||||||
|
set_error_msg("config server client already exists");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if CONFIG_SERVER_CLIENT_STOPPING.load(Ordering::Acquire) {
|
||||||
|
set_error_msg("config server client is stopping");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
clear_last_callback_error();
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
let _data_plane_usage_guard = match crate::data_plane::lock_for_config_server_start() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&err);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CONFIG_SERVER_CLIENT_ACTIVE.store(true, Ordering::Release);
|
||||||
|
let hooks = Arc::new(ManagedConfigServerClientHooks::new(callback, user_data));
|
||||||
|
let client = match ASYNC_RUNTIME.block_on(run_web_client(
|
||||||
|
&config_server_url,
|
||||||
|
config_server_machine_id_options(machine_id),
|
||||||
|
hostname,
|
||||||
|
secure_mode,
|
||||||
|
INSTANCE_MANAGER.clone(),
|
||||||
|
Some(hooks.clone()),
|
||||||
|
)) {
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(err) => {
|
||||||
|
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||||
|
set_error_msg(&format!("failed to start config server client: {}", err));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
*guard = Some(ManagedConfigServerClient { client, hooks });
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stop_config_server_client() -> c_int {
|
||||||
|
if in_config_server_callback() {
|
||||||
|
set_error_msg("cannot stop config server client from config server callback");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut guard = match CONFIG_SERVER_CLIENT.lock() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&format!("failed to lock config server client: {}", err));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(managed) = guard.as_ref() else {
|
||||||
|
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
if CONFIG_SERVER_CLIENT_STOPPING.swap(true, Ordering::AcqRel) {
|
||||||
|
set_error_msg("config server client is stopping");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
let hooks = managed.hooks.clone();
|
||||||
|
let managed = guard.take().expect("config server client exists");
|
||||||
|
drop(guard);
|
||||||
|
|
||||||
|
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||||
|
let tracked_ids = hooks.start_stopping();
|
||||||
|
drop(managed);
|
||||||
|
|
||||||
|
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(err) => {
|
||||||
|
hooks.wait_for_callback_delivery();
|
||||||
|
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||||
|
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
|
||||||
|
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let delete_result = INSTANCE_MANAGER.delete_network_instance(tracked_ids.clone());
|
||||||
|
if delete_result.is_ok() {
|
||||||
|
remove_instance_name_ids(&tracked_ids);
|
||||||
|
remove_data_plane_handles_by_instance_ids(&tracked_ids);
|
||||||
|
}
|
||||||
|
drop(_mutation_guard);
|
||||||
|
hooks.wait_for_callback_delivery();
|
||||||
|
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||||
|
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
|
||||||
|
|
||||||
|
if let Err(err) = delete_result {
|
||||||
|
set_error_msg(&format!(
|
||||||
|
"failed to delete config server instances: {}",
|
||||||
|
err
|
||||||
|
));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_config_server_client_connected() -> c_int {
|
||||||
|
CONFIG_SERVER_CLIENT
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|guard| guard.as_ref().map(|managed| managed.client.is_connected()))
|
||||||
|
.map(i32::from)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,854 @@
|
|||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
use std::{
|
||||||
|
future::Future,
|
||||||
|
net::{IpAddr, SocketAddr},
|
||||||
|
sync::{
|
||||||
|
Arc, RwLock,
|
||||||
|
atomic::{AtomicU64, Ordering},
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
use dashmap::DashMap;
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
use easytier::launcher::{DataPlaneTcpListener, DataPlaneTcpStream, DataPlaneUdpSocket};
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf};
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
use crate::{
|
||||||
|
config_server::{in_config_server_callback, is_config_server_active_or_stopping},
|
||||||
|
error::{free_string, set_error_msg},
|
||||||
|
state::{INSTANCE_MANAGER, INSTANCE_NAME_ID_MAP},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
static NEXT_DATA_PLANE_HANDLE: AtomicU64 = AtomicU64::new(1);
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
static DATA_PLANE_HANDLES: once_cell::sync::Lazy<DashMap<u64, DataPlaneHandle>> =
|
||||||
|
once_cell::sync::Lazy::new(DashMap::new);
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
static DATA_PLANE_USAGE_LOCK: once_cell::sync::Lazy<RwLock<()>> =
|
||||||
|
once_cell::sync::Lazy::new(|| RwLock::new(()));
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
struct DataPlaneHandle {
|
||||||
|
instance_id: uuid::Uuid,
|
||||||
|
runtime: tokio::runtime::Handle,
|
||||||
|
// Cancelled by close() to wake any in-flight op on this handle.
|
||||||
|
close_token: CancellationToken,
|
||||||
|
resource: DataPlaneResource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
struct TcpHalves {
|
||||||
|
read: tokio::sync::Mutex<ReadHalf<DataPlaneTcpStream>>,
|
||||||
|
write: tokio::sync::Mutex<WriteHalf<DataPlaneTcpStream>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
enum DataPlaneResource {
|
||||||
|
Tcp(Arc<TcpHalves>),
|
||||||
|
TcpListener(Arc<tokio::sync::Mutex<DataPlaneTcpListener>>),
|
||||||
|
Udp(Arc<DataPlaneUdpSocket>),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Several helper functions for FFI data plane operations to facilitate logic reuse.
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
fn next_handle() -> u64 {
|
||||||
|
NEXT_DATA_PLANE_HANDLE.fetch_add(1, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
fn timeout_duration(timeout_ms: u64) -> Duration {
|
||||||
|
Duration::from_millis(timeout_ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
unsafe fn cstr_to_string(ptr: *const std::ffi::c_char, name: &str) -> Option<String> {
|
||||||
|
if ptr.is_null() {
|
||||||
|
set_error_msg(&format!("{} is null", name));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
fn get_instance_id(inst_name: &str) -> Option<uuid::Uuid> {
|
||||||
|
INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id.value())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
fn parse_socket_addr(host: &str, port: u16) -> Option<SocketAddr> {
|
||||||
|
let ip = match host.parse::<IpAddr>() {
|
||||||
|
Ok(ip) => ip,
|
||||||
|
Err(e) => {
|
||||||
|
set_error_msg(&format!("failed to parse ip address: {}", e));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(SocketAddr::new(ip, port))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode an IP address for FFI return. Returns `*mut c_char` to match
|
||||||
|
/// `CString::into_raw`; caller releases it via `free_string`.
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
fn into_ffi_ip_cstring(ip: IpAddr) -> Option<*mut std::ffi::c_char> {
|
||||||
|
match std::ffi::CString::new(ip.to_string()) {
|
||||||
|
Ok(s) => Some(s.into_raw()),
|
||||||
|
Err(e) => {
|
||||||
|
set_error_msg(&format!("failed to encode ip: {}", e));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
fn get_runtime_handle(
|
||||||
|
inst_id: &uuid::Uuid,
|
||||||
|
deadline: std::time::Instant,
|
||||||
|
) -> Option<tokio::runtime::Handle> {
|
||||||
|
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||||
|
let Some(rt) = INSTANCE_MANAGER.data_plane_wait_runtime_handle(inst_id, remaining) else {
|
||||||
|
set_error_msg("instance runtime is not ready");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(rt)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
fn insert_tcp_stream_handle(
|
||||||
|
instance_id: uuid::Uuid,
|
||||||
|
runtime: tokio::runtime::Handle,
|
||||||
|
stream: DataPlaneTcpStream,
|
||||||
|
) -> u64 {
|
||||||
|
let (rd, wr) = tokio::io::split(stream);
|
||||||
|
let handle = next_handle();
|
||||||
|
DATA_PLANE_HANDLES.insert(
|
||||||
|
handle,
|
||||||
|
DataPlaneHandle {
|
||||||
|
instance_id,
|
||||||
|
runtime,
|
||||||
|
close_token: CancellationToken::new(),
|
||||||
|
resource: DataPlaneResource::Tcp(Arc::new(TcpHalves {
|
||||||
|
read: tokio::sync::Mutex::new(rd),
|
||||||
|
write: tokio::sync::Mutex::new(wr),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
fn get_tcp_stream(
|
||||||
|
handle: u64,
|
||||||
|
) -> Option<(Arc<TcpHalves>, tokio::runtime::Handle, CancellationToken)> {
|
||||||
|
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
|
||||||
|
set_error_msg("tcp stream handle not found");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
match &h.resource {
|
||||||
|
DataPlaneResource::Tcp(halves) => {
|
||||||
|
Some((halves.clone(), h.runtime.clone(), h.close_token.clone()))
|
||||||
|
}
|
||||||
|
DataPlaneResource::TcpListener(_) | DataPlaneResource::Udp(_) => {
|
||||||
|
set_error_msg("handle is not a tcp stream");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
fn get_tcp_listener(
|
||||||
|
handle: u64,
|
||||||
|
) -> Option<(
|
||||||
|
Arc<tokio::sync::Mutex<DataPlaneTcpListener>>,
|
||||||
|
tokio::runtime::Handle,
|
||||||
|
CancellationToken,
|
||||||
|
uuid::Uuid,
|
||||||
|
)> {
|
||||||
|
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
|
||||||
|
set_error_msg("tcp listener handle not found");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
match &h.resource {
|
||||||
|
DataPlaneResource::TcpListener(listener) => Some((
|
||||||
|
listener.clone(),
|
||||||
|
h.runtime.clone(),
|
||||||
|
h.close_token.clone(),
|
||||||
|
h.instance_id,
|
||||||
|
)),
|
||||||
|
DataPlaneResource::Tcp(_) | DataPlaneResource::Udp(_) => {
|
||||||
|
set_error_msg("handle is not a tcp listener");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
fn get_udp_socket(
|
||||||
|
handle: u64,
|
||||||
|
) -> Option<(
|
||||||
|
Arc<DataPlaneUdpSocket>,
|
||||||
|
tokio::runtime::Handle,
|
||||||
|
CancellationToken,
|
||||||
|
)> {
|
||||||
|
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
|
||||||
|
set_error_msg("udp socket handle not found");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
match &h.resource {
|
||||||
|
DataPlaneResource::Udp(socket) => {
|
||||||
|
Some((socket.clone(), h.runtime.clone(), h.close_token.clone()))
|
||||||
|
}
|
||||||
|
DataPlaneResource::Tcp(_) | DataPlaneResource::TcpListener(_) => {
|
||||||
|
set_error_msg("handle is not a udp socket");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) fn remove_data_plane_handles_by_instance_ids(ids: &[Uuid]) {
|
||||||
|
if ids.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DATA_PLANE_HANDLES.retain(|_, handle| {
|
||||||
|
if ids.contains(&handle.instance_id) {
|
||||||
|
handle.close_token.cancel();
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "ffi-dataplane"))]
|
||||||
|
pub(crate) fn remove_data_plane_handles_by_instance_ids(_ids: &[uuid::Uuid]) {}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
fn data_plane_rejected() -> bool {
|
||||||
|
if in_config_server_callback() {
|
||||||
|
set_error_msg("cannot use data plane from config server callback");
|
||||||
|
true
|
||||||
|
} else if is_config_server_active_or_stopping() {
|
||||||
|
set_error_msg("cannot use data plane while config server client is active");
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
fn enter_data_plane_operation() -> Option<std::sync::RwLockReadGuard<'static, ()>> {
|
||||||
|
if data_plane_rejected() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let guard = match DATA_PLANE_USAGE_LOCK.read() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&format!("failed to lock data plane usage: {}", err));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if data_plane_rejected() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(guard)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run an IO op on the resource's owning runtime, supporting
|
||||||
|
/// timeout and cancellation.
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
async fn run_with_cancel<T, F>(
|
||||||
|
close_token: &CancellationToken,
|
||||||
|
timeout_ms: u64,
|
||||||
|
error_prefix: &str,
|
||||||
|
op: F,
|
||||||
|
) -> Option<Result<T, std::io::Error>>
|
||||||
|
where
|
||||||
|
F: Future<Output = Result<T, std::io::Error>>,
|
||||||
|
{
|
||||||
|
tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = close_token.cancelled() => {
|
||||||
|
set_error_msg(&format!("{}: handle closed", error_prefix));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
res = tokio::time::timeout(timeout_duration(timeout_ms), op) => match res {
|
||||||
|
Ok(r) => Some(r),
|
||||||
|
Err(_) => {
|
||||||
|
set_error_msg(&format!("{} timed out", error_prefix));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) fn lock_for_config_server_start()
|
||||||
|
-> Result<std::sync::RwLockWriteGuard<'static, ()>, String> {
|
||||||
|
let guard = DATA_PLANE_USAGE_LOCK
|
||||||
|
.write()
|
||||||
|
.map_err(|err| format!("failed to lock data plane usage: {}", err))?;
|
||||||
|
if !DATA_PLANE_HANDLES.is_empty() {
|
||||||
|
return Err("cannot start config server client while data plane is in use".to_string());
|
||||||
|
}
|
||||||
|
Ok(guard)
|
||||||
|
}
|
||||||
|
/// # Safety
|
||||||
|
/// Open a TCP stream through an EasyTier instance data plane. Returns 0 on
|
||||||
|
/// failure. On success, writes the local socket address chosen for this
|
||||||
|
/// connection into `out_local_ip` (a heap-allocated C string the caller must
|
||||||
|
/// release via `free_string`) and `out_local_port`. Both out pointers must be
|
||||||
|
/// non-null.
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) unsafe fn data_plane_tcp_connect(
|
||||||
|
inst_name: *const std::ffi::c_char,
|
||||||
|
dst_ip: *const std::ffi::c_char,
|
||||||
|
dst_port: std::ffi::c_ushort,
|
||||||
|
timeout_ms: u64,
|
||||||
|
out_local_ip: *mut *const std::ffi::c_char,
|
||||||
|
out_local_port: *mut std::ffi::c_ushort,
|
||||||
|
) -> u64 {
|
||||||
|
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||||
|
Some(guard) => guard,
|
||||||
|
None => return 0,
|
||||||
|
};
|
||||||
|
if out_local_ip.is_null() || out_local_port.is_null() {
|
||||||
|
set_error_msg("output pointer is null");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let Some(dst_ip) = (unsafe { cstr_to_string(dst_ip, "dst_ip") }) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let Some(inst_id) = get_instance_id(&inst_name) else {
|
||||||
|
set_error_msg("instance not found");
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let Some(dst_addr) = parse_socket_addr(&dst_ip, dst_port) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
|
||||||
|
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||||
|
let result =
|
||||||
|
runtime.block_on(INSTANCE_MANAGER.data_plane_tcp_connect(&inst_id, dst_addr, remaining));
|
||||||
|
match result {
|
||||||
|
Ok(stream) => {
|
||||||
|
let local_addr = stream.local_addr();
|
||||||
|
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let handle = insert_tcp_stream_handle(inst_id, runtime, stream);
|
||||||
|
unsafe {
|
||||||
|
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||||
|
*out_local_port = local_addr.port();
|
||||||
|
}
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
set_error_msg(&format!("failed to connect tcp data plane: {}", e));
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Bind a TCP listener through an EasyTier instance data plane. Returns 0 on
|
||||||
|
/// failure. The local address actually bound is written into `out_local_ip` /
|
||||||
|
/// `out_local_port`; the caller must release `*out_local_ip` via `free_string`.
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) unsafe fn data_plane_tcp_bind(
|
||||||
|
inst_name: *const std::ffi::c_char,
|
||||||
|
local_port: std::ffi::c_ushort,
|
||||||
|
timeout_ms: u64,
|
||||||
|
out_local_ip: *mut *const std::ffi::c_char,
|
||||||
|
out_local_port: *mut std::ffi::c_ushort,
|
||||||
|
) -> u64 {
|
||||||
|
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||||
|
Some(guard) => guard,
|
||||||
|
None => return 0,
|
||||||
|
};
|
||||||
|
if out_local_ip.is_null() || out_local_port.is_null() {
|
||||||
|
set_error_msg("output pointer is null");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let Some(inst_id) = get_instance_id(&inst_name) else {
|
||||||
|
set_error_msg("instance not found");
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
|
||||||
|
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||||
|
let result =
|
||||||
|
runtime.block_on(INSTANCE_MANAGER.data_plane_tcp_bind(&inst_id, local_port, remaining));
|
||||||
|
match result {
|
||||||
|
Ok(listener) => {
|
||||||
|
let local_addr = listener.local_addr();
|
||||||
|
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let handle = next_handle();
|
||||||
|
DATA_PLANE_HANDLES.insert(
|
||||||
|
handle,
|
||||||
|
DataPlaneHandle {
|
||||||
|
instance_id: inst_id,
|
||||||
|
runtime,
|
||||||
|
close_token: CancellationToken::new(),
|
||||||
|
resource: DataPlaneResource::TcpListener(Arc::new(tokio::sync::Mutex::new(
|
||||||
|
listener,
|
||||||
|
))),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
unsafe {
|
||||||
|
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||||
|
*out_local_port = local_addr.port();
|
||||||
|
}
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
set_error_msg(&format!("failed to bind tcp data plane: {}", e));
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Accept one connection from a TCP data-plane listener. Returns a TCP stream
|
||||||
|
/// handle, or 0 on failure. Local and peer addresses are written into out
|
||||||
|
/// parameters; returned IP strings must be released via `free_string`.
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) unsafe fn data_plane_tcp_accept(
|
||||||
|
handle: u64,
|
||||||
|
timeout_ms: u64,
|
||||||
|
out_local_ip: *mut *const std::ffi::c_char,
|
||||||
|
out_local_port: *mut std::ffi::c_ushort,
|
||||||
|
out_peer_ip: *mut *const std::ffi::c_char,
|
||||||
|
out_peer_port: *mut std::ffi::c_ushort,
|
||||||
|
) -> u64 {
|
||||||
|
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||||
|
Some(guard) => guard,
|
||||||
|
None => return 0,
|
||||||
|
};
|
||||||
|
if out_local_ip.is_null()
|
||||||
|
|| out_local_port.is_null()
|
||||||
|
|| out_peer_ip.is_null()
|
||||||
|
|| out_peer_port.is_null()
|
||||||
|
{
|
||||||
|
set_error_msg("output pointer is null");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let Some((listener, runtime, close_token, instance_id)) = get_tcp_listener(handle) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ret = runtime.block_on(async move {
|
||||||
|
let mut listener = listener.lock().await;
|
||||||
|
run_with_cancel(
|
||||||
|
&close_token,
|
||||||
|
timeout_ms,
|
||||||
|
"tcp data plane accept",
|
||||||
|
listener.accept(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
|
||||||
|
match ret {
|
||||||
|
Some(Ok((stream, peer_addr))) => {
|
||||||
|
let local_addr = stream.local_addr();
|
||||||
|
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let Some(peer_ip) = into_ffi_ip_cstring(peer_addr.ip()) else {
|
||||||
|
free_string(local_ip);
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let stream_handle = insert_tcp_stream_handle(instance_id, runtime, stream);
|
||||||
|
unsafe {
|
||||||
|
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||||
|
*out_local_port = local_addr.port();
|
||||||
|
*out_peer_ip = peer_ip as *const std::ffi::c_char;
|
||||||
|
*out_peer_port = peer_addr.port();
|
||||||
|
}
|
||||||
|
stream_handle
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
set_error_msg(&format!("failed to accept tcp data plane: {}", e));
|
||||||
|
0
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Read from a TCP data-plane stream.
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) unsafe fn data_plane_tcp_read(
|
||||||
|
handle: u64,
|
||||||
|
buf: *mut std::ffi::c_uchar,
|
||||||
|
len: u32,
|
||||||
|
timeout_ms: u64,
|
||||||
|
) -> std::ffi::c_int {
|
||||||
|
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||||
|
Some(guard) => guard,
|
||||||
|
None => return -1,
|
||||||
|
};
|
||||||
|
if buf.is_null() {
|
||||||
|
set_error_msg("buf is null");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
let Some((halves, runtime, close_token)) = get_tcp_stream(handle) else {
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
// Safety: caller-owned buffer outlives this blocking call.
|
||||||
|
let buf = unsafe { std::slice::from_raw_parts_mut(buf, len as usize) };
|
||||||
|
runtime.block_on(async move {
|
||||||
|
let mut rd = halves.read.lock().await;
|
||||||
|
match run_with_cancel(
|
||||||
|
&close_token,
|
||||||
|
timeout_ms,
|
||||||
|
"failed to read tcp data plane",
|
||||||
|
rd.read(buf),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Some(Ok(n)) => n as std::ffi::c_int,
|
||||||
|
Some(Err(e)) => {
|
||||||
|
set_error_msg(&format!("failed to read tcp data plane: {}", e));
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
None => -1,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Write to a TCP data-plane stream.
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) unsafe fn data_plane_tcp_write(
|
||||||
|
handle: u64,
|
||||||
|
buf: *const std::ffi::c_uchar,
|
||||||
|
len: u32,
|
||||||
|
timeout_ms: u64,
|
||||||
|
) -> std::ffi::c_int {
|
||||||
|
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||||
|
Some(guard) => guard,
|
||||||
|
None => return -1,
|
||||||
|
};
|
||||||
|
if buf.is_null() {
|
||||||
|
set_error_msg("buf is null");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
let Some((halves, runtime, close_token)) = get_tcp_stream(handle) else {
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
let total = len as usize;
|
||||||
|
// Safety: caller-owned buffer outlives this blocking call.
|
||||||
|
let buf = unsafe { std::slice::from_raw_parts(buf, total) };
|
||||||
|
runtime.block_on(async move {
|
||||||
|
let mut wr = halves.write.lock().await;
|
||||||
|
// Use `write_all` to honor `net.Conn::Write` semantics on the Go side
|
||||||
|
// (must write everything or return an error); single `write()` can
|
||||||
|
// silently short-write and corrupt streams that the caller assumes are
|
||||||
|
// fully written.
|
||||||
|
match run_with_cancel(
|
||||||
|
&close_token,
|
||||||
|
timeout_ms,
|
||||||
|
"failed to write tcp data plane",
|
||||||
|
wr.write_all(buf),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Some(Ok(())) => total as std::ffi::c_int,
|
||||||
|
Some(Err(e)) => {
|
||||||
|
set_error_msg(&format!("failed to write tcp data plane: {}", e));
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
None => -1,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) fn data_plane_tcp_close(handle: u64) -> std::ffi::c_int {
|
||||||
|
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||||
|
Some(guard) => guard,
|
||||||
|
None => return -1,
|
||||||
|
};
|
||||||
|
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
|
||||||
|
matches!(e.resource, DataPlaneResource::Tcp(_))
|
||||||
|
}) else {
|
||||||
|
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
|
||||||
|
"handle is not a tcp stream"
|
||||||
|
} else {
|
||||||
|
"tcp stream handle not found"
|
||||||
|
});
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
h.close_token.cancel();
|
||||||
|
if let DataPlaneResource::Tcp(halves) = h.resource {
|
||||||
|
// Best-effort half-close; if write half is in use, the in-flight call
|
||||||
|
// observes the cancel token and releases the lock shortly after.
|
||||||
|
h.runtime.spawn(async move {
|
||||||
|
if let Ok(mut wr) = halves.write.try_lock() {
|
||||||
|
let _ = wr.shutdown().await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) fn data_plane_tcp_listener_close(handle: u64) -> std::ffi::c_int {
|
||||||
|
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||||
|
Some(guard) => guard,
|
||||||
|
None => return -1,
|
||||||
|
};
|
||||||
|
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
|
||||||
|
matches!(e.resource, DataPlaneResource::TcpListener(_))
|
||||||
|
}) else {
|
||||||
|
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
|
||||||
|
"handle is not a tcp listener"
|
||||||
|
} else {
|
||||||
|
"tcp listener handle not found"
|
||||||
|
});
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
h.close_token.cancel();
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Bind a UDP socket through an EasyTier instance data plane. Returns 0 on
|
||||||
|
/// failure. The local address actually bound (which may differ from the
|
||||||
|
/// requested port when `local_port == 0`) is written into `out_local_ip` /
|
||||||
|
/// `out_local_port`; the caller must release `*out_local_ip` via `free_string`.
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) unsafe fn data_plane_udp_bind(
|
||||||
|
inst_name: *const std::ffi::c_char,
|
||||||
|
local_port: std::ffi::c_ushort,
|
||||||
|
timeout_ms: u64,
|
||||||
|
out_local_ip: *mut *const std::ffi::c_char,
|
||||||
|
out_local_port: *mut std::ffi::c_ushort,
|
||||||
|
) -> u64 {
|
||||||
|
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||||
|
Some(guard) => guard,
|
||||||
|
None => return 0,
|
||||||
|
};
|
||||||
|
if out_local_ip.is_null() || out_local_port.is_null() {
|
||||||
|
set_error_msg("output pointer is null");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let Some(inst_id) = get_instance_id(&inst_name) else {
|
||||||
|
set_error_msg("instance not found");
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
|
||||||
|
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||||
|
let result =
|
||||||
|
runtime.block_on(INSTANCE_MANAGER.data_plane_udp_bind(&inst_id, local_port, remaining));
|
||||||
|
match result {
|
||||||
|
Ok(socket) => {
|
||||||
|
let local_addr = socket.local_addr();
|
||||||
|
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let handle = next_handle();
|
||||||
|
DATA_PLANE_HANDLES.insert(
|
||||||
|
handle,
|
||||||
|
DataPlaneHandle {
|
||||||
|
instance_id: inst_id,
|
||||||
|
runtime,
|
||||||
|
close_token: CancellationToken::new(),
|
||||||
|
resource: DataPlaneResource::Udp(Arc::new(socket)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
unsafe {
|
||||||
|
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||||
|
*out_local_port = local_addr.port();
|
||||||
|
}
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
set_error_msg(&format!("failed to bind udp data plane: {}", e));
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Send a datagram through a UDP data-plane socket.
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) unsafe fn data_plane_udp_send_to(
|
||||||
|
handle: u64,
|
||||||
|
dst_ip: *const std::ffi::c_char,
|
||||||
|
dst_port: std::ffi::c_ushort,
|
||||||
|
buf: *const std::ffi::c_uchar,
|
||||||
|
len: u32,
|
||||||
|
timeout_ms: u64,
|
||||||
|
) -> std::ffi::c_int {
|
||||||
|
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||||
|
Some(guard) => guard,
|
||||||
|
None => return -1,
|
||||||
|
};
|
||||||
|
if buf.is_null() {
|
||||||
|
set_error_msg("buf is null");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
let Some(dst_ip) = (unsafe { cstr_to_string(dst_ip, "dst_ip") }) else {
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
let Some(dst_addr) = parse_socket_addr(&dst_ip, dst_port) else {
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
let Some((socket, runtime, close_token)) = get_udp_socket(handle) else {
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
let total = len as usize;
|
||||||
|
// Safety: caller-owned buffer outlives this blocking call.
|
||||||
|
let buf = unsafe { std::slice::from_raw_parts(buf, total) };
|
||||||
|
runtime.block_on(async move {
|
||||||
|
match run_with_cancel(
|
||||||
|
&close_token,
|
||||||
|
timeout_ms,
|
||||||
|
"failed to send udp data plane",
|
||||||
|
socket.send_to(buf, dst_addr),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Some(Ok(n)) => n as std::ffi::c_int,
|
||||||
|
Some(Err(e)) => {
|
||||||
|
set_error_msg(&format!("failed to send udp data plane: {}", e));
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
None => -1,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Receive a datagram from a UDP data-plane socket.
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) unsafe fn data_plane_udp_recv_from(
|
||||||
|
handle: u64,
|
||||||
|
buf: *mut std::ffi::c_uchar,
|
||||||
|
len: u32,
|
||||||
|
out_ip: *mut *const std::ffi::c_char,
|
||||||
|
out_port: *mut std::ffi::c_ushort,
|
||||||
|
timeout_ms: u64,
|
||||||
|
) -> std::ffi::c_int {
|
||||||
|
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||||
|
Some(guard) => guard,
|
||||||
|
None => return -1,
|
||||||
|
};
|
||||||
|
if buf.is_null() || out_ip.is_null() || out_port.is_null() {
|
||||||
|
set_error_msg("output pointer is null");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
let Some((socket, runtime, close_token)) = get_udp_socket(handle) else {
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
let total = len as usize;
|
||||||
|
// Safety: caller-owned buffer outlives this blocking call.
|
||||||
|
let buf = unsafe { std::slice::from_raw_parts_mut(buf, total) };
|
||||||
|
let ret = runtime.block_on(run_with_cancel(
|
||||||
|
&close_token,
|
||||||
|
timeout_ms,
|
||||||
|
"udp data plane receive",
|
||||||
|
socket.recv_from(buf),
|
||||||
|
));
|
||||||
|
|
||||||
|
match ret {
|
||||||
|
Some(Ok((n, addr))) => {
|
||||||
|
// The returned ip pointer must be released by the caller via
|
||||||
|
// `free_string` (which calls `CString::from_raw`, matching
|
||||||
|
// `CString::into_raw` here).
|
||||||
|
let Some(ip_cstr) = into_ffi_ip_cstring(addr.ip()) else {
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
*out_ip = ip_cstr as *const std::ffi::c_char;
|
||||||
|
*out_port = addr.port() as std::ffi::c_ushort;
|
||||||
|
}
|
||||||
|
n as std::ffi::c_int
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
set_error_msg(&format!("failed to receive udp data plane: {}", e));
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
None => -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
pub(crate) fn data_plane_udp_close(handle: u64) -> std::ffi::c_int {
|
||||||
|
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||||
|
Some(guard) => guard,
|
||||||
|
None => return -1,
|
||||||
|
};
|
||||||
|
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
|
||||||
|
matches!(e.resource, DataPlaneResource::Udp(_))
|
||||||
|
}) else {
|
||||||
|
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
|
||||||
|
"handle is not a udp socket"
|
||||||
|
} else {
|
||||||
|
"udp socket handle not found"
|
||||||
|
});
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
h.close_token.cancel();
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "ffi-dataplane"))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::{sync::mpsc, time::Duration};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_server_start_waits_for_data_plane_operation() {
|
||||||
|
let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap();
|
||||||
|
let (done_tx, done_rx) = mpsc::channel();
|
||||||
|
let waiter = std::thread::spawn(move || {
|
||||||
|
let _write_guard = lock_for_config_server_start().unwrap();
|
||||||
|
done_tx.send(()).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
|
||||||
|
drop(read_guard);
|
||||||
|
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||||
|
waiter.join().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
ffi::{CString, c_char},
|
||||||
|
};
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
// # Thread Safety
|
||||||
|
// set_error_msg and get_error_msg must be called on the same thread to
|
||||||
|
// get correct error. And since `Handle::block_on` polls the top-level
|
||||||
|
// future on the calling thread, set_error_msg always runs on the same
|
||||||
|
// thread as the corresponding get_error_msg.
|
||||||
|
static ERROR_MSG: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_error_msg(msg: &str) {
|
||||||
|
ERROR_MSG.with(|cell| {
|
||||||
|
let mut buf = cell.borrow_mut();
|
||||||
|
buf.clear();
|
||||||
|
buf.extend_from_slice(msg.as_bytes());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn thread_local_error_msg() -> Option<String> {
|
||||||
|
ERROR_MSG.with(|cell| {
|
||||||
|
let buf = cell.borrow();
|
||||||
|
if buf.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(String::from_utf8_lossy(&buf).into_owned())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) unsafe fn get_error_msg(out: *mut *const c_char) {
|
||||||
|
let msg = match (
|
||||||
|
thread_local_error_msg(),
|
||||||
|
crate::config_server::last_callback_error(),
|
||||||
|
) {
|
||||||
|
(Some(error), Some(callback_error)) => Some(format!(
|
||||||
|
"{}; config server callback error: {}",
|
||||||
|
error, callback_error
|
||||||
|
)),
|
||||||
|
(Some(error), None) => Some(error),
|
||||||
|
(None, Some(callback_error)) => {
|
||||||
|
Some(format!("config server callback error: {}", callback_error))
|
||||||
|
}
|
||||||
|
(None, None) => None,
|
||||||
|
};
|
||||||
|
let cstr = msg.and_then(|msg| CString::new(msg).ok());
|
||||||
|
unsafe {
|
||||||
|
*out = match cstr {
|
||||||
|
Some(s) => s.into_raw() as *const c_char,
|
||||||
|
None => std::ptr::null(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn free_string(s: *const c_char) {
|
||||||
|
if s.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
let _ = CString::from_raw(s as *mut c_char);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
use std::ffi::{c_char, c_int};
|
||||||
|
|
||||||
|
use easytier::common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config_server::{
|
||||||
|
in_config_server_callback, remove_config_server_tracked_instance_ids,
|
||||||
|
wait_for_config_server_delivery,
|
||||||
|
},
|
||||||
|
data_plane::remove_data_plane_handles_by_instance_ids,
|
||||||
|
error::set_error_msg,
|
||||||
|
state::{
|
||||||
|
INSTANCE_MANAGER, INSTANCE_MUTATION_LOCK, INSTANCE_NAME_ID_MAP, instance_name_exists,
|
||||||
|
lock_remote_instance_mutation,
|
||||||
|
},
|
||||||
|
types::KeyValuePair,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Set the tun fd
|
||||||
|
pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
|
||||||
|
let inst_name = unsafe {
|
||||||
|
assert!(!inst_name.is_null());
|
||||||
|
std::ffi::CStr::from_ptr(inst_name)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
};
|
||||||
|
if !INSTANCE_NAME_ID_MAP.contains_key(&inst_name) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inst_id = *INSTANCE_NAME_ID_MAP
|
||||||
|
.get(&inst_name)
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.value();
|
||||||
|
|
||||||
|
match INSTANCE_MANAGER.set_tun_fd(&inst_id, fd) {
|
||||||
|
Ok(_) => 0,
|
||||||
|
Err(_) => -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Parse the config
|
||||||
|
pub(crate) unsafe fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||||
|
let cfg_str = unsafe {
|
||||||
|
assert!(!cfg_str.is_null());
|
||||||
|
std::ffi::CStr::from_ptr(cfg_str)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = TomlConfigLoader::new_from_str(&cfg_str) {
|
||||||
|
set_error_msg(&format!("failed to parse config: {:?}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Run the network instance
|
||||||
|
pub(crate) unsafe fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||||
|
if in_config_server_callback() {
|
||||||
|
set_error_msg("cannot run network instance from config server callback");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cfg_str = unsafe {
|
||||||
|
assert!(!cfg_str.is_null());
|
||||||
|
std::ffi::CStr::from_ptr(cfg_str)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
};
|
||||||
|
let cfg = match TomlConfigLoader::new_from_str(&cfg_str) {
|
||||||
|
Ok(cfg) => cfg,
|
||||||
|
Err(e) => {
|
||||||
|
set_error_msg(&format!("failed to parse config: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let inst_name = cfg.get_inst_name();
|
||||||
|
|
||||||
|
wait_for_config_server_delivery();
|
||||||
|
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||||
|
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if instance_name_exists(&inst_name) {
|
||||||
|
set_error_msg("instance already exists");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance_id =
|
||||||
|
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(e) => {
|
||||||
|
set_error_msg(&format!("failed to start instance: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
INSTANCE_NAME_ID_MAP.insert(inst_name, instance_id);
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Retain the network instance
|
||||||
|
pub(crate) unsafe fn retain_network_instance(
|
||||||
|
inst_names: *const *const std::ffi::c_char,
|
||||||
|
length: usize,
|
||||||
|
) -> std::ffi::c_int {
|
||||||
|
if in_config_server_callback() {
|
||||||
|
set_error_msg("cannot retain network instances from config server callback");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_config_server_delivery();
|
||||||
|
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||||
|
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||||
|
Ok(guard) => guard,
|
||||||
|
Err(err) => {
|
||||||
|
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
let removed_ids = INSTANCE_MANAGER.list_network_instance_ids();
|
||||||
|
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
|
||||||
|
set_error_msg(&format!("failed to delete instances: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
remove_config_server_tracked_instance_ids(&removed_ids);
|
||||||
|
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||||
|
INSTANCE_NAME_ID_MAP.clear();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inst_names = unsafe {
|
||||||
|
assert!(!inst_names.is_null());
|
||||||
|
std::slice::from_raw_parts(inst_names, length)
|
||||||
|
.iter()
|
||||||
|
.map(|&name| {
|
||||||
|
assert!(!name.is_null());
|
||||||
|
std::ffi::CStr::from_ptr(name)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let removed_ids = INSTANCE_MANAGER
|
||||||
|
.list_network_instance_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|id| {
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.get_instance_name(id)
|
||||||
|
.is_none_or(|name| !inst_names.contains(&name))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
|
||||||
|
set_error_msg(&format!("failed to delete instances: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_config_server_tracked_instance_ids(&removed_ids);
|
||||||
|
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||||
|
INSTANCE_NAME_ID_MAP.retain(|k, _| inst_names.contains(k));
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Collect the network infos
|
||||||
|
pub(crate) unsafe fn collect_network_infos(
|
||||||
|
infos: *mut KeyValuePair,
|
||||||
|
max_length: usize,
|
||||||
|
) -> std::ffi::c_int {
|
||||||
|
if in_config_server_callback() {
|
||||||
|
set_error_msg("cannot collect network infos from config server callback");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if max_length == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let infos = unsafe {
|
||||||
|
assert!(!infos.is_null());
|
||||||
|
std::slice::from_raw_parts_mut(infos, max_length)
|
||||||
|
};
|
||||||
|
|
||||||
|
let collected_infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||||
|
Ok(infos) => infos,
|
||||||
|
Err(e) => {
|
||||||
|
set_error_msg(&format!("failed to collect network infos: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut index = 0;
|
||||||
|
for (instance_id, value) in collected_infos.iter() {
|
||||||
|
if index >= max_length {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let Some(key) = INSTANCE_MANAGER.get_instance_name(instance_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// convert value to json string
|
||||||
|
let value = match serde_json::to_string(&value) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(e) => {
|
||||||
|
set_error_msg(&format!("failed to serialize instance info: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
infos[index] = KeyValuePair {
|
||||||
|
key: std::ffi::CString::new(key).unwrap().into_raw(),
|
||||||
|
value: std::ffi::CString::new(value).unwrap().into_raw(),
|
||||||
|
};
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
index as std::ffi::c_int
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
|||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use easytier::instance_manager::NetworkInstanceManager;
|
||||||
|
use tokio::runtime::{Builder, Runtime};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub(crate) static INSTANCE_NAME_ID_MAP: once_cell::sync::Lazy<DashMap<String, Uuid>> =
|
||||||
|
once_cell::sync::Lazy::new(DashMap::new);
|
||||||
|
pub(crate) static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NetworkInstanceManager>> =
|
||||||
|
once_cell::sync::Lazy::new(|| Arc::new(NetworkInstanceManager::new()));
|
||||||
|
pub(crate) static ASYNC_RUNTIME: once_cell::sync::Lazy<Runtime> =
|
||||||
|
once_cell::sync::Lazy::new(|| {
|
||||||
|
Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("tokio runtime for easytier-ffi")
|
||||||
|
});
|
||||||
|
pub(crate) static INSTANCE_MUTATION_LOCK: once_cell::sync::Lazy<Mutex<()>> =
|
||||||
|
once_cell::sync::Lazy::new(|| Mutex::new(()));
|
||||||
|
|
||||||
|
pub(crate) fn remove_instance_name_ids(ids: &[Uuid]) {
|
||||||
|
if ids.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTANCE_NAME_ID_MAP.retain(|_, instance_id| !ids.contains(instance_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lock_remote_instance_mutation() -> tokio::sync::OwnedMutexGuard<()> {
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.remote_mutation_lock()
|
||||||
|
.blocking_lock_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn instance_name_exists(inst_name: &str) -> bool {
|
||||||
|
find_instance_id_by_name(inst_name).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn find_instance_id_by_name(inst_name: &str) -> Option<Uuid> {
|
||||||
|
INSTANCE_NAME_ID_MAP
|
||||||
|
.get(inst_name)
|
||||||
|
.map(|id| *id)
|
||||||
|
.or_else(|| {
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.list_network_instance_ids()
|
||||||
|
.into_iter()
|
||||||
|
.find(|id| {
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.get_instance_name(id)
|
||||||
|
.is_some_and(|name| name == inst_name)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
use std::ffi::{CStr, c_char};
|
||||||
|
|
||||||
|
pub(crate) unsafe fn c_str_to_string(ptr: *const c_char, name: &str) -> Result<String, String> {
|
||||||
|
if ptr.is_null() {
|
||||||
|
return Err(format!("{} is null", name));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe { CStr::from_ptr(ptr) }
|
||||||
|
.to_str()
|
||||||
|
.map(|value| value.to_string())
|
||||||
|
.map_err(|err| format!("{} is not valid UTF-8: {}", name, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) unsafe fn optional_c_str_to_string(
|
||||||
|
ptr: *const c_char,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Option<String>, String> {
|
||||||
|
if ptr.is_null() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe { c_str_to_string(ptr, name) }.map(Some)
|
||||||
|
}
|
||||||
@@ -0,0 +1,477 @@
|
|||||||
|
use crate::{
|
||||||
|
config_server::{
|
||||||
|
ConfigServerCallbackScope, ManagedConfigServerClientHooks, set_active_for_test,
|
||||||
|
},
|
||||||
|
state::{
|
||||||
|
INSTANCE_MANAGER, INSTANCE_NAME_ID_MAP, find_instance_id_by_name,
|
||||||
|
lock_remote_instance_mutation, remove_instance_name_ids,
|
||||||
|
},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
use easytier::{
|
||||||
|
common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader},
|
||||||
|
web_client::WebClientHooks,
|
||||||
|
};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
ffi::{CStr, CString, c_char, c_void},
|
||||||
|
sync::{Mutex, mpsc},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_config() {
|
||||||
|
let cfg_str = r#"
|
||||||
|
inst_name = "test"
|
||||||
|
network = "test_network"
|
||||||
|
"#;
|
||||||
|
let cstr = std::ffi::CString::new(cfg_str).unwrap();
|
||||||
|
unsafe {
|
||||||
|
assert_eq!(parse_config(cstr.as_ptr()), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_network_instance() {
|
||||||
|
let cfg_str = r#"
|
||||||
|
inst_name = "test"
|
||||||
|
network = "test_network"
|
||||||
|
"#;
|
||||||
|
let cstr = std::ffi::CString::new(cfg_str).unwrap();
|
||||||
|
unsafe {
|
||||||
|
assert_eq!(run_network_instance(cstr.as_ptr()), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_error_msg_returns_config_server_callback_error() {
|
||||||
|
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||||
|
let callback_error = format!("callback delivery failed {}", Uuid::new_v4());
|
||||||
|
crate::config_server::clear_last_callback_error();
|
||||||
|
hooks.note_callback_error(callback_error.clone());
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let mut error_ptr: *const c_char = std::ptr::null();
|
||||||
|
get_error_msg(&mut error_ptr);
|
||||||
|
assert!(!error_ptr.is_null());
|
||||||
|
let error_msg = CStr::from_ptr(error_ptr).to_string_lossy().into_owned();
|
||||||
|
free_string(error_ptr);
|
||||||
|
assert!(error_msg.contains(&callback_error));
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::config_server::clear_last_callback_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn record_config_server_event(event_json: *const c_char, user_data: *mut c_void) {
|
||||||
|
let events = unsafe { &*(user_data as *const Mutex<Vec<String>>) };
|
||||||
|
events.lock().unwrap().push(
|
||||||
|
unsafe { CStr::from_ptr(event_json) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn config_server_hooks_emit_run_event() {
|
||||||
|
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||||
|
let hooks = ManagedConfigServerClientHooks::new(
|
||||||
|
Some(record_config_server_event),
|
||||||
|
&events as *const _ as *mut c_void,
|
||||||
|
);
|
||||||
|
let instance_id = Uuid::new_v4();
|
||||||
|
let cfg = TomlConfigLoader::default();
|
||||||
|
cfg.set_id(instance_id);
|
||||||
|
let inst_name = format!("test-{}", instance_id);
|
||||||
|
cfg.set_inst_name(inst_name.clone());
|
||||||
|
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
hooks.post_run_network_instance(&instance_id).await.unwrap();
|
||||||
|
|
||||||
|
let duplicate_cfg = TomlConfigLoader::default();
|
||||||
|
duplicate_cfg.set_inst_name(inst_name);
|
||||||
|
duplicate_cfg.set_id(Uuid::new_v4());
|
||||||
|
assert!(
|
||||||
|
hooks
|
||||||
|
.pre_run_network_instance(&duplicate_cfg)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(hooks.tracked_instance_ids(), vec![instance_id]);
|
||||||
|
let events = events.lock().unwrap();
|
||||||
|
assert_eq!(events.len(), 1);
|
||||||
|
let event: Value = serde_json::from_str(&events[0]).unwrap();
|
||||||
|
assert_eq!(event["event"], "run_network_instance");
|
||||||
|
assert_eq!(event["success"], true);
|
||||||
|
assert_eq!(event["instance_id"], instance_id.to_string());
|
||||||
|
assert!(event["error"].is_null());
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.delete_network_instance(vec![instance_id])
|
||||||
|
.unwrap();
|
||||||
|
remove_instance_name_ids(&[instance_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn config_server_hooks_emit_delete_events_for_tracked_instances() {
|
||||||
|
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||||
|
let hooks = ManagedConfigServerClientHooks::new(
|
||||||
|
Some(record_config_server_event),
|
||||||
|
&events as *const _ as *mut c_void,
|
||||||
|
);
|
||||||
|
let instance_id_1 = Uuid::new_v4();
|
||||||
|
let instance_id_2 = Uuid::new_v4();
|
||||||
|
let unknown_instance_id = Uuid::new_v4();
|
||||||
|
for id in [instance_id_1, instance_id_2] {
|
||||||
|
let cfg = TomlConfigLoader::default();
|
||||||
|
cfg.set_id(id);
|
||||||
|
cfg.set_inst_name(format!("test-{}", id));
|
||||||
|
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
hooks
|
||||||
|
.post_run_network_instance(&instance_id_1)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
hooks
|
||||||
|
.post_run_network_instance(&instance_id_2)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
events.lock().unwrap().clear();
|
||||||
|
|
||||||
|
hooks
|
||||||
|
.post_remove_network_instances(&[instance_id_1, unknown_instance_id, instance_id_2])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(hooks.tracked_instance_ids().is_empty());
|
||||||
|
let events = events.lock().unwrap();
|
||||||
|
assert_eq!(events.len(), 2);
|
||||||
|
let event_ids = events
|
||||||
|
.iter()
|
||||||
|
.map(|event| {
|
||||||
|
let event: Value = serde_json::from_str(event).unwrap();
|
||||||
|
assert_eq!(event["event"], "delete_network_instance");
|
||||||
|
assert_eq!(event["success"], true);
|
||||||
|
assert!(event["error"].is_null());
|
||||||
|
event["instance_id"].as_str().unwrap().to_string()
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
event_ids,
|
||||||
|
HashSet::from([instance_id_1.to_string(), instance_id_2.to_string()])
|
||||||
|
);
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.delete_network_instance(vec![instance_id_1, instance_id_2])
|
||||||
|
.unwrap();
|
||||||
|
remove_instance_name_ids(&[instance_id_1, instance_id_2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn config_server_hooks_remove_untracked_name_mapping_without_event() {
|
||||||
|
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||||
|
let hooks = ManagedConfigServerClientHooks::new(
|
||||||
|
Some(record_config_server_event),
|
||||||
|
&events as *const _ as *mut c_void,
|
||||||
|
);
|
||||||
|
let local_id = Uuid::new_v4();
|
||||||
|
let inst_name = format!("local-{}", local_id);
|
||||||
|
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), local_id);
|
||||||
|
|
||||||
|
hooks
|
||||||
|
.post_remove_network_instances(&[local_id])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(INSTANCE_NAME_ID_MAP.get(&inst_name).is_none());
|
||||||
|
assert!(events.lock().unwrap().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn config_server_hooks_reject_duplicate_instance_name() {
|
||||||
|
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||||
|
let inst_name = format!("test-{}", Uuid::new_v4());
|
||||||
|
let existing_id = Uuid::new_v4();
|
||||||
|
let new_id = Uuid::new_v4();
|
||||||
|
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), existing_id);
|
||||||
|
|
||||||
|
let cfg = TomlConfigLoader::default();
|
||||||
|
cfg.set_inst_name(inst_name.clone());
|
||||||
|
cfg.set_id(new_id);
|
||||||
|
|
||||||
|
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
|
||||||
|
assert_eq!(*INSTANCE_NAME_ID_MAP.get(&inst_name).unwrap(), existing_id);
|
||||||
|
INSTANCE_NAME_ID_MAP.remove(&inst_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn config_server_hooks_remove_overwritten_id_before_duplicate_name_error() {
|
||||||
|
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||||
|
let hooks = ManagedConfigServerClientHooks::new(
|
||||||
|
Some(record_config_server_event),
|
||||||
|
&events as *const _ as *mut c_void,
|
||||||
|
);
|
||||||
|
let old_name = format!("old-{}", Uuid::new_v4());
|
||||||
|
let duplicate_name = format!("duplicate-{}", Uuid::new_v4());
|
||||||
|
let overwritten_id = Uuid::new_v4();
|
||||||
|
let duplicate_id = Uuid::new_v4();
|
||||||
|
hooks.instance_ids.lock().unwrap().insert(overwritten_id);
|
||||||
|
INSTANCE_NAME_ID_MAP.insert(old_name.clone(), overwritten_id);
|
||||||
|
INSTANCE_NAME_ID_MAP.insert(duplicate_name.clone(), duplicate_id);
|
||||||
|
|
||||||
|
hooks
|
||||||
|
.post_remove_network_instances(&[overwritten_id])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let cfg = TomlConfigLoader::default();
|
||||||
|
cfg.set_inst_name(duplicate_name.clone());
|
||||||
|
cfg.set_id(overwritten_id);
|
||||||
|
|
||||||
|
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
|
||||||
|
assert!(hooks.tracked_instance_ids().is_empty());
|
||||||
|
assert!(INSTANCE_NAME_ID_MAP.get(&old_name).is_none());
|
||||||
|
assert_eq!(
|
||||||
|
*INSTANCE_NAME_ID_MAP.get(&duplicate_name).unwrap(),
|
||||||
|
duplicate_id
|
||||||
|
);
|
||||||
|
assert_eq!(events.lock().unwrap().len(), 1);
|
||||||
|
INSTANCE_NAME_ID_MAP.remove(&duplicate_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn config_server_hooks_remove_tracked_state_before_overwrite_retry() {
|
||||||
|
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||||
|
let inst_name = format!("test-{}", Uuid::new_v4());
|
||||||
|
let instance_id = Uuid::new_v4();
|
||||||
|
hooks.instance_ids.lock().unwrap().insert(instance_id);
|
||||||
|
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), instance_id);
|
||||||
|
|
||||||
|
let cfg = TomlConfigLoader::default();
|
||||||
|
cfg.set_inst_name(inst_name.clone());
|
||||||
|
cfg.set_id(instance_id);
|
||||||
|
|
||||||
|
hooks
|
||||||
|
.post_remove_network_instances(&[instance_id])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||||
|
|
||||||
|
assert!(hooks.tracked_instance_ids().is_empty());
|
||||||
|
assert!(INSTANCE_NAME_ID_MAP.get(&inst_name).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn config_server_hooks_reject_post_run_after_external_delete() {
|
||||||
|
let hooks = ManagedConfigServerClientHooks::new(None, std::ptr::null_mut());
|
||||||
|
let instance_id = Uuid::new_v4();
|
||||||
|
let cfg = TomlConfigLoader::default();
|
||||||
|
cfg.set_id(instance_id);
|
||||||
|
cfg.set_inst_name(format!("test-{}", instance_id));
|
||||||
|
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||||
|
.unwrap();
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.delete_network_instance(vec![instance_id])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(hooks.post_run_network_instance(&instance_id).await.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_instance_id_by_name_resolves_uncommitted_manager_instance_name() {
|
||||||
|
let instance_id = Uuid::new_v4();
|
||||||
|
let inst_name = format!("test-{}", instance_id);
|
||||||
|
let cfg = TomlConfigLoader::default();
|
||||||
|
cfg.set_id(instance_id);
|
||||||
|
cfg.set_inst_name(inst_name.clone());
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(find_instance_id_by_name(&inst_name), Some(instance_id));
|
||||||
|
INSTANCE_MANAGER
|
||||||
|
.delete_network_instance(vec![instance_id])
|
||||||
|
.unwrap();
|
||||||
|
remove_instance_name_ids(&[instance_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ffi_remote_mutation_lock_uses_manager_lock() {
|
||||||
|
let manager_guard = INSTANCE_MANAGER
|
||||||
|
.remote_mutation_lock()
|
||||||
|
.blocking_lock_owned();
|
||||||
|
let (done_tx, done_rx) = mpsc::channel();
|
||||||
|
let waiter = std::thread::spawn(move || {
|
||||||
|
let _ffi_guard = lock_remote_instance_mutation();
|
||||||
|
done_tx.send(()).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
|
||||||
|
drop(manager_guard);
|
||||||
|
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||||
|
waiter.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn config_server_hooks_suppress_late_run_events_while_stopping() {
|
||||||
|
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||||
|
let hooks = ManagedConfigServerClientHooks::new(
|
||||||
|
Some(record_config_server_event),
|
||||||
|
&events as *const _ as *mut c_void,
|
||||||
|
);
|
||||||
|
hooks.start_stopping();
|
||||||
|
|
||||||
|
hooks
|
||||||
|
.post_run_network_instance(&Uuid::new_v4())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(hooks.tracked_instance_ids().is_empty());
|
||||||
|
assert!(events.lock().unwrap().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
|
||||||
|
let _callback_scope = ConfigServerCallbackScope::enter();
|
||||||
|
assert_eq!(is_config_server_client_connected(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { collect_network_infos(std::ptr::null_mut(), 0) },
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
let cfg = CString::new("inst_name = \"callback-test\"\nlisteners = []").unwrap();
|
||||||
|
assert_eq!(unsafe { run_network_instance(cfg.as_ptr()) }, -1);
|
||||||
|
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 0) }, -1);
|
||||||
|
let url = CString::new("ring://test/token").unwrap();
|
||||||
|
let machine_id = CString::new("test-machine").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
unsafe {
|
||||||
|
start_config_server_client(
|
||||||
|
url.as_ptr(),
|
||||||
|
std::ptr::null(),
|
||||||
|
machine_id.as_ptr(),
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
assert_eq!(stop_config_server_client(), -1);
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
unsafe {
|
||||||
|
data_plane_tcp_connect(
|
||||||
|
std::ptr::null(),
|
||||||
|
std::ptr::null(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe {
|
||||||
|
data_plane_tcp_bind(
|
||||||
|
std::ptr::null(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe {
|
||||||
|
data_plane_tcp_accept(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { data_plane_tcp_read(0, std::ptr::null_mut(), 0, 0) },
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { data_plane_tcp_write(0, std::ptr::null(), 0, 0) },
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
assert_eq!(data_plane_tcp_close(0), -1);
|
||||||
|
assert_eq!(data_plane_tcp_listener_close(0), -1);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe {
|
||||||
|
data_plane_udp_bind(
|
||||||
|
std::ptr::null(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { data_plane_udp_send_to(0, std::ptr::null(), 0, std::ptr::null(), 0, 0) },
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe {
|
||||||
|
data_plane_udp_recv_from(
|
||||||
|
0,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
assert_eq!(data_plane_udp_close(0), -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ffi-dataplane")]
|
||||||
|
#[test]
|
||||||
|
fn active_config_server_rejects_data_plane() {
|
||||||
|
set_active_for_test(true);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
unsafe {
|
||||||
|
data_plane_tcp_connect(
|
||||||
|
std::ptr::null(),
|
||||||
|
std::ptr::null(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { data_plane_tcp_read(0, std::ptr::null_mut(), 0, 0) },
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
|
||||||
|
set_active_for_test(false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use std::ffi::{c_char, c_void};
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct KeyValuePair {
|
||||||
|
pub key: *const c_char,
|
||||||
|
pub value: *const c_char,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ConfigServerEventCallback = Option<unsafe extern "C" fn(*const c_char, *mut c_void)>;
|
||||||
@@ -34,6 +34,7 @@ pub struct NetworkInstanceManager {
|
|||||||
instance_error_messages: Arc<DashMap<uuid::Uuid, String>>,
|
instance_error_messages: Arc<DashMap<uuid::Uuid, String>>,
|
||||||
config_dir: Option<PathBuf>,
|
config_dir: Option<PathBuf>,
|
||||||
guard_counter: Arc<()>,
|
guard_counter: Arc<()>,
|
||||||
|
remote_mutation_lock: Arc<tokio::sync::Mutex<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for NetworkInstanceManager {
|
impl Default for NetworkInstanceManager {
|
||||||
@@ -51,6 +52,7 @@ impl NetworkInstanceManager {
|
|||||||
instance_error_messages: Arc::new(DashMap::new()),
|
instance_error_messages: Arc::new(DashMap::new()),
|
||||||
config_dir: None,
|
config_dir: None,
|
||||||
guard_counter: Arc::new(()),
|
guard_counter: Arc::new(()),
|
||||||
|
remote_mutation_lock: Arc::new(tokio::sync::Mutex::new(())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +61,10 @@ impl NetworkInstanceManager {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remote_mutation_lock(&self) -> Arc<tokio::sync::Mutex<()>> {
|
||||||
|
self.remote_mutation_lock.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn start_instance_task(&self, instance_id: uuid::Uuid) -> Result<(), anyhow::Error> {
|
fn start_instance_task(&self, instance_id: uuid::Uuid) -> Result<(), anyhow::Error> {
|
||||||
if tokio::runtime::Handle::try_current().is_err() {
|
if tokio::runtime::Handle::try_current().is_err() {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
@@ -272,6 +278,12 @@ impl NetworkInstanceManager {
|
|||||||
.map(|instance| instance.value().get_config_file_control().clone())
|
.map(|instance| instance.value().get_config_file_control().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_instance_config(&self, instance_id: &uuid::Uuid) -> Option<TomlConfigLoader> {
|
||||||
|
self.instance_map
|
||||||
|
.get(instance_id)
|
||||||
|
.map(|instance| instance.value().get_config())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_instance_network_config_source(
|
pub fn get_instance_network_config_source(
|
||||||
&self,
|
&self,
|
||||||
instance_id: &uuid::Uuid,
|
instance_id: &uuid::Uuid,
|
||||||
|
|||||||
@@ -500,6 +500,10 @@ impl NetworkInstance {
|
|||||||
&self.config_file_control
|
&self.config_file_control
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_config(&self) -> TomlConfigLoader {
|
||||||
|
self.config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_network_config_source(&self) -> ConfigSource {
|
pub fn get_network_config_source(&self) -> ConfigSource {
|
||||||
self.config.get_network_config_source()
|
self.config.get_network_config_source()
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,10 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait WebClientHooks: Send + Sync {
|
pub trait WebClientHooks: Send + Sync {
|
||||||
|
fn manages_remote_config_instances(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
async fn pre_run_network_instance(&self, _cfg: &TomlConfigLoader) -> Result<(), String> {
|
async fn pre_run_network_instance(&self, _cfg: &TomlConfigLoader) -> Result<(), String> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user