use std::sync::Arc;
use std::sync::atomic::Ordering;
use jni::objects::{Global, JClass, JObject, JString, JValue};
use jni::sys::{JNI_FALSE, JNI_TRUE, jboolean, jint, jlong};
use jni::{EnvUnowned, Outcome};
use log::{Record, error, info};
use tokio::sync::oneshot;
use crate::logging::{DynamicFileLogWriter, LogWriter};
use crate::tun::{FnSocketProtector, set_global_socket_protector};
use super::common::{
self, LOG_FILE, LOGGER_INITIALIZED, TUN_SERVICE, TunServiceHandle, setup_log_file,
};
struct LogcatWriter;
impl LogWriter for LogcatWriter {
fn write_log(&self, record: &Record, _formatted: &str) {
#[cfg(target_os = "android")]
{
use std::ffi::CString;
let tag = CString::new("shoes").unwrap_or_default();
let msg = CString::new(format!("{}", record.args())).unwrap_or_default();
let priority = match record.level() {
log::Level::Error => 6, log::Level::Warn => 5, log::Level::Info => 4, log::Level::Debug => 3, log::Level::Trace => 2, };
unsafe {
ndk_sys::__android_log_write(priority as i32, tag.as_ptr(), msg.as_ptr());
}
}
#[cfg(not(target_os = "android"))]
let _ = record;
}
fn flush(&self) {}
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_shoesproxy_ShoesNative_init<'local>(
mut unowned: EnvUnowned<'local>,
_class: JClass<'local>,
log_level: JString<'local>,
) -> jint {
let level_str: String = match unowned
.with_env(|env| env.get_string(&log_level).map(|s| s.to_string()))
.into_outcome()
{
Outcome::Ok(s) => s,
Outcome::Err(e) => {
eprintln!("Failed to get log level string: {}", e);
return -1;
}
Outcome::Panic(_) => return -1,
};
let level = crate::logging::parse_log_level(&level_str).unwrap_or(log::LevelFilter::Info);
if !LOGGER_INITIALIZED.swap(true, Ordering::SeqCst) {
LOG_FILE.get_or_init(|| parking_lot::Mutex::new(None));
let writers: Vec<Box<dyn LogWriter>> = vec![
Box::new(LogcatWriter),
Box::new(DynamicFileLogWriter::new(&LOG_FILE)),
];
let directives = vec![crate::logging::Directive { name: None, level }];
crate::logging::init_multi_logger(writers, directives);
}
info!("shoes initialized with log level: {}", level_str);
0
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_shoesproxy_ShoesNative_getVersion<'local>(
mut unowned: EnvUnowned<'local>,
_class: JClass<'local>,
) -> JString<'local> {
let version = env!("CARGO_PKG_VERSION");
match unowned
.with_env(|env| env.new_string(version))
.into_outcome()
{
Outcome::Ok(s) => s,
_ => JString::null(),
}
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_shoesproxy_ShoesNative_setLogFile<'local>(
mut unowned: EnvUnowned<'local>,
_class: JClass<'local>,
log_path: JString<'local>,
) -> jint {
let path_str: String = match unowned
.with_env(|env| env.get_string(&log_path).map(|s| s.to_string()))
.into_outcome()
{
Outcome::Ok(s) => s,
Outcome::Err(e) => {
error!("Failed to get log path string: {}", e);
return -1;
}
Outcome::Panic(_) => return -1,
};
setup_log_file(&path_str)
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_shoesproxy_ShoesNative_start<'local>(
mut unowned: EnvUnowned<'local>,
_class: JClass<'local>,
config_yaml: JString<'local>,
protect_callback: JObject<'local>,
) -> jlong {
let _lifecycle = common::SERVICE_LIFECYCLE.lock();
if TUN_SERVICE
.get()
.is_some_and(|service| service.lock().is_some())
{
error!("Stop the previous shoes service before starting another");
return -1;
}
info!("Starting shoes service");
let result = unowned
.with_env(
|env| -> jni::errors::Result<(String, Global<JObject<'static>>, jni::JavaVM)> {
let config_str: String = env.get_string(&config_yaml).map(|s| s.to_string())?;
let callback_ref = env.new_global_ref(protect_callback)?;
let jvm = env.get_java_vm()?;
Ok((config_str, callback_ref, jvm))
},
)
.into_outcome();
let (config_str, callback_ref, jvm) = match result {
Outcome::Ok(v) => v,
Outcome::Err(e) => {
error!("Failed to extract JNI values for start: {}", e);
return -1;
}
Outcome::Panic(_) => return -1,
};
let jvm: Arc<jni::JavaVM> = Arc::new(jvm);
let callback_ref: Arc<Global<JObject<'static>>> = Arc::new(callback_ref);
let jvm_clone = jvm.clone();
let callback_clone = callback_ref.clone();
let protector = FnSocketProtector::new(move |fd: i32| {
let protect_ok = jvm_clone
.attach_current_thread(|env: &mut jni::Env| -> jni::errors::Result<bool> {
let v = env.call_method(
&*callback_clone,
jni::jni_str!("protect"),
jni::jni_sig!("(I)Z"),
&[JValue::Int(fd)],
)?;
Ok(v.z().unwrap_or(false))
})
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{}", e)))?;
if protect_ok {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"VpnService.protect() returned false",
))
}
});
set_global_socket_protector(Arc::new(protector));
crate::thread_util::set_num_threads(2);
let runtime = match tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
error!("Failed to create tokio runtime: {}", e);
crate::tun::clear_global_socket_protector();
return -1;
}
};
let (shutdown_tx, shutdown_rx) = oneshot::channel();
let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
let task = common::spawn_service(&runtime, config_str, shutdown_rx, running.clone());
let handle = TunServiceHandle {
runtime,
shutdown_tx: Some(shutdown_tx),
running,
task,
};
let service = TUN_SERVICE.get_or_init(|| parking_lot::Mutex::new(None));
*service.lock() = Some(handle);
info!("TUN service started successfully");
1
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_shoesproxy_ShoesNative_stop(
_env: EnvUnowned,
_class: JClass,
_handle: jlong,
) {
common::stop_service();
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_shoesproxy_ShoesNative_isRunning(
_env: EnvUnowned,
_class: JClass,
) -> jboolean {
if common::is_service_running() {
JNI_TRUE
} else {
JNI_FALSE
}
}