Skip to content

Commit

Permalink
6.4.6 (#1288)
Browse files Browse the repository at this point in the history
- Fix regression when connecting solely using old STARTTLS connection
method
- Fix crash when being unable to connect while creating new account
- Fix connection problems on slow connections
- Show timestamp of last received push in notification debug menu
- Fix various other crashes
  • Loading branch information
tmolitor-stud-tu authored Nov 19, 2024
2 parents 18ca827 + b31093f commit cbff369
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 79 deletions.
2 changes: 1 addition & 1 deletion Monal/Classes/ActiveChatsViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, weak) IBOutlet UIBarButtonItem* settingsButton;
@property (weak, nonatomic) IBOutlet UIBarButtonItem* spinnerButton;
@property (nonatomic, weak) IBOutlet UIBarButtonItem* composeButton;
@property (nonatomic, strong) chatViewController* currentChatViewController;
@property (nonatomic, strong) UIActivityIndicatorView* spinner;
@property (atomic, readonly) chatViewController* _Nullable currentChatView;

-(void) showCallContactNotFoundAlert:(NSString*) jid;
-(void) callContact:(MLContact*) contact withUIKitSender:(_Nullable id) sender;
Expand Down
19 changes: 15 additions & 4 deletions Monal/Classes/ActiveChatsViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -361,13 +361,13 @@ -(void) refreshDisplay

dispatch_async(dispatch_get_main_queue(), ^{
//make sure we don't display a chat view for a disabled account
if(self.currentChatViewController != nil && self.currentChatViewController.contact != nil)
if([MLNotificationManager sharedInstance].currentContact != nil)
{
BOOL found = NO;
for(NSDictionary* accountDict in [[DataLayer sharedInstance] enabledAccountList])
{
NSNumber* accountNo = accountDict[kAccountID];
if(self.currentChatViewController.contact.accountId.intValue == accountNo.intValue)
NSNumber* accountId = accountDict[kAccountID];
if([MLNotificationManager sharedInstance].currentContact.accountId.intValue == accountId.intValue)
found = YES;
}
if(!found)
Expand Down Expand Up @@ -879,6 +879,7 @@ -(void) presentSplitPlaceholder
UIViewController* detailsViewController = [[SwiftuiInterface new] makeViewWithName:@"ChatPlaceholder"];
[self showDetailViewController:detailsViewController sender:self];
}
[MLNotificationManager sharedInstance].currentContact = nil;
}

-(void) showNotificationSettings
Expand Down Expand Up @@ -1094,7 +1095,6 @@ -(void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id) sender
UIBarButtonItem* barButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil];
self.navigationItem.backBarButtonItem = barButtonItem;
[chatVC setupWithContact:sender];
self.currentChatViewController = chatVC;
}
else if([segue.identifier isEqualToString:@"showDetails"])
{
Expand Down Expand Up @@ -1513,6 +1513,17 @@ -(void) dismissRecursorWithViewControllers:(NSMutableArray*) viewControllers ani
}
}

-(chatViewController* _Nullable) currentChatView
{
NSArray* controllers = ((UINavigationController*)self.splitViewController.viewControllers[0]).viewControllers;
chatViewController* chatView = nil;
if(controllers.count > 1)
chatView = [((UINavigationController*)controllers[1]).viewControllers firstObject];
if(![chatView isKindOfClass:NSClassFromString(@"chatViewController")])
chatView = nil;
return chatView;
}

-(void) scrollToContact:(MLContact*) contact
{
__block NSIndexPath* indexPath = nil;
Expand Down
4 changes: 4 additions & 0 deletions Monal/Classes/HelperTools.m
Original file line number Diff line number Diff line change
Expand Up @@ -2352,8 +2352,12 @@ +(NSString*) encodeRandomResource
u_int32_t i=arc4random();
#if TARGET_OS_MACCATALYST
NSString* resource = [NSString stringWithFormat:@"Monal-macOS.%@", [self hexadecimalString:[NSData dataWithBytes: &i length: sizeof(i)]]];
#else
#if IS_QUICKSY
NSString* resource = [NSString stringWithFormat:@"Quicksy-iOS.%@", [self hexadecimalString:[NSData dataWithBytes: &i length: sizeof(i)]]];
#else
NSString* resource = [NSString stringWithFormat:@"Monal-iOS.%@", [self hexadecimalString:[NSData dataWithBytes: &i length: sizeof(i)]]];
#endif
#endif
return resource;
}
Expand Down
27 changes: 25 additions & 2 deletions Monal/Classes/MLDelayableTimer.m
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ -(void) start
return;
}
DDLogDebug(@"Starting timer: %@", self);
[[HelperTools getExtraRunloopWithIdentifier:MLRunLoopIdentifierTimer] addTimer:_wrappedTimer forMode:NSRunLoopCommonModes];
//scheduling and unscheduling of a timer must be done from the same thread --> use our runloop
[self scheduleBlockInRunLoop:^{
[[HelperTools getExtraRunloopWithIdentifier:MLRunLoopIdentifierTimer] addTimer:self->_wrappedTimer forMode:NSRunLoopCommonModes];
}];
}
}

