diff --git a/CHANGELOG.md b/CHANGELOG.md
index adc6ff3..c409f3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,13 @@ SPDX-FileCopyrightText: 2024 Foundation Devices Inc.
SPDX-License-Identifier: MIT
-->
+## 0.2.1
+
+* Tear down the Tor client deterministically on `stop()`: await the proxy
+ accept loop, cancel accepted connections, and dispose the Rust client
+ instead of waiting for Dart GC, which left `tor_cache/dir.lock` held and
+ forced a restarted client's directory store into silent read-only mode.
+
## 0.0.9
* Bumped arti to version 1.4.3
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 0b6256a..220c4eb 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -398,7 +398,7 @@ packages:
path: ".."
relative: true
source: path
- version: "0.2.0"
+ version: "0.2.1"
typed_data:
dependency: transitive
description:
diff --git a/ios/tor/rust_lib_tor.xcframework/Info.plist b/ios/tor/rust_lib_tor.xcframework/Info.plist
index edf8576..daadedb 100644
--- a/ios/tor/rust_lib_tor.xcframework/Info.plist
+++ b/ios/tor/rust_lib_tor.xcframework/Info.plist
@@ -8,32 +8,32 @@
BinaryPath
rust_lib_tor.framework/rust_lib_tor
LibraryIdentifier
- ios-arm64
+ ios-arm64_x86_64-simulator
LibraryPath
rust_lib_tor.framework
SupportedArchitectures
arm64
+ x86_64
SupportedPlatform
ios
+ SupportedPlatformVariant
+ simulator
BinaryPath
rust_lib_tor.framework/rust_lib_tor
LibraryIdentifier
- ios-arm64_x86_64-simulator
+ ios-arm64
LibraryPath
rust_lib_tor.framework
SupportedArchitectures
arm64
- x86_64
SupportedPlatform
ios
- SupportedPlatformVariant
- simulator
CFBundlePackageType
diff --git a/ios/tor/rust_lib_tor.xcframework/ios-arm64/rust_lib_tor.framework/Info.plist b/ios/tor/rust_lib_tor.xcframework/ios-arm64/rust_lib_tor.framework/Info.plist
index bbd88fd..907840d 100644
--- a/ios/tor/rust_lib_tor.xcframework/ios-arm64/rust_lib_tor.framework/Info.plist
+++ b/ios/tor/rust_lib_tor.xcframework/ios-arm64/rust_lib_tor.framework/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
FMWK
CFBundleShortVersionString
- 0.2.0
+ 0.2.1
CFBundleSupportedPlatforms
iPhoneOS
diff --git a/ios/tor/rust_lib_tor.xcframework/ios-arm64/rust_lib_tor.framework/rust_lib_tor b/ios/tor/rust_lib_tor.xcframework/ios-arm64/rust_lib_tor.framework/rust_lib_tor
index bce53ac..9a1e02f 100755
Binary files a/ios/tor/rust_lib_tor.xcframework/ios-arm64/rust_lib_tor.framework/rust_lib_tor and b/ios/tor/rust_lib_tor.xcframework/ios-arm64/rust_lib_tor.framework/rust_lib_tor differ
diff --git a/ios/tor/rust_lib_tor.xcframework/ios-arm64_x86_64-simulator/rust_lib_tor.framework/Info.plist b/ios/tor/rust_lib_tor.xcframework/ios-arm64_x86_64-simulator/rust_lib_tor.framework/Info.plist
index 56f3d93..2ee1570 100644
--- a/ios/tor/rust_lib_tor.xcframework/ios-arm64_x86_64-simulator/rust_lib_tor.framework/Info.plist
+++ b/ios/tor/rust_lib_tor.xcframework/ios-arm64_x86_64-simulator/rust_lib_tor.framework/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
FMWK
CFBundleShortVersionString
- 0.2.0
+ 0.2.1
CFBundleSupportedPlatforms
iPhoneSimulator
diff --git a/ios/tor/rust_lib_tor.xcframework/ios-arm64_x86_64-simulator/rust_lib_tor.framework/rust_lib_tor b/ios/tor/rust_lib_tor.xcframework/ios-arm64_x86_64-simulator/rust_lib_tor.framework/rust_lib_tor
index 875ed1a..378a0c4 100755
Binary files a/ios/tor/rust_lib_tor.xcframework/ios-arm64_x86_64-simulator/rust_lib_tor.framework/rust_lib_tor and b/ios/tor/rust_lib_tor.xcframework/ios-arm64_x86_64-simulator/rust_lib_tor.framework/rust_lib_tor differ
diff --git a/lib/tor.dart b/lib/tor.dart
index 1356e2e..b39b6d3 100644
--- a/lib/tor.dart
+++ b/lib/tor.dart
@@ -48,8 +48,7 @@ class Tor {
bool _started = false;
/// True while the client is starting or re-bootstrapping.
- bool get starting =>
- _startInFlight != null || _bootstrapInFlight != null;
+ bool get starting => _startInFlight != null || _bootstrapInFlight != null;
Future? _startInFlight;
Future? _bootstrapInFlight;
@@ -197,6 +196,9 @@ class Tor {
_client = torInstance.client;
_proxy = torInstance.proxy;
_proxyPort = torInstance.socksPort;
+ // The getters above clone; free the container now instead of at GC so
+ // it cannot keep an extra client reference (and dir.lock) alive.
+ torInstance.dispose();
_started = true;
_bootstrapped = true; // startTor creates a bootstrapped client
@@ -258,6 +260,7 @@ class Tor {
/// Stops the proxy
Future stop() async {
final proxy = _proxy;
+ final client = _client;
// Stop publishing the route before awaiting native shutdown so callers
// cannot start new work against a proxy that is being torn down.
@@ -268,22 +271,14 @@ class Tor {
_bootstrapped = false;
broadcastState();
- if (proxy == null) {
- return;
- }
-
try {
- // This is now safe! FRB catches any panic and throws PanicException
- await rust.stopProxy(proxy: proxy);
- } on rust.TorError catch (e) {
- if (kDebugMode) {
- print('Error stopping proxy: $e');
- }
- } on PanicException catch (e) {
- // Previously this would SIGABRT the app, now it's catchable!
- if (kDebugMode) {
- print('Proxy stop panicked (caught safely): ${e.message}');
+ if (proxy != null) {
+ await rust.stopProxy(proxy: proxy);
}
+ } finally {
+ // Drop the Rust client now instead of at GC: a lingering client holds
+ // tor_cache/dir.lock, forcing a restarted client's dirmgr into read-only.
+ client?.dispose();
}
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 46d2821..1466b51 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -4,7 +4,7 @@
name: tor
description: A multi-platform Flutter plugin for managing a Tor proxy. Based on arti.
-version: 0.2.0
+version: 0.2.1
homepage: https://github.com/Foundation-Devices/tor
environment:
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index aeb65ec..074088f 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -3326,21 +3326,24 @@ dependencies = [
[[package]]
name = "rust_lib_tor"
-version = "0.2.0"
+version = "0.2.1"
dependencies = [
"anyhow",
"arti",
"arti-client",
"flutter_rust_bridge",
+ "futures",
"lazy_static",
"libc",
"liblzma",
"log",
"rlimit 0.10.2",
"security-framework 2.10.0",
+ "tempfile",
"thiserror 1.0.69",
"time",
"tokio",
+ "tokio-util",
"tor-config",
"tor-rtcompat",
]
@@ -4188,6 +4191,7 @@ dependencies = [
"futures-core",
"futures-io",
"futures-sink",
+ "futures-util",
"pin-project-lite",
"tokio",
]
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 617bc19..1685893 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -4,7 +4,7 @@
[package]
name = "rust_lib_tor"
-version = "0.2.0"
+version = "0.2.1"
authors = ["Igor Cota "]
edition = "2021"
@@ -13,8 +13,10 @@ crate-type = ["cdylib", "staticlib", "rlib"]
[dependencies]
flutter_rust_bridge = "=2.11.1"
+futures = "0.3"
lazy_static = "1.4"
tokio = { version = "1", features = ["full"] }
+tokio-util = { version = "0.7", features = ["rt"] }
libc = "0.2"
liblzma = { version = "0.4.7", features = ["static"] }
arti-client = { version = "0.44.0", features = ["static", "onion-service-client"] }
@@ -27,6 +29,9 @@ anyhow = "1.0.79"
time = "0.3.36"
thiserror = "1.0"
+[dev-dependencies]
+tempfile = "3"
+
[target.'cfg(target_os = "ios")'.dependencies]
# Specific version for iOS
security-framework = "=2.10.0"
diff --git a/rust/src/api/tor.rs b/rust/src/api/tor.rs
index eef5672..693993c 100644
--- a/rust/src/api/tor.rs
+++ b/rust/src/api/tor.rs
@@ -7,17 +7,146 @@ use arti::proxy::{self, ListenProtocols};
use arti_client::config::CfgPath;
use arti_client::{DormantMode, TorClient, TorClientConfig};
use flutter_rust_bridge::frb;
+use futures::future::FutureObj;
+use futures::task::{Spawn, SpawnError};
use lazy_static::lazy_static;
+use std::future::Future;
use std::io;
use std::sync::Arc;
-use tokio::runtime::{Builder, Runtime};
+use tokio::runtime::{Builder, Runtime as TokioRuntime};
use tokio::task::JoinHandle;
+use tokio_util::sync::CancellationToken;
+use tokio_util::task::TaskTracker;
use tor_config::Listen;
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
-use tor_rtcompat::{NetStreamProvider, TcpListenOptions, ToplevelBlockOn};
+use tor_rtcompat::{
+ Blocking, CompoundRuntime, NetStreamProvider, TcpListenOptions, ToplevelBlockOn,
+};
lazy_static! {
- static ref RUNTIME: io::Result = Builder::new_multi_thread().enable_all().build();
+ static ref RUNTIME: io::Result = Builder::new_multi_thread().enable_all().build();
+}
+
+const PROXY_SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
+
+type TrackedTorRuntime = CompoundRuntime<
+ TrackedSpawner,
+ TokioNativeTlsRuntime,
+ TokioNativeTlsRuntime,
+ TokioNativeTlsRuntime,
+ TokioNativeTlsRuntime,
+ TokioNativeTlsRuntime,
+ TokioNativeTlsRuntime,
+>;
+
+#[derive(Clone, Debug, Default)]
+struct TrackedTasks {
+ tracker: TaskTracker,
+ cancellation: CancellationToken,
+}
+
+impl TrackedTasks {
+ fn stop_and_wait(&self) -> Result<(), TorError> {
+ self.cancellation.cancel();
+ self.tracker.close();
+ let rt = RUNTIME
+ .as_ref()
+ .map_err(|error| TorError::RuntimeError(error.to_string()))?;
+ if rt
+ .block_on(async {
+ tokio::time::timeout(PROXY_SHUTDOWN_TIMEOUT, self.tracker.wait()).await
+ })
+ .is_err()
+ {
+ Err(TorError::ProxyStopError(
+ "Timed out stopping Tor client tasks".to_owned(),
+ ))
+ } else {
+ Ok(())
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+struct TrackedSpawner {
+ runtime: TokioNativeTlsRuntime,
+ tasks: TrackedTasks,
+}
+
+impl TrackedSpawner {
+ fn new(runtime: TokioNativeTlsRuntime) -> Self {
+ Self {
+ runtime,
+ tasks: TrackedTasks::default(),
+ }
+ }
+}
+
+impl Spawn for TrackedSpawner {
+ fn spawn_obj(&self, future: FutureObj<'static, ()>) -> Result<(), SpawnError> {
+ if self.tasks.tracker.is_closed() {
+ return Err(SpawnError::shutdown());
+ }
+
+ let cancellation = self.tasks.cancellation.clone();
+ let future = self.tasks.tracker.track_future(async move {
+ tokio::select! {
+ _ = cancellation.cancelled() => {}
+ _ = future => {}
+ }
+ });
+ self.runtime.spawn_obj(Box::new(future).into())
+ }
+}
+
+impl Blocking for TrackedSpawner {
+ type ThreadHandle = ::ThreadHandle;
+
+ fn spawn_blocking(&self, f: F) -> Self::ThreadHandle
+ where
+ F: FnOnce() -> T + Send + 'static,
+ T: Send + 'static,
+ {
+ self.runtime.spawn_blocking(f)
+ }
+
+ fn reenter_block_on(&self, future: F) -> F::Output
+ where
+ F: Future,
+ F::Output: Send + 'static,
+ {
+ self.runtime.reenter_block_on(future)
+ }
+
+ fn blocking_io(&self, f: F) -> impl Future