tor lifecycle management

This commit is contained in:
Fabian Freund
2025-03-16 20:55:33 +01:00
parent 6830d94bac
commit 9bc9c53559
10 changed files with 176 additions and 24 deletions
@@ -6,7 +6,7 @@ part of 'tab.dart';
// RiverpodGenerator
// **************************************************************************
String _$tabRepositoryHash() => r'10768bd2c880edd40f82ed49532d9e2e2043e61f';
String _$tabRepositoryHash() => r'9bdd6ddc0254c830b783eb2a987d98d4f1d313fb';
/// See also [TabRepository].
@ProviderFor(TabRepository)
@@ -0,0 +1,18 @@
import 'dart:ui';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'lifecycle.g.dart';
@Riverpod(keepAlive: true)
class BrowserViewLifecycle extends _$BrowserViewLifecycle {
// ignore: use_setters_to_change_properties
void update(AppLifecycleState? newState) {
state = newState;
}
@override
AppLifecycleState? build() {
return null;
}
}
@@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'lifecycle.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$browserViewLifecycleHash() =>
r'9082d058611aaaae268b60b8ce130150f73d453d';
/// See also [BrowserViewLifecycle].
@ProviderFor(BrowserViewLifecycle)
final browserViewLifecycleProvider =
NotifierProvider<BrowserViewLifecycle, AppLifecycleState?>.internal(
BrowserViewLifecycle.new,
name: r'browserViewLifecycleProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$browserViewLifecycleHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$BrowserViewLifecycle = Notifier<AppLifecycleState?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -14,6 +15,7 @@ import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/browser/domain/providers/lifecycle.dart';
import 'package:lensai/features/geckoview/features/browser/domain/services/browser_data.dart';
import 'package:lensai/features/geckoview/features/browser/domain/services/engine_settings_replication.dart';
import 'package:lensai/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart';
@@ -135,6 +137,12 @@ class _BrowserViewState extends ConsumerState<BrowserView>
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
ref
.read(browserViewLifecycleProvider.notifier)
.update(SchedulerBinding.instance.lifecycleState);
});
WidgetsBinding.instance.addObserver(this);
//Initialize and register dependencies
@@ -172,6 +180,8 @@ class _BrowserViewState extends ConsumerState<BrowserView>
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
ref.read(browserViewLifecycleProvider.notifier).update(state);
switch (state) {
case AppLifecycleState.detached:
case AppLifecycleState.inactive:
@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:lensai/features/geckoview/features/browser/domain/providers/lifecycle.dart';
import 'package:lensai/features/tor/utils/tor_entrypoint.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';
@@ -18,6 +20,14 @@ class _TorService {
await service.startService();
}
Future<void> requestSync() async {
service.invoke("sync");
}
Future<void> sendHeartbeat() async {
service.invoke("heartbeat");
}
Future<void> stop() async {
service.invoke("stop");
}
@@ -39,9 +49,9 @@ class _TorService {
});
}
void dispose() {
unawaited(stop());
unawaited(_portSubject.close());
Future<void> dispose() async {
await stop();
await _portSubject.close();
}
}
@@ -49,17 +59,44 @@ class _TorService {
class TorProxyService extends _$TorProxyService {
final _tor = _TorService();
Timer? _heartbeatUpdate;
//This is managed by widget state changes to resume a timer
bool _timerPaused = false;
void _enableHeartbeatTimer() {
_heartbeatUpdate?.cancel();
_timerPaused = false;
_heartbeatUpdate = Timer.periodic(const Duration(seconds: 30), (
timer,
) async {
await _tor.sendHeartbeat();
});
}
Future<void> connect({bool forceReconnect = false}) async {
final currentPort = _tor.portStream.valueOrNull;
final currentPort = await requestSync();
if (currentPort == null || forceReconnect) {
state = const AsyncLoading();
await _tor.start();
_enableHeartbeatTimer();
}
}
Future<int?> requestSync() async {
final nextPortUpdate = _tor.portStream.first;
await _tor.requestSync();
return nextPortUpdate;
}
Future<void> disconnect() async {
state = const AsyncLoading();
_heartbeatUpdate?.cancel();
_heartbeatUpdate = null;
_timerPaused = false;
await _tor.stop();
}
@@ -71,9 +108,31 @@ class TorProxyService extends _$TorProxyService {
await _tor.initializeService();
ref.listen(browserViewLifecycleProvider, (previous, next) {
switch (next) {
case AppLifecycleState.resumed:
if (_timerPaused) {
_enableHeartbeatTimer();
}
case AppLifecycleState.detached:
case AppLifecycleState.inactive:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
case null:
if (_heartbeatUpdate?.isActive == true) {
_heartbeatUpdate?.cancel();
_timerPaused = true;
}
}
});
ref.onDispose(() async {
_heartbeatUpdate?.cancel();
_heartbeatUpdate = null;
_timerPaused = false;
await portSub.cancel();
_tor.dispose();
await _tor.dispose();
});
return _tor.portStream.valueOrNull;
@@ -6,7 +6,7 @@ part of 'tor_proxy.dart';
// RiverpodGenerator
// **************************************************************************
String _$torProxyServiceHash() => r'782708b8e5159931cecf8d32b843cb96a929fb1f';
String _$torProxyServiceHash() => r'1f0c7611fd939ff25ad0acbdbd205b7e6e83067c';
/// See also [TorProxyService].
@ProviderFor(TorProxyService)
@@ -12,24 +12,28 @@ class StartProxyController extends _$StartProxyController {
// ignore: document_ignores is used for dialog
// ignore: avoid_build_context_in_providers
Future<void> maybeStartProxy(BuildContext context) async {
final torProxyRunning = ref.read(torProxyServiceProvider);
final torProxyRunning =
await ref.read(torProxyServiceProvider.notifier).requestSync();
if (torProxyRunning.valueOrNull == null) {
final result = await showDialog<bool>(
context: context,
builder: (context) {
return TorDialog();
},
);
if (torProxyRunning == null) {
if (context.mounted) {
final result = await showDialog<bool>(
context: context,
builder: (context) {
return TorDialog();
},
);
if (result == true) {
final connection = ref.read(torProxyServiceProvider.notifier).connect();
if (result == true) {
final connection =
ref.read(torProxyServiceProvider.notifier).connect();
ref
.read(overlayControllerProvider.notifier)
.show(Positioned(top: 0, left: 0, child: TorNotification()));
ref
.read(overlayControllerProvider.notifier)
.show(Positioned(top: 0, left: 0, child: TorNotification()));
await connection;
await connection;
}
}
}
}
@@ -7,7 +7,7 @@ part of 'start_tor_proxy.dart';
// **************************************************************************
String _$startProxyControllerHash() =>
r'7ba8d6eded4664a8aa3eb1360926c56e9185cac9';
r'b83cdc1b6d1c358a2042ff2b7cc2f19f82e5d5ed';
/// See also [StartProxyController].
@ProviderFor(StartProxyController)
@@ -3,6 +3,7 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/tor/domain/services/tor_proxy.dart';
import 'package:lensai/presentation/hooks/on_initialization.dart';
class TorProxyScreen extends HookConsumerWidget {
const TorProxyScreen();
@@ -11,6 +12,10 @@ class TorProxyScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final torProxyPort = ref.watch(torProxyServiceProvider);
useOnInitialization(() async {
await ref.read(torProxyServiceProvider.notifier).requestSync();
});
return Scaffold(
appBar: AppBar(title: const Text('Tor Proxy')),
body: ColoredBox(
+30 -2
View File
@@ -1,22 +1,50 @@
import 'dart:async';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:tor/tor.dart';
@pragma('vm:entry-point')
Future<void> onStart(ServiceInstance service) async {
Timer? timeout;
await Tor.init();
final portSub = Tor.instance.events.stream.listen((port) {
service.invoke('portUpdate', {'port': port});
});
await Tor.instance.start();
Future<void> stopService() async {
timeout?.cancel();
timeout = null;
service.on("stop").listen((event) async {
Tor.instance.stop();
service.invoke('portUpdate', {'port': -1});
await portSub.cancel();
await service.stopSelf();
}
void addHeartbeat() {
timeout?.cancel();
timeout = Timer(const Duration(minutes: 15), () async {
// logger.i('Terminating tor due to timeout');
await stopService();
});
}
await Tor.instance.start();
service.on("heartbeat").listen((event) {
// logger.d('Received tor heartbeat');
addHeartbeat();
});
service.on("sync").listen((event) {
service.invoke('portUpdate', {'port': Tor.instance.port});
});
service.on("stop").listen((event) async {
await stopService();
});
}