Skip to content

Fix ObjC build error and add server socket API demo coverage#7

Merged
dustturtle merged 6 commits intomainfrom
copilot/add-server-socket-support
Mar 4, 2026
Merged

Fix ObjC build error and add server socket API demo coverage#7
dustturtle merged 6 commits intomainfrom
copilot/add-server-socket-support

Conversation

Copy link
Contributor

Copilot AI commented Mar 4, 2026

CI ObjC Demo (macOS) job fails due to a compilation error in GCDAsyncSocket.m. The new server-side APIs (acceptOnPort:, acceptOnInterface:port:, acceptOnUrl:) also lacked any test/demo coverage.

Build fixes

  • nw_endpoint_get_port return type: API returns uint16_t, not const char *. Removed incorrect pointer assignment and strtoul parsing.
  • isListening atomicity mismatch: Class extension declared nonatomic but public header declared atomic readonly. Aligned to atomic.

Server socket API demo (demo 6)

Added DemoSocketDelegate support for socket:didAcceptNewSocket: and a new interactive demo exercising all server-side APIs:

  • acceptOnPort: with port 0 (system-assigned)
  • Double-accept error handling
  • Client→Server loopback connection verifying didAcceptNewSocket: callback
  • disconnect stops listener, isListening → NO
  • acceptOnInterface:port: on localhost
  • acceptOnUrl: Unix Domain Socket
  • acceptOnUrl: non-file URL rejection

CI

Updated demo.yml ObjC runner input to include the new demo option.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits March 4, 2026 08:38
Implement acceptOnPort:error:, acceptOnInterface:port:error:, and
acceptOnUrl:error: using Network.framework's nw_listener_t. Add
socket:didAcceptNewSocket: delegate callback, isListening property,
and proper listener cleanup in disconnect/dealloc.

Co-authored-by: dustturtle <2305214+dustturtle@users.noreply.github.com>
…te sharing

- Use unix:// scheme for nw_endpoint_create_url instead of file:// URL
- Add comments explaining delegate sharing matches GCDAsyncSocket behavior

Co-authored-by: dustturtle <2305214+dustturtle@users.noreply.github.com>
Copilot AI changed the title [WIP] Add server socket support to Objective-C API Add server socket support (nw_listener_t) to ObjC GCDAsyncSocket Mar 4, 2026
…istening atomicity

- nw_endpoint_get_port() returns uint16_t, not const char*; remove
  incorrect pointer assignment and string conversion
- Change isListening from nonatomic to atomic in class extension to
  match the atomic readonly declaration in the public header

Co-authored-by: dustturtle <2305214+dustturtle@users.noreply.github.com>
Copilot AI changed the title Add server socket support (nw_listener_t) to ObjC GCDAsyncSocket Fix ObjC build: nw_endpoint_get_port returns uint16_t, fix isListening atomicity Mar 4, 2026
Copilot AI and others added 2 commits March 4, 2026 08:57
…nterface, acceptOnUrl tests

- Add didAcceptNewSocket: delegate to DemoSocketDelegate
- Demo 6a: acceptOnPort: with port 0 (system-assigned)
- Demo 6b: Double-accept error handling
- Demo 6c: Client→Server connection, verify didAcceptNewSocket:
- Demo 6d: Disconnect server, verify listener stops
- Demo 6e: acceptOnInterface:port: on localhost
- Demo 6f: acceptOnUrl: Unix Domain Socket
- Demo 6g: acceptOnUrl: non-file URL error rejection
- Update CI workflow for new demo option

Co-authored-by: dustturtle <2305214+dustturtle@users.noreply.github.com>
Co-authored-by: dustturtle <2305214+dustturtle@users.noreply.github.com>
Copilot AI changed the title Fix ObjC build: nw_endpoint_get_port returns uint16_t, fix isListening atomicity Fix ObjC build error and add server socket API demo coverage Mar 4, 2026
@dustturtle dustturtle marked this pull request as ready for review March 4, 2026 09:04
Copilot AI review requested due to automatic review settings March 4, 2026 09:04
@dustturtle dustturtle merged commit 6d2e213 into main Mar 4, 2026
5 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes an Objective-C compilation issue in GCDAsyncSocket.m and expands the ObjC demo/CI coverage to exercise the new server-side accept/listen APIs (including TCP, interface-bound TCP, and Unix Domain Sockets).

Changes:

  • Fix Network.framework port handling by treating nw_endpoint_get_port() as uint16_t and align isListening property atomicity with the public header.
  • Implement server socket APIs (acceptOnPort:, acceptOnInterface:port:, acceptOnUrl:) and add delegate callback support for accepted connections.
  • Add a new ObjC demo (demo 6) and update CI workflow input to run it as part of “run all demos”.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
ObjC/ObjCDemo/main.m Adds a new interactive demo (6) to exercise server/listener APIs and acceptance callback behavior.
ObjC/NWAsyncSocketObjC/include/GCDAsyncSocketDelegate.h Adds optional socket:didAcceptNewSocket: delegate API and documents retention requirement.
ObjC/NWAsyncSocketObjC/include/GCDAsyncSocket.h Exposes isListening and documents server accept APIs in the public interface.
ObjC/NWAsyncSocketObjC/GCDAsyncSocket.m Implements listener lifecycle, accept APIs, disconnect behavior updates, and fixes port type usage.
.github/workflows/demo.yml Updates scripted stdin to include the new demo in CI “run all demos”.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +593 to +601
#if NW_FRAMEWORK_AVAILABLE
// Stop listener if in server mode
if (strongSelf.listener) {
nw_listener_cancel(strongSelf.listener);
strongSelf.listener = nil;
strongSelf.isListening = NO;
strongSelf.localPort = 0;
}
#endif
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

