Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/demo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:

- name: Build ObjC Demo
run: |
clang -fobjc-arc -framework Foundation -framework Network \
clang -fobjc-arc -framework Foundation -framework Network -framework Security \
-I ObjC/NWAsyncSocketObjC/include \
ObjC/NWAsyncSocketObjC/NWStreamBuffer.m \
ObjC/NWAsyncSocketObjC/NWSSEParser.m \
Expand Down
60 changes: 56 additions & 4 deletions ObjC/NWAsyncSocketObjC/GCDAsyncSocket.m
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ @interface GCDAsyncSocket ()
// TLS
@property (nonatomic, assign) BOOL tlsEnabled;

// Write queue tracking
@property (nonatomic, assign) NSUInteger pendingWriteCount;
@property (nonatomic, assign) BOOL flagDisconnectAfterWrites;
@property (nonatomic, assign) BOOL flagDisconnectAfterReads;

@end

@implementation GCDAsyncSocket
Expand Down Expand Up @@ -674,11 +679,35 @@ - (void)disconnect {
}

- (void)disconnectAfterWriting {
[self disconnect];
__weak typeof(self) weakSelf = self;
dispatch_async(self.socketQueue, ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;

strongSelf.flagDisconnectAfterWrites = YES;
// If no writes are in flight, disconnect immediately.
// Otherwise the send-completion handler will disconnect
// once the last pending write finishes.
if (strongSelf.pendingWriteCount == 0) {
[strongSelf disconnectInternalWithError:nil];
}
});
}

- (void)disconnectAfterReading {
[self disconnect];
__weak typeof(self) weakSelf = self;
dispatch_async(self.socketQueue, ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;

strongSelf.flagDisconnectAfterReads = YES;
// If no read requests are pending, disconnect immediately.
// Otherwise the read-completion callback will disconnect
// once the last pending read request is fulfilled.
if (strongSelf.readQueue.count == 0) {
[strongSelf disconnectInternalWithError:nil];
}
});
}

#pragma mark - Reading
Expand Down Expand Up @@ -773,9 +802,15 @@ - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)t
strongSelf.socketQueue, timeoutBlock);
}

strongSelf.pendingWriteCount++;

// is_complete must be false so the TCP stream stays open for
// subsequent writes (e.g. HTTP header followed by body).
// Passing true here would send a TCP FIN after each write,
// closing the write side of the connection prematurely.
nw_connection_send(strongSelf.connection, dispatchData,
NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT,
true, ^(nw_error_t _Nullable error) {
false, ^(nw_error_t _Nullable error) {
writeCompleted = YES;
if (timeoutBlock) {
dispatch_block_cancel(timeoutBlock);
Expand All @@ -785,19 +820,28 @@ - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)t
__strong typeof(weakSelf) sself = weakSelf;
if (!sself) return;

// This completion handler fires on socketQueue (set via
// nw_connection_set_queue), so we can safely mutate state.
sself.pendingWriteCount--;

if (error) {
NSError *nsError = [sself socketErrorWithCode:GCDAsyncSocketErrorConnectionFailed
description:@"Write failed."
reason:@"nw_connection_send failed"
nwError:error];
[sself disconnectWithError:nsError];
[sself disconnectInternalWithError:nsError];
} else {
Comment on lines +823 to 833
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pendingWriteCount is decremented unconditionally in the nw_connection_send completion. If disconnectInternalWithError: runs while writes are in-flight (e.g., user calls disconnect, a read error triggers disconnect, or a send error causes disconnect while other sends are still pending), it currently resets pendingWriteCount to 0 and cancels the connection; subsequent send completions can still fire and will underflow the NSUInteger counter. Consider guarding the decrement (only decrement when > 0) and/or not resetting the counter until all in-flight completions have been accounted for (e.g., mark a disconnecting state and ignore late completions).

Copilot uses AI. Check for mistakes.
dispatch_async(sself.delegateQueue, ^{
id delegate = sself.delegate;
if ([delegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) {
[delegate socket:sself didWriteDataWithTag:tag];
}
});

// Check if we should disconnect after all writes complete
if (sself.flagDisconnectAfterWrites && sself.pendingWriteCount == 0) {
[sself disconnectInternalWithError:nil];
}
}
});
});
Expand Down Expand Up @@ -1013,6 +1057,11 @@ - (void)processReadQueue {
}
}
}

// Check if we should disconnect after all read requests are fulfilled
if (self.flagDisconnectAfterReads && self.readQueue.count == 0) {
[self disconnectInternalWithError:nil];
}
}

#pragma mark - Private: Disconnect
Expand All @@ -1035,6 +1084,9 @@ - (void)disconnectInternalWithError:(NSError *)error {

self.isConnected = NO;
self.isReadingContinuously = NO;
self.flagDisconnectAfterWrites = NO;
self.flagDisconnectAfterReads = NO;
self.pendingWriteCount = 0;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disconnectInternalWithError: resets pendingWriteCount to 0 even though nw_connection_send completions may still be pending after nw_connection_cancel. This can lead to counter underflow in the send completion handler and can also incorrectly satisfy pendingWriteCount == 0 checks (e.g., disconnect-after-writing). Prefer keeping the counter accurate until completions drain (or introduce a separate disconnecting flag and make the completion handler no-op once disconnect starts).

Suggested change
self.pendingWriteCount = 0;

Copilot uses AI. Check for mistakes.

#if NW_FRAMEWORK_AVAILABLE
if (self.listener) {
Expand Down
2 changes: 1 addition & 1 deletion ObjC/ObjCDemo/main.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// 6. GCDAsyncSocket — Server socket API (accept/listen)
//
// Build (from repository root):
// clang -framework Foundation \
// clang -framework Foundation -framework Network -framework Security \
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build comment here still omits -fobjc-arc, but GCDAsyncSocket.m uses __weak (ARC-only) and the README/CI build commands include ARC. Please update this build example to include -fobjc-arc so the documented command actually compiles.

Suggested change
// clang -framework Foundation -framework Network -framework Security \
// clang -fobjc-arc -framework Foundation -framework Network -framework Security \

Copilot uses AI. Check for mistakes.
// -I ObjC/NWAsyncSocketObjC/include \
// ObjC/NWAsyncSocketObjC/NWStreamBuffer.m \
// ObjC/NWAsyncSocketObjC/NWSSEParser.m \
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ The demo menu lets you test each component individually or run all at once:
Build the ObjC CLI demo on macOS:

```bash
clang -framework Foundation \
clang -fobjc-arc -framework Foundation -framework Network -framework Security \
-I ObjC/NWAsyncSocketObjC/include \
ObjC/NWAsyncSocketObjC/NWStreamBuffer.m \
ObjC/NWAsyncSocketObjC/NWSSEParser.m \
Expand Down