Expand Down Expand Up @@ -131,8 +134,28 @@ -(void) invalidate
return;
}
//DDLogVerbose(@"Invalidating timer: %@", self);
[_wrappedTimer invalidate];
//scheduling and unscheduling of a timer must be done from the same thread --> use our runloop
[self scheduleBlockInRunLoop:^{
[self->_wrappedTimer invalidate];
}];
}
}

-(void) scheduleBlockInRunLoop:(monal_void_block_t) block
{
NSRunLoop* runLoop = [HelperTools getExtraRunloopWithIdentifier:MLRunLoopIdentifierTimer];
// NSCondition* condition = [NSCondition new];
// [condition lock];
CFRunLoopPerformBlock([runLoop getCFRunLoop], (__bridge CFStringRef)NSDefaultRunLoopMode, ^{
block();
// [condition lock];
// [condition signal];
// [condition unlock];
});
CFRunLoopWakeUp([runLoop getCFRunLoop]); //trigger wakeup of runloop to execute the block as soon as possible
// //wait for our block to finish executing
// [condition wait];
// [condition unlock];
}

@end
12 changes: 6 additions & 6 deletions Monal/Classes/MLStream.m
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ @interface MLInputStream()
//(mutexes can not be unlocked in a thread different from the one it got locked in and NSLock internally uses mutext --> both can not be used)
dispatch_semaphore_t _read_sem;
}
@property (atomic, readonly) void (^incoming_data_handler)(NSData* _Nullable, BOOL, NSError* _Nullable, BOOL allow_next_read);
@property (atomic, readonly) void (^incoming_data_handler)(NSData* _Nullable, BOOL, NSError* _Nullable, BOOL);
@end

