watch value fix, invitation fix, logging

This commit is contained in:
Christien Rioux 2024-04-06 22:36:30 -04:00
parent b3e9cbd4f3
commit 8335e36876
7 changed files with 133 additions and 77 deletions

View File

@ -73,13 +73,9 @@ class ValidContactInvitation {
..identitySignature = identitySignature.toProto(); ..identitySignature = identitySignature.toProto();
// Write the acceptance to the inbox // Write the acceptance to the inbox
if (await contactRequestInbox.tryWriteProtobuf( await contactRequestInbox
proto.SignedContactResponse.fromBuffer, .eventualWriteProtobuf(signedContactResponse, subkey: 1);
signedContactResponse,
subkey: 1) !=
null) {
throw Exception('failed to accept contact invitation');
}
return AcceptedContact( return AcceptedContact(
remoteProfile: _contactRequestPrivate.profile, remoteProfile: _contactRequestPrivate.profile,
remoteIdentity: _contactIdentityMaster, remoteIdentity: _contactIdentityMaster,
@ -129,13 +125,8 @@ class ValidContactInvitation {
..identitySignature = identitySignature.toProto(); ..identitySignature = identitySignature.toProto();
// Write the rejection to the inbox // Write the rejection to the inbox
if (await contactRequestInbox.tryWriteProtobuf( await contactRequestInbox.eventualWriteProtobuf(signedContactResponse,
proto.SignedContactResponse.fromBuffer, signedContactResponse, subkey: 1);
subkey: 1) !=
null) {
log.error('failed to reject contact invitation');
return false;
}
return true; return true;
}); });
} }

View File

@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
@ -222,6 +223,16 @@ class InvitationDialogState extends State<InvitationDialog> {
_validInvitation = null; _validInvitation = null;
widget.onValidationFailed(); widget.onValidationFailed();
}); });
} on VeilidAPIException {
final errorText = translate('invitation_dialog.invalid_invitation');
if (mounted) {
showErrorToast(context, errorText);
}
setState(() {
_isValidating = false;
_validInvitation = null;
widget.onValidationFailed();
});
} on Exception catch (e) { } on Exception catch (e) {
log.debug('exception: $e', e); log.debug('exception: $e', e);
setState(() { setState(() {
@ -233,6 +244,48 @@ class InvitationDialogState extends State<InvitationDialog> {
} }
} }
List<Widget> _buildPreAccept() => <Widget>[
if (!_isValidating && _validInvitation == null)
widget.buildInviteControl(context, this, _validateInviteData),
if (_isValidating)
Column(children: [
Text(translate('invitation_dialog.validating'))
.paddingLTRB(0, 0, 0, 16),
buildProgressIndicator().paddingAll(16),
]).toCenter(),
if (_validInvitation == null &&
!_isValidating &&
widget.inviteControlIsValid())
Column(children: [
Text(translate('invitation_dialog.invalid_invitation')),
const Icon(Icons.error).paddingAll(16)
]).toCenter(),
if (_validInvitation != null && !_isValidating)
Column(children: [
Container(
constraints: const BoxConstraints(maxHeight: 64),
width: double.infinity,
child:
ProfileWidget(profile: _validInvitation!.remoteProfile))
.paddingLTRB(0, 0, 0, 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.check_circle),
label: Text(translate('button.accept')),
onPressed: _onAccept,
).paddingLTRB(0, 0, 8, 0),
ElevatedButton.icon(
icon: const Icon(Icons.cancel),
label: Text(translate('button.reject')),
onPressed: _onReject,
).paddingLTRB(8, 0, 0, 0)
],
),
])
];
@override @override
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -240,63 +293,20 @@ class InvitationDialogState extends State<InvitationDialog> {
// final scale = theme.extension<ScaleScheme>()!; // final scale = theme.extension<ScaleScheme>()!;
// final textTheme = theme.textTheme; // final textTheme = theme.textTheme;
// final height = MediaQuery.of(context).size.height; // final height = MediaQuery.of(context).size.height;
final dismissible = !_isAccepting && !_isValidating;
if (_isAccepting) { final dialog = ConstrainedBox(
return SizedBox(
height: 300,
width: 300,
child: buildProgressIndicator().toCenter())
.paddingAll(16);
}
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400), constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400),
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: _isAccepting
widget.buildInviteControl(context, this, _validateInviteData), ? [buildProgressIndicator().paddingAll(16)]
if (_isValidating) : _buildPreAccept()),
Column(children: [
Text(translate('invitation_dialog.validating'))
.paddingLTRB(0, 0, 0, 16),
buildProgressIndicator().paddingAll(16),
]).toCenter(),
if (_validInvitation == null &&
!_isValidating &&
widget.inviteControlIsValid())
Column(children: [
Text(translate('invitation_dialog.invalid_invitation')),
const Icon(Icons.error)
]).paddingAll(16).toCenter(),
if (_validInvitation != null && !_isValidating)
Column(children: [
Container(
constraints: const BoxConstraints(maxHeight: 64),
width: double.infinity,
child: ProfileWidget(
profile: _validInvitation!.remoteProfile))
.paddingLTRB(0, 0, 0, 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.check_circle),
label: Text(translate('button.accept')),
onPressed: _onAccept,
),
ElevatedButton.icon(
icon: const Icon(Icons.cancel),
label: Text(translate('button.reject')),
onPressed: _onReject,
)
],
),
])
]),
), ),
); );
return PopControl(dismissible: dismissible, child: dialog);
} }
@override @override