disconnect cancels and nils listener (and sets isListening = NO) before calling disconnectInternalWithError:. This can make disconnectInternalWithError: hit its early-return guard for a listen-only socket, skipping state reset and socketDidDisconnect:withError:. Consider removing the pre-cancel block here and letting disconnectInternalWithError: handle listener cancellation/cleanup (or avoid clearing listener/isListening before calling into it).

Suggested change
#if NW_FRAMEWORK_AVAILABLE
// Stop listener if in server mode
if (strongSelf.listener) {
nw_listener_cancel(strongSelf.listener);
strongSelf.listener = nil;
strongSelf.isListening = NO;
strongSelf.localPort = 0;
}
#endif

Copilot uses AI. Check for mistakes.
Comment on lines +285 to +295
- (BOOL)acceptOnInterface:(NSString *)interface port:(uint16_t)port error:(NSError **)errPtr {
#if NW_FRAMEWORK_AVAILABLE
if (self.isListening) {
if (errPtr) {
*errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain
code:GCDAsyncSocketErrorAlreadyConnected
userInfo:@{NSLocalizedDescriptionKey: @"Socket is already listening."}];
}
return NO;
}

Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

acceptOnInterface:port:error: only checks self.isListening to reject a second listen. During the window after self.listener is created but before the state handler sets isListening = YES, a second call can succeed and replace the listener. Also, calling accept while isConnected is true should be rejected. Consider guarding on (self.listener != NULL) || self.isListening || self.isConnected (and returning an appropriate error).

Copilot uses AI. Check for mistakes.
Comment on lines +394 to +404
- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr {
#if NW_FRAMEWORK_AVAILABLE
if (self.isListening) {
if (errPtr) {
*errPtr = [NSError errorWithDomain:GCDAsyncSocketErrorDomain
code:GCDAsyncSocketErrorAlreadyConnected
userInfo:@{NSLocalizedDescriptionKey: @"Socket is already listening."}];
}
return NO;
}

Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

acceptOnUrl:error: only checks self.isListening to reject a second listen, but self.listener is assigned before isListening is flipped in the state handler. A second call during startup could incorrectly succeed and replace the listener. Also consider rejecting accept calls when isConnected is already true. Guarding on self.listener != NULL (and/or setting isListening immediately when listener creation succeeds) would make this robust.

Copilot uses AI. Check for mistakes.
Comment on lines 53 to +58
/// Whether the socket is currently disconnected.
@property (atomic, readonly) BOOL isDisconnected;

/// Whether the socket is currently listening for incoming connections (server mode).
@property (atomic, readonly) BOOL isListening;

Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

With the introduction of isListening, a socket that is listening shouldn’t generally report isDisconnected == YES. The current implementation in GCDAsyncSocket.m returns !isConnected only, which will read as disconnected while listening. Consider updating isDisconnected semantics to include !isListening (e.g. disconnected iff not connected and not listening).

Copilot uses AI. Check for mistakes.
Comment on lines +612 to +615
printf(" Accepted sockets count: %lu (expected: 1)\n",
(unsigned long)delegate.acceptedSockets.count);
if (delegate.acceptedSockets.count > 0) {
GCDAsyncSocket *accepted = delegate.acceptedSockets[0];
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

acceptedSockets is mutated on delegateQueue in didAcceptNewSocket:, but here it’s read from the main thread without synchronization. Since NSMutableArray isn’t thread-safe this can race/crash intermittently. Consider reading acceptedSockets via dispatch_sync(queue, ...) (same queue as delegate callbacks) or otherwise synchronizing access.

Suggested change
printf(" Accepted sockets count: %lu (expected: 1)\n",
(unsigned long)delegate.acceptedSockets.count);
if (delegate.acceptedSockets.count > 0) {
GCDAsyncSocket *accepted = delegate.acceptedSockets[0];
// Access acceptedSockets on the same queue used for delegate callbacks
__block NSUInteger acceptedCount = 0;
__block GCDAsyncSocket *accepted = nil;
dispatch_sync(queue, ^{
acceptedCount = delegate.acceptedSockets.count;
if (acceptedCount > 0) {
accepted = delegate.acceptedSockets[0];
}
});
printf(" Accepted sockets count: %lu (expected: 1)\n",
(unsigned long)acceptedCount);
if (acceptedCount > 0 && accepted != nil) {

Copilot uses AI. Check for mistakes.
Comment on lines +632 to +635
[clientSocket disconnect];
for (GCDAsyncSocket *s in delegate.acceptedSockets) {
[s disconnect];
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

Similar to the earlier count/index access: iterating delegate.acceptedSockets from the main thread can race with didAcceptNewSocket: appends on delegateQueue. Consider synchronizing (e.g. take a snapshot on delegateQueue and iterate that).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants