contact reject

This commit is contained in:
Christien Rioux 2023-08-05 19:34:00 -04:00
parent 9be3d100e4
commit b12cbcf684
11 changed files with 267 additions and 76 deletions

View File

@ -34,9 +34,10 @@
3. Set ContactRequest unicastinbox DHT record writer subkey with SignedContactResponse, encrypted with writer secret 3. Set ContactRequest unicastinbox DHT record writer subkey with SignedContactResponse, encrypted with writer secret
## Receiving an accept/reject ## Receiving an accept/reject
1. Decrypt with writer secret 1. Open and get SignedContactResponse from ContactRequest unicaseinbox DHT record
2. Get DHT record for contact's AccountMaster 2. Decrypt with writer secret
3. Validate the SignedContactResponse signature 3. Get DHT record for contact's AccountMaster
4. Validate the SignedContactResponse signature
If accept == false: If accept == false:
1. Announce rejection 1. Announce rejection

View File

@ -57,6 +57,7 @@ class ContactInvitationItemWidget extends ConsumerWidget {
await ref.read(fetchActiveAccountProvider.future); await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo != null) { if (activeAccountInfo != null) {
await deleteContactInvitation( await deleteContactInvitation(
accepted: false,
activeAccountInfo: activeAccountInfo, activeAccountInfo: activeAccountInfo,
contactInvitationRecord: contactInvitationRecord); contactInvitationRecord: contactInvitationRecord);
ref.invalidate(fetchContactInvitationRecordsProvider); ref.invalidate(fetchContactInvitationRecordsProvider);

View File

@ -1575,7 +1575,7 @@ class ContactResponse extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactResponse', createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactResponse', createEmptyInstance: create)
..aOB(1, _omitFieldNames ? '' : 'accept') ..aOB(1, _omitFieldNames ? '' : 'accept')
..aOM<TypedKey>(2, _omitFieldNames ? '' : 'accountMasterRecordKey', subBuilder: TypedKey.create) ..aOM<TypedKey>(2, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: TypedKey.create)
..aOM<TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationKey', subBuilder: TypedKey.create) ..aOM<TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationKey', subBuilder: TypedKey.create)
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -1611,15 +1611,15 @@ class ContactResponse extends $pb.GeneratedMessage {
void clearAccept() => clearField(1); void clearAccept() => clearField(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
TypedKey get accountMasterRecordKey => $_getN(1); TypedKey get identityMasterRecordKey => $_getN(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set accountMasterRecordKey(TypedKey v) { setField(2, v); } set identityMasterRecordKey(TypedKey v) { setField(2, v); }
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasAccountMasterRecordKey() => $_has(1); $core.bool hasIdentityMasterRecordKey() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearAccountMasterRecordKey() => clearField(2); void clearIdentityMasterRecordKey() => clearField(2);
@$pb.TagNumber(2) @$pb.TagNumber(2)
TypedKey ensureAccountMasterRecordKey() => $_ensure(1); TypedKey ensureIdentityMasterRecordKey() => $_ensure(1);
@$pb.TagNumber(3) @$pb.TagNumber(3)
TypedKey get remoteConversationKey => $_getN(2); TypedKey get remoteConversationKey => $_getN(2);

View File

@ -427,17 +427,17 @@ const ContactResponse$json = {
'1': 'ContactResponse', '1': 'ContactResponse',
'2': [ '2': [
{'1': 'accept', '3': 1, '4': 1, '5': 8, '10': 'accept'}, {'1': 'accept', '3': 1, '4': 1, '5': 8, '10': 'accept'},
{'1': 'account_master_record_key', '3': 2, '4': 1, '5': 11, '6': '.TypedKey', '10': 'accountMasterRecordKey'}, {'1': 'identity_master_record_key', '3': 2, '4': 1, '5': 11, '6': '.TypedKey', '10': 'identityMasterRecordKey'},
{'1': 'remote_conversation_key', '3': 3, '4': 1, '5': 11, '6': '.TypedKey', '10': 'remoteConversationKey'}, {'1': 'remote_conversation_key', '3': 3, '4': 1, '5': 11, '6': '.TypedKey', '10': 'remoteConversationKey'},
], ],
}; };
/// Descriptor for `ContactResponse`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ContactResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List contactResponseDescriptor = $convert.base64Decode( final $typed_data.Uint8List contactResponseDescriptor = $convert.base64Decode(
'Cg9Db250YWN0UmVzcG9uc2USFgoGYWNjZXB0GAEgASgIUgZhY2NlcHQSRAoZYWNjb3VudF9tYX' 'Cg9Db250YWN0UmVzcG9uc2USFgoGYWNjZXB0GAEgASgIUgZhY2NlcHQSRgoaaWRlbnRpdHlfbW'
'N0ZXJfcmVjb3JkX2tleRgCIAEoCzIJLlR5cGVkS2V5UhZhY2NvdW50TWFzdGVyUmVjb3JkS2V5' 'FzdGVyX3JlY29yZF9rZXkYAiABKAsyCS5UeXBlZEtleVIXaWRlbnRpdHlNYXN0ZXJSZWNvcmRL'
'EkEKF3JlbW90ZV9jb252ZXJzYXRpb25fa2V5GAMgASgLMgkuVHlwZWRLZXlSFXJlbW90ZUNvbn' 'ZXkSQQoXcmVtb3RlX2NvbnZlcnNhdGlvbl9rZXkYAyABKAsyCS5UeXBlZEtleVIVcmVtb3RlQ2'
'ZlcnNhdGlvbktleQ=='); '9udmVyc2F0aW9uS2V5');
@$core.Deprecated('Use signedContactResponseDescriptor instead') @$core.Deprecated('Use signedContactResponseDescriptor instead')
const SignedContactResponse$json = { const SignedContactResponse$json = {

View File

@ -307,7 +307,7 @@ message ContactRequestPrivate {
CryptoKey writer_key = 1; CryptoKey writer_key = 1;
// Snapshot of profile // Snapshot of profile
Profile profile = 2; Profile profile = 2;
// Identity master dht key // Identity master DHT record key
TypedKey identity_master_record_key = 3; TypedKey identity_master_record_key = 3;
// Local chat DHT record key // Local chat DHT record key
TypedKey chat_record_key = 4; TypedKey chat_record_key = 4;
@ -319,8 +319,8 @@ message ContactRequestPrivate {
message ContactResponse { message ContactResponse {
// Accept or reject // Accept or reject
bool accept = 1; bool accept = 1;
// Account master record key // Remote identity master DHT record key
TypedKey account_master_record_key = 2; TypedKey identity_master_record_key = 2;
// Remote chat DHT record key if accepted // Remote chat DHT record key if accepted
TypedKey remote_conversation_key = 3; TypedKey remote_conversation_key = 3;
} }

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -5,10 +7,13 @@ import 'package:split_view/split_view.dart';
import 'package:signal_strength_indicator/signal_strength_indicator.dart'; import 'package:signal_strength_indicator/signal_strength_indicator.dart';
import '../components/chat_component.dart'; import '../components/chat_component.dart';
import '../providers/account.dart';
import '../providers/contact.dart';
import '../providers/local_accounts.dart'; import '../providers/local_accounts.dart';
import '../providers/logins.dart'; import '../providers/logins.dart';
import '../providers/window_control.dart'; import '../providers/window_control.dart';
import '../tools/tools.dart'; import '../tools/tools.dart';
import '../veilid_support/dht_support/dht_record_pool.dart';
import 'main_pager/main_pager.dart'; import 'main_pager/main_pager.dart';
class HomePage extends ConsumerStatefulWidget { class HomePage extends ConsumerStatefulWidget {
@ -19,10 +24,17 @@ class HomePage extends ConsumerStatefulWidget {
HomePageState createState() => HomePageState(); HomePageState createState() => HomePageState();
} }
// XXX Eliminate this when we have ValueChanged
const int ticksPerContactInvitationCheck = 5;
class HomePageState extends ConsumerState<HomePage> class HomePageState extends ConsumerState<HomePage>
with TickerProviderStateMixin { with TickerProviderStateMixin {
final _unfocusNode = FocusNode(); final _unfocusNode = FocusNode();
Timer? _homeTickTimer;
bool _inHomeTick = false;
int _contactInvitationCheckTick = 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -31,15 +43,69 @@ class HomePageState extends ConsumerState<HomePage>
setState(() {}); setState(() {});
await ref.read(windowControlProvider.notifier).changeWindowSetup( await ref.read(windowControlProvider.notifier).changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal); TitleBarStyle.normal, OrientationCapability.normal);
_homeTickTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!_inHomeTick) {
unawaited(_onHomeTick());
}
});
}); });
} }
@override @override
void dispose() { void dispose() {
final homeTickTimer = _homeTickTimer;
if (homeTickTimer != null) {
homeTickTimer.cancel();
}
_unfocusNode.dispose(); _unfocusNode.dispose();
super.dispose(); super.dispose();
} }
Future<void> _onHomeTick() async {
_inHomeTick = true;
try {
// Check extant contact invitations once every 5 seconds
_contactInvitationCheckTick += 1;
if (_contactInvitationCheckTick >= ticksPerContactInvitationCheck) {
_contactInvitationCheckTick = 0;
await _doContactInvitationCheck();
}
} finally {
_inHomeTick = false;
}
}
Future<void> _doContactInvitationCheck() async {
final contactInvitationRecords =
await ref.read(fetchContactInvitationRecordsProvider.future);
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
if (contactInvitationRecords == null || activeAccountInfo == null) {
return;
}
final allChecks = <Future<void>>[];
for (final contactInvitationRecord in contactInvitationRecords) {
allChecks.add(() async {
final acceptReject = await checkAcceptRejectContact(
activeAccountInfo: activeAccountInfo,
contactInvitationRecord: contactInvitationRecord);
if (acceptReject != null) {
if (acceptReject) {
// Accept
ref
..invalidate(fetchContactInvitationRecordsProvider)
..invalidate(fetchContactListProvider);
} else {
// Reject
ref.invalidate(fetchContactInvitationRecordsProvider);
}
}
}());
}
await Future.wait(allChecks);
}
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget buildPhone(BuildContext context) { Widget buildPhone(BuildContext context) {
// //

View File

@ -14,8 +14,8 @@ import '../entities/proto.dart'
ContactInvitationRecord, ContactInvitationRecord,
ContactRequest, ContactRequest,
ContactRequestPrivate, ContactRequestPrivate,
SignedContactInvitation,
ContactResponse, ContactResponse,
SignedContactInvitation,
SignedContactResponse; SignedContactResponse;
import '../log/loggy.dart'; import '../log/loggy.dart';
import '../tools/tools.dart'; import '../tools/tools.dart';
@ -24,9 +24,90 @@ import 'account.dart';
part 'contact.g.dart'; part 'contact.g.dart';
Future<void> deleteContactInvitation( Future<bool?> checkAcceptRejectContact(
{required ActiveAccountInfo activeAccountInfo, {required ActiveAccountInfo activeAccountInfo,
required ContactInvitationRecord contactInvitationRecord}) async { required ContactInvitationRecord contactInvitationRecord}) async {
// Open the contact request inbox
try {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final writerKey =
proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerKey);
final writerSecret =
proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerSecret);
final writer = TypedKeyPair(
kind: contactInvitationRecord.contactRequestInbox.recordKey.kind,
key: writerKey,
secret: writerSecret);
final acceptReject = await (await pool.openRead(
proto.TypedKeyProto.fromProto(
contactInvitationRecord.contactRequestInbox.recordKey),
crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer),
parent: accountRecordKey,
defaultSubkey: 1))
.scope((contactRequestInbox) async {
//
final signedContactResponse = await contactRequestInbox
.getProtobuf(SignedContactResponse.fromBuffer, forceRefresh: true);
if (signedContactResponse == null) {
return null;
}
final contactResponseBytes =
Uint8List.fromList(signedContactResponse.contactResponse);
final contactResponse = ContactResponse.fromBuffer(contactResponseBytes);
final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto(
contactResponse.identityMasterRecordKey);
final cs = await pool.veilid.getCryptoSystem(
contactInvitationRecord.contactRequestInbox.recordKey.kind);
// Fetch the remote contact's account master
final contactIdentityMaster = await openIdentityMaster(
identityMasterRecordKey: contactIdentityMasterRecordKey);
// Verify
final signature = proto.SignatureProto.fromProto(
signedContactResponse.identitySignature);
try {
await cs.verify(contactIdentityMaster.identityPublicKey,
contactResponseBytes, signature);
} on Exception catch (e) {
log.error('Bad identity used, failed to verify: $e');
return false;
}
return contactResponse.accept;
});
if (acceptReject == null) {
return null;
}
// Add contact if accepted
if (acceptReject) {
//
await deleteContactInvitation(
accepted: true,
activeAccountInfo: activeAccountInfo,
contactInvitationRecord: contactInvitationRecord);
return true;
} else {
await deleteContactInvitation(
accepted: false,
activeAccountInfo: activeAccountInfo,
contactInvitationRecord: contactInvitationRecord);
return false;
}
} on Exception catch (e) {
log.error('Exception in checkAcceptRejectContact: $e');
return null;
}
}
Future<void> deleteContactInvitation(
{required bool accepted,
required ActiveAccountInfo activeAccountInfo,
required ContactInvitationRecord contactInvitationRecord}) async {
final pool = await DHTRecordPool.instance(); final pool = await DHTRecordPool.instance();
final accountRecordKey = final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
@ -53,12 +134,18 @@ Future<void> deleteContactInvitation(
proto.OwnedDHTRecordPointerProto.fromProto( proto.OwnedDHTRecordPointerProto.fromProto(
contactInvitationRecord.contactRequestInbox), contactInvitationRecord.contactRequestInbox),
parent: accountRecordKey)) parent: accountRecordKey))
.delete(); .scope((contactRequestInbox) async {
// Wipe out old invitation so it shows up as invalid
await contactRequestInbox.tryWriteBytes(Uint8List(0));
await contactRequestInbox.delete();
});
if (!accepted) {
await (await pool.openOwned( await (await pool.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto( proto.OwnedDHTRecordPointerProto.fromProto(
contactInvitationRecord.localConversation), contactInvitationRecord.localConversation),
parent: accountRecordKey)) parent: accountRecordKey))
.delete(); .delete();
}
}); });
} }
@ -255,7 +342,7 @@ Future<void> acceptContactInvitation(ActiveAccountInfo activeAccountInfo,
// xxx // xxx
final contactResponse = ContactResponse() final contactResponse = ContactResponse()
..accept = false ..accept = false
..accountMasterRecordKey = activeAccountInfo ..identityMasterRecordKey = activeAccountInfo
.localAccount.identityMaster.masterRecordKey .localAccount.identityMaster.masterRecordKey
.toProto(); .toProto();
final contactResponseBytes = contactResponse.writeToBuffer(); final contactResponseBytes = contactResponse.writeToBuffer();
@ -290,7 +377,7 @@ Future<void> rejectContactInvitation(ActiveAccountInfo activeAccountInfo,
final contactResponse = ContactResponse() final contactResponse = ContactResponse()
..accept = false ..accept = false
..accountMasterRecordKey = activeAccountInfo ..identityMasterRecordKey = activeAccountInfo
.localAccount.identityMaster.masterRecordKey .localAccount.identityMaster.masterRecordKey
.toProto(); .toProto();
final contactResponseBytes = contactResponse.writeToBuffer(); final contactResponseBytes = contactResponse.writeToBuffer();

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart';
import 'package:veilid/veilid.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../veilid_support.dart'; import '../veilid_support.dart';
@ -51,7 +50,7 @@ class DHTRecord {
} }
final pool = await DHTRecordPool.instance(); final pool = await DHTRecordPool.instance();
await _routingContext.closeDHTRecord(_recordDescriptor.key); await _routingContext.closeDHTRecord(_recordDescriptor.key);
pool.recordClosed(this); pool.recordClosed(_recordDescriptor.key);
_open = false; _open = false;
} }
@ -71,18 +70,24 @@ class DHTRecord {
try { try {
return await scopeFunction(this); return await scopeFunction(this);
} finally { } finally {
if (_valid) {
await close(); await close();
} }
} }
}
Future<T> deleteScope<T>( Future<T> deleteScope<T>(
FutureOr<T> Function(DHTRecord) scopeFunction) async { FutureOr<T> Function(DHTRecord) scopeFunction) async {
try { try {
final out = await scopeFunction(this); final out = await scopeFunction(this);
if (_valid && _open) {
await close(); await close();
}
return out; return out;
} on Exception catch (_) { } on Exception catch (_) {
if (_valid) {
await delete(); await delete();
}
rethrow; rethrow;
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:mutex/mutex.dart';
import '../../log/loggy.dart'; import '../../log/loggy.dart';
import '../veilid_support.dart'; import '../veilid_support.dart';
@ -38,14 +39,14 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext) DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext)
: _state = DHTRecordPoolAllocations( : _state = DHTRecordPoolAllocations(
childrenByParent: IMap(), parentByChild: IMap()), childrenByParent: IMap(), parentByChild: IMap()),
_opened = <TypedKey, DHTRecord>{}, _opened = <TypedKey, Mutex>{},
_routingContext = routingContext, _routingContext = routingContext,
_veilid = veilid; _veilid = veilid;
// Persistent DHT record list // Persistent DHT record list
DHTRecordPoolAllocations _state; DHTRecordPoolAllocations _state;
// Which DHT records are currently open // Which DHT records are currently open
final Map<TypedKey, DHTRecord> _opened; final Map<TypedKey, Mutex> _opened;
// Default routing context to use for new keys // Default routing context to use for new keys
final VeilidRoutingContext _routingContext; final VeilidRoutingContext _routingContext;
// Convenience accessor // Convenience accessor
@ -89,14 +90,20 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
Veilid get veilid => _veilid; Veilid get veilid => _veilid;
void _recordOpened(DHTRecord record) { Future<void> _recordOpened(TypedKey key) async {
assert(!_opened.containsKey(record.key), 'record already opened'); // no race because dart is single threaded until async breaks
_opened[record.key] = record; final m = _opened[key] ?? Mutex();
_opened[key] = m;
await m.acquire();
_opened[key] = m;
} }
void recordClosed(DHTRecord record) { void recordClosed(TypedKey key) {
assert(_opened.containsKey(record.key), 'record already closed'); final m = _opened.remove(key);
_opened.remove(record.key); if (m == null) {
throw StateError('record already closed');
}
m.release();
} }
Future<void> deleteDeep(TypedKey parent) async { Future<void> deleteDeep(TypedKey parent) async {
@ -191,7 +198,8 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
if (parent != null) { if (parent != null) {
await _addDependency(parent, rec.key); await _addDependency(parent, rec.key);
} }
_recordOpened(rec);
await _recordOpened(rec.key);
return rec; return rec;
} }
@ -202,6 +210,10 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
TypedKey? parent, TypedKey? parent,
int defaultSubkey = 0, int defaultSubkey = 0,
DHTRecordCrypto? crypto}) async { DHTRecordCrypto? crypto}) async {
await _recordOpened(recordKey);
late final DHTRecord rec;
try {
// 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
final existingParent = _state.parentByChild[recordKey.toJson()]; final existingParent = _state.parentByChild[recordKey.toJson()];
@ -210,7 +222,7 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
// Open from the veilid api // Open from the veilid api
final dhtctx = routingContext ?? _routingContext; final dhtctx = routingContext ?? _routingContext;
final recordDescriptor = await dhtctx.openDHTRecord(recordKey, null); final recordDescriptor = await dhtctx.openDHTRecord(recordKey, null);
final rec = DHTRecord( rec = DHTRecord(
routingContext: dhtctx, routingContext: dhtctx,
recordDescriptor: recordDescriptor, recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey, defaultSubkey: defaultSubkey,
@ -220,7 +232,10 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
if (parent != null) { if (parent != null) {
await _addDependency(parent, rec.key); await _addDependency(parent, rec.key);
} }
_recordOpened(rec); } on Exception catch (_) {
recordClosed(recordKey);
rethrow;
}
return rec; return rec;
} }
@ -234,6 +249,10 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
int defaultSubkey = 0, int defaultSubkey = 0,
DHTRecordCrypto? crypto, DHTRecordCrypto? crypto,
}) async { }) async {
await _recordOpened(recordKey);
late final DHTRecord rec;
try {
// 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
final existingParent = _state.parentByChild[recordKey.toJson()]; final existingParent = _state.parentByChild[recordKey.toJson()];
@ -242,7 +261,7 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
// Open from the veilid api // Open from the veilid api
final dhtctx = routingContext ?? _routingContext; final dhtctx = routingContext ?? _routingContext;
final recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer); final recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer);
final rec = DHTRecord( rec = DHTRecord(
routingContext: dhtctx, routingContext: dhtctx,
recordDescriptor: recordDescriptor, recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey, defaultSubkey: defaultSubkey,
@ -255,7 +274,10 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
if (parent != null) { if (parent != null) {
await _addDependency(parent, rec.key); await _addDependency(parent, rec.key);
} }
_recordOpened(rec); } on Exception catch (_) {
recordClosed(recordKey);
rethrow;
}
return rec; return rec;
} }

View File

@ -781,6 +781,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.7.8" version: "2.7.8"
mutex:
dependency: "direct main"
description:
name: mutex
sha256: "03116a4e46282a671b46c12de649d72c0ed18188ffe12a8d0fc63e83f4ad88f4"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
octo_image: octo_image:
dependency: transitive dependency: transitive
description: description:

View File

@ -44,6 +44,7 @@ dependencies:
json_annotation: ^4.8.1 json_annotation: ^4.8.1
loggy: ^2.0.3 loggy: ^2.0.3
motion_toast: ^2.7.8 motion_toast: ^2.7.8
mutex: ^3.0.1
path: ^1.8.2 path: ^1.8.2
path_provider: ^2.0.11 path_provider: ^2.0.11
pinput: ^2.3.0 pinput: ^2.3.0