View File

@ -17,10 +17,13 @@ class PasteInvitationDialog extends StatefulWidget {
PasteInvitationDialogState createState() => PasteInvitationDialogState(); PasteInvitationDialogState createState() => PasteInvitationDialogState();
static Future<void> show(BuildContext context) async { static Future<void> show(BuildContext context) async {
await StyledDialog.show<void>( final modalContext = context;
await showPopControlDialog<void>(
context: context, context: context,
title: translate('paste_invitation_dialog.title'), builder: (context) => StyledDialog(
child: PasteInvitationDialog(modalContext: context)); title: translate('paste_invitation_dialog.title'),
child: PasteInvitationDialog(modalContext: modalContext)));
} }
final BuildContext modalContext; final BuildContext modalContext;
@ -67,8 +70,13 @@ class PasteInvitationDialogState extends State<PasteInvitationDialog> {
.sublist(firstline, lastline) .sublist(firstline, lastline)
.join() .join()
.replaceAll(RegExp(r'[^A-Za-z0-9\-_]'), ''); .replaceAll(RegExp(r'[^A-Za-z0-9\-_]'), '');
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
var inviteData = Uint8List(0);
try {
inviteData = base64UrlNoPadDecode(inviteDataBase64);
} on Exception {
//
}
await validateInviteData(inviteData: inviteData); await validateInviteData(inviteData: inviteData);
} }
@ -105,7 +113,7 @@ class PasteInvitationDialogState extends State<PasteInvitationDialog> {
return Column(mainAxisSize: MainAxisSize.min, children: [ return Column(mainAxisSize: MainAxisSize.min, children: [
Text( Text(
translate('paste_invitation_dialog.paste_invite_here'), translate('paste_invitation_dialog.paste_invite_here'),
).paddingLTRB(0, 0, 0, 8), ).paddingLTRB(0, 0, 0, 16),
Container( Container(
constraints: const BoxConstraints(maxHeight: 200), constraints: const BoxConstraints(maxHeight: 200),
child: TextField( child: TextField(

View File

@ -110,10 +110,12 @@ class ScanInvitationDialog extends StatefulWidget {
ScanInvitationDialogState createState() => ScanInvitationDialogState(); ScanInvitationDialogState createState() => ScanInvitationDialogState();
static Future<void> show(BuildContext context) async { static Future<void> show(BuildContext context) async {
await StyledDialog.show<void>( final modalContext = context;
await showPopControlDialog<void>(
context: context, context: context,
title: translate('scan_invitation_dialog.title'), builder: (context) => StyledDialog(
child: ScanInvitationDialog(modalContext: context)); title: translate('scan_invitation_dialog.title'),
child: ScanInvitationDialog(modalContext: modalContext)));
} }
final BuildContext modalContext; final BuildContext modalContext;

View File

@ -24,7 +24,8 @@ class VeilidChatGlobalInit {
await ProcessorRepository.instance.startup(); await ProcessorRepository.instance.startup();
// DHT Record Pool // DHT Record Pool
await DHTRecordPool.init(); await DHTRecordPool.init(
logger: (message) => log.debug('DHTRecordPool: $message'));
} }
// Initialize repositories // Initialize repositories

View File

@ -22,7 +22,9 @@ class PopControl extends StatelessWidget {
final route = ModalRoute.of(context); final route = ModalRoute.of(context);
if (route != null && route is PopControlDialogRoute) { if (route != null && route is PopControlDialogRoute) {
route.barrierDismissible = dismissible; WidgetsBinding.instance.addPostFrameCallback((_) {
route.barrierDismissible = dismissible;
});
} }
return PopScope( return PopScope(
@ -65,6 +67,7 @@ class PopControlDialogRoute<T> extends DialogRoute<T> {
set barrierDismissible(bool d) { set barrierDismissible(bool d) {
_barrierDismissible = d; _barrierDismissible = d;
changedInternalState();
} }
bool _barrierDismissible; bool _barrierDismissible;

View File

@ -17,6 +17,8 @@ part 'dht_record.dart';
const int watchBackoffMultiplier = 2; const int watchBackoffMultiplier = 2;
const int watchBackoffMax = 30; const int watchBackoffMax = 30;
typedef DHTRecordPoolLogger = void Function(String message);
/// Record pool that managed DHTRecords and allows for tagged deletion /// Record pool that managed DHTRecords and allows for tagged deletion
/// String versions of keys due to IMap<> json unsupported in key /// String versions of keys due to IMap<> json unsupported in key
@freezed @freezed
@ -90,6 +92,12 @@ class OpenedRecordInfo {
defaultRoutingContext: defaultRoutingContext); defaultRoutingContext: defaultRoutingContext);
SharedDHTRecordData shared; SharedDHTRecordData shared;
Set<DHTRecord> records = {}; Set<DHTRecord> records = {};
String get debugNames {
final r = records.toList()
..sort((a, b) => a.key.toString().compareTo(b.key.toString()));
return '[${r.map((x) => x.debugName).join(',')}]';
}
} }
class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> { class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
@ -100,6 +108,9 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
_routingContext = routingContext, _routingContext = routingContext,
_veilid = veilid; _veilid = veilid;
// Logger
DHTRecordPoolLogger? _logger;
// Persistent DHT record list // Persistent DHT record list
DHTRecordPoolAllocations _state; DHTRecordPoolAllocations _state;
// Create/open Mutex // Create/open Mutex
@ -136,15 +147,21 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
static DHTRecordPool get instance => _singleton!; static DHTRecordPool get instance => _singleton!;
static Future<void> init() async { static Future<void> init({DHTRecordPoolLogger? logger}) async {
final routingContext = await Veilid.instance.routingContext(); final routingContext = await Veilid.instance.routingContext();
final globalPool = DHTRecordPool._(Veilid.instance, routingContext); final globalPool = DHTRecordPool._(Veilid.instance, routingContext);
globalPool._state = await globalPool.load(); globalPool
.._logger = logger
.._state = await globalPool.load();
_singleton = globalPool; _singleton = globalPool;
} }
Veilid get veilid => _veilid; Veilid get veilid => _veilid;
void log(String message) {
_logger?.call(message);
}
Future<OpenedRecordInfo> _recordCreateInner( Future<OpenedRecordInfo> _recordCreateInner(
{required String debugName, {required String debugName,
required VeilidRoutingContext dhtctx, required VeilidRoutingContext dhtctx,
@ -156,6 +173,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
// Create the record // Create the record
final recordDescriptor = await dhtctx.createDHTRecord(schema); final recordDescriptor = await dhtctx.createDHTRecord(schema);
log('createDHTRecord: debugName=$debugName key=${recordDescriptor.key}');
// Reopen if a writer is specified to ensure // Reopen if a writer is specified to ensure
// we switch the default writer // we switch the default writer
if (writer != null) { if (writer != null) {
@ -185,6 +204,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
TypedKey? parent}) async { TypedKey? parent}) async {
assert(_mutex.isLocked, 'should be locked here'); assert(_mutex.isLocked, 'should be locked here');
log('openDHTRecord: debugName=$debugName key=$recordKey');
// If we are opening a key that already exists // If we are opening a key that already exists
// make sure we are using the same parent if one was specified // make sure we are using the same parent if one was specified
_validateParentInner(parent, recordKey); _validateParentInner(parent, recordKey);
@ -238,6 +259,9 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
Future<void> _recordClosed(DHTRecord record) async { Future<void> _recordClosed(DHTRecord record) async {
await _mutex.protect(() async { await _mutex.protect(() async {
final key = record.key; final key = record.key;
log('closeDHTRecord: debugName=${record.debugName} key=$key');
final openedRecordInfo = _opened[key]; final openedRecordInfo = _opened[key];
if (openedRecordInfo == null || if (openedRecordInfo == null ||
!openedRecordInfo.records.remove(record)) { !openedRecordInfo.records.remove(record)) {
@ -284,6 +308,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
} }
Future<void> _deleteInner(TypedKey recordKey) async { Future<void> _deleteInner(TypedKey recordKey) async {
log('deleteDHTRecord: key=$recordKey');
// Remove this child from parents // Remove this child from parents
await _removeDependenciesInner([recordKey]); await _removeDependenciesInner([recordKey]);
await _routingContext.deleteDHTRecord(recordKey); await _routingContext.deleteDHTRecord(recordKey);
@ -676,9 +702,14 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
var success = false; var success = false;
try { try {
success = await dhtctx.cancelDHTWatch(openedRecordKey); success = await dhtctx.cancelDHTWatch(openedRecordKey);
log('cancelDHTWatch: key=$openedRecordKey, success=$success, '
'debugNames=${openedRecordInfo.debugNames}');
openedRecordInfo.shared.needsWatchStateUpdate = false; openedRecordInfo.shared.needsWatchStateUpdate = false;
} on VeilidAPIException { } on VeilidAPIException catch (e) {
// Failed to cancel DHT watch, try again next tick // Failed to cancel DHT watch, try again next tick
log('Exception in watch cancel: $e');
} }
return success; return success;
}); });
@ -687,12 +718,21 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
// Record needs new watch // Record needs new watch
var success = false; var success = false;
try { try {
final subkeys = watchState.subkeys?.toList();
final count = watchState.count;
final expiration = watchState.expiration;
final realExpiration = await dhtctx.watchDHTValues( final realExpiration = await dhtctx.watchDHTValues(
openedRecordKey, openedRecordKey,
subkeys: watchState.subkeys?.toList(), subkeys: watchState.subkeys?.toList(),
count: watchState.count, count: watchState.count,
expiration: watchState.expiration); expiration: watchState.expiration);
log('watchDHTValues: key=$openedRecordKey, subkeys=$subkeys, '
'count=$count, expiration=$expiration, '
'realExpiration=$realExpiration, '
'debugNames=${openedRecordInfo.debugNames}');
// Update watch states with real expiration // Update watch states with real expiration
if (realExpiration.value != BigInt.zero) { if (realExpiration.value != BigInt.zero) {
openedRecordInfo.shared.needsWatchStateUpdate = false; openedRecordInfo.shared.needsWatchStateUpdate = false;
@ -700,8 +740,9 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
openedRecordInfo.records, realExpiration); openedRecordInfo.records, realExpiration);
success = true; success = true;
} }
} on VeilidAPIException { } on VeilidAPIException catch (e) {
// Failed to cancel DHT watch, try again next tick // Failed to cancel DHT watch, try again next tick
log('Exception in watch update: $e');
} }
return success; return success;
}); });