@interface MLOutputStream()
Expand Down Expand Up @@ -89,7 +89,7 @@ -(instancetype) initWithSharedState:(MLSharedStreamState*) shared
//this handler will be called by the schedule_read method
//since the framer swallows all data, nw_connection_receive() and the framer cannot race against each other and deliver reordered data
weakify(self);
_incoming_data_handler = ^(NSData* _Nullable content, BOOL is_complete, NSError* _Nullable st_error, BOOL allow_next_read) {
_incoming_data_handler = ^(NSData* _Nullable content, BOOL is_complete, NSError* _Nullable st_error, BOOL polling_active) {
strongify(self);
if(self == nil)
return;
Expand Down Expand Up @@ -142,7 +142,7 @@ -(instancetype) initWithSharedState:(MLSharedStreamState*) shared
[self generateEvent:NSStreamEventEndEncountered];

//try to read again
if(!is_complete && !generate_bytes_available_event && allow_next_read)
if(!is_complete && !generate_error_event && !generate_bytes_available_event && polling_active)
[self schedule_read];
};
return self;
Expand Down Expand Up @@ -235,8 +235,8 @@ -(void) schedule_read
DDLogDebug(@"now calling nw_framer_parse_input inside framer queue");
nw_framer_parse_input(self.shared_state.framer, 1, BUFFER_SIZE, nil, ^size_t(uint8_t* buffer, size_t buffer_length, bool is_complete) {
DDLogDebug(@"nw_framer_parse_input got callback with is_complete:%@, length=%zu", bool2str(is_complete), (unsigned long)buffer_length);
//we only want to allow new calls to schedule_read if we received some data --> set last arg accordingly
self.incoming_data_handler([NSData dataWithBytes:buffer length:buffer_length], is_complete, nil, buffer_length > 0);
//we don't want to do "polling" here, our next nw_framer_parse_input will be triggered by the nw_framer_set_input_handler block
self.incoming_data_handler([NSData dataWithBytes:buffer length:buffer_length], is_complete, nil, NO);
return buffer_length;
});
});
Expand All @@ -249,7 +249,7 @@ -(void) schedule_read
NSError* st_error = nil;
if(receive_error)
st_error = (NSError*)CFBridgingRelease(nw_error_copy_cf_error(receive_error));
//we always want to allow new calls to schedule_read --> set last arg to YES
//we want to do "polling" here (e.g. start our next blocking nw_connection_receive call if we did not receive new data nor any error)
self.incoming_data_handler((NSData*)content, is_complete, st_error, YES);
});
}
Expand Down
8 changes: 4 additions & 4 deletions Monal/Classes/MonalAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -1972,10 +1972,10 @@ -(void) sendAllOutboxes
monal_id_block_t cleanup = ^(NSDictionary* payload) {
[[DataLayer sharedInstance] deleteShareSheetPayloadWithId:payload[@"id"]];
[[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil];
if(self.activeChats.currentChatViewController != nil)
if(self.activeChats.currentChatView != nil)
{
[self.activeChats.currentChatViewController scrollToBottomAnimated:NO];
[self.activeChats.currentChatViewController hideUploadHUD];
[self.activeChats.currentChatView scrollToBottomAnimated:NO];
[self.activeChats.currentChatView hideUploadHUD];
}
//send next item (if there is one left)
[self sendAllOutboxes];
Expand Down Expand Up @@ -2007,7 +2007,7 @@ -(void) sendAllOutboxes
else if([payload[@"type"] isEqualToString:@"image"] || [payload[@"type"] isEqualToString:@"file"] || [payload[@"type"] isEqualToString:@"contact"] || [payload[@"type"] isEqualToString:@"audiovisual"])
{
DDLogInfo(@"Got %@ upload: %@", payload[@"type"], payload[@"data"]);
[self.activeChats.currentChatViewController showUploadHUD];
[self.activeChats.currentChatView showUploadHUD];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
$call(payload[@"data"], $ID(account), $BOOL(encrypted), $ID(completion, (^(NSString* url, NSString* mimeType, NSNumber* size, NSError* error) {
dispatch_async(dispatch_get_main_queue(), ^{
Expand Down
3 changes: 2 additions & 1 deletion Monal/Classes/chatViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -3560,7 +3560,8 @@ -(NSInteger)collectionView:(nonnull UICollectionView*) collectionView numberOfIt

-(void) notifyUploadQueueRemoval:(NSUInteger) index
{
MLAssert(index < self.uploadQueue.count, @"index is only allowed to be smaller than uploadQueue.count");
if(index >= self.uploadQueue.count)
return;
[self.uploadMenuView performBatchUpdates:^{
[self deleteQueueItemAtIndex:index];
} completion:^(BOOL finished) {
Expand Down
133 changes: 72 additions & 61 deletions Monal/Classes/xmpp.m
Original file line number Diff line number Diff line change
Expand Up @@ -829,19 +829,21 @@ -(void) unfreeze
//this operation has highest priority to make sure it will be executed first once unfrozen
NSBlockOperation* unfreezeOperation = [NSBlockOperation blockOperationWithBlock:^{
//this has to be the very first thing even before unfreezing the parse or send queues
if(self.accountState < kStateReconnecting)
{
DDLogInfo(@"Reloading UNfrozen account %@", self.accountNo);
//(re)read persisted state (could be changed by appex)
[self readState];
@synchronized(self->_stateLockObject) {
if(self.accountState < kStateReconnecting)
{
DDLogInfo(@"Reloading UNfrozen account %@", self.accountNo);
//(re)read persisted state (could be changed by appex)
[self readState];
}
else
DDLogInfo(@"Not reloading UNfrozen account %@, already connected", self.accountNo);

//this must be inside the dispatch async, because it will dispatch *SYNC* to the receive queue and potentially block or even deadlock the system
[self unfreezeParseQueue];

[self unfreezeSendQueue];
}
else
DDLogInfo(@"Not reloading UNfrozen account %@, already connected", self.accountNo);

//this must be inside the dispatch async, because it will dispatch *SYNC* to the receive queue and potentially block or even deadlock the system
[self unfreezeParseQueue];

[self unfreezeSendQueue];
}];
unfreezeOperation.queuePriority = NSOperationQueuePriorityVeryHigh; //make sure this will become the first operation executed once unfrozen
[self->_receiveQueue addOperations: @[unfreezeOperation] waitUntilFinished:NO];
Expand Down Expand Up @@ -962,11 +964,67 @@ -(void) disconnectWithStreamError:(MLXMLNode* _Nullable) streamError
-(void) disconnectWithStreamError:(MLXMLNode* _Nullable) streamError andExplicitLogout:(BOOL) explicitLogout
{
DDLogInfo(@"disconnect called...");
//commonly used by shortcut outside of receive queue and called from inside the receive queue, too
monal_void_block_t doExplicitLogout = ^{
@synchronized(self->_stateLockObject) {
DDLogVerbose(@"explicitLogout == YES --> clearing state");

//preserve unAckedStanzas even on explicitLogout and resend them on next connect
//if we don't do this, messages could get lost when logging out directly after sending them
//and: sending messages twice is less intrusive than silently loosing them
NSMutableArray* stanzas = self.unAckedStanzas;

//reset smacks state to sane values (this can be done even if smacks is not supported)
[self initSM3];
self.unAckedStanzas = stanzas;

//inform all old iq handlers of invalidation and clear _iqHandlers dictionary afterwards
@synchronized(self->_iqHandlers) {
for(NSString* iqid in [self->_iqHandlers allKeys])
{
DDLogWarn(@"Invalidating iq handler for iq id '%@'", iqid);
if(self->_iqHandlers[iqid][@"handler"] != nil)
$invalidate(self->_iqHandlers[iqid][@"handler"], $ID(account, self), $ID(reason, @"disconnect"));
else if(self->_iqHandlers[iqid][@"errorHandler"])
((monal_iq_handler_t)self->_iqHandlers[iqid][@"errorHandler"])(nil);
}
self->_iqHandlers = [NSMutableDictionary new];
}

//invalidate pubsub queue (*after* iq handlers that also might invalidate a result handler of the queued operation)
[self.pubsub invalidateQueue];

//clear pipeline cache
self->_pipeliningState = kPipelinedNothing;
self->_cachedStreamFeaturesBeforeAuth = nil;
self->_cachedStreamFeaturesAfterAuth = nil;

//clear all reconnection handlers
@synchronized(self->_reconnectionHandlers) {
[self->_reconnectionHandlers removeAllObjects];
}

//persist these changes
[self persistState];
}

[[DataLayer sharedInstance] resetContactsForAccount:self.accountNo];

//trigger view updates to make sure enabled/disabled account state propagates to all ui elements
[[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil];
};

//short-circuit common case without dispatching to receive queue
//this allows calling a noop disconnect while the receive queue is frozen
if(self->_accountState<kStateReconnecting && !explicitLogout)
//every change to our state is locked by our _stateLockObject and the receive queue unfreeze uses this lock, too
//--> an unfreeze can not happen half way through this explicit logout and therefore can't corrupt any state
//--> an unfreeze is needed to dispatch to the receive queue which is used by our connect method
if(self->_accountState<kStateReconnecting)
{
if(explicitLogout)
doExplicitLogout();
return;
}

MLAssert(!_receiveQueue.suspended, @"receive queue suspended while trying to disconnect!");

Expand Down Expand Up @@ -996,54 +1054,7 @@ -(void) disconnectWithStreamError:(MLXMLNode* _Nullable) streamError andExplicit
{
DDLogVerbose(@"not doing logout because already logged out, but clearing state if explicitLogout was yes");
if(explicitLogout)
{
@synchronized(self->_stateLockObject) {
DDLogVerbose(@"explicitLogout == YES --> clearing state");

//preserve unAckedStanzas even on explicitLogout and resend them on next connect
//if we don't do this, messages could get lost when logging out directly after sending them
//and: sending messages twice is less intrusive than silently loosing them
NSMutableArray* stanzas = self.unAckedStanzas;

//reset smacks state to sane values (this can be done even if smacks is not supported)
[self initSM3];
self.unAckedStanzas = stanzas;

//inform all old iq handlers of invalidation and clear _iqHandlers dictionary afterwards
@synchronized(self->_iqHandlers) {
for(NSString* iqid in [self->_iqHandlers allKeys])
{
DDLogWarn(@"Invalidating iq handler for iq id '%@'", iqid);
if(self->_iqHandlers[iqid][@"handler"] != nil)
$invalidate(self->_iqHandlers[iqid][@"handler"], $ID(account, self), $ID(reason, @"disconnect"));
else if(self->_iqHandlers[iqid][@"errorHandler"])
((monal_iq_handler_t)self->_iqHandlers[iqid][@"errorHandler"])(nil);
}
self->_iqHandlers = [NSMutableDictionary new];
}

//invalidate pubsub queue (*after* iq handlers that also might invalidate a result handler of the queued operation)
[self.pubsub invalidateQueue];

//clear pipeline cache
self->_pipeliningState = kPipelinedNothing;
self->_cachedStreamFeaturesBeforeAuth = nil;
self->_cachedStreamFeaturesAfterAuth = nil;

//clear all reconnection handlers
@synchronized(self->_reconnectionHandlers) {
[self->_reconnectionHandlers removeAllObjects];
}

//persist these changes
[self persistState];
}

[[DataLayer sharedInstance] resetContactsForAccount:self.accountNo];

//trigger view updates to make sure enabled/disabled account state propagates to all ui elements
[[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil];
}
doExplicitLogout();
return;
}
DDLogInfo(@"disconnecting");
Expand Down

0 comments on commit cbff369

Please sign in to comment.