Fix ObjC build error and add server socket API demo coverage#7
Fix ObjC build error and add server socket API demo coverage#7dustturtle merged 6 commits intomainfrom
Conversation
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>
…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>
…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>
There was a problem hiding this comment.
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()asuint16_tand alignisListeningproperty 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.
| #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 |
There was a problem hiding this comment.
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).
| #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 |
| - (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; | ||
| } | ||
|
|
There was a problem hiding this comment.
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).
| - (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; | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
| /// 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; | ||
|
|
There was a problem hiding this comment.
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).
| printf(" Accepted sockets count: %lu (expected: 1)\n", | ||
| (unsigned long)delegate.acceptedSockets.count); | ||
| if (delegate.acceptedSockets.count > 0) { | ||
| GCDAsyncSocket *accepted = delegate.acceptedSockets[0]; |
There was a problem hiding this comment.
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.
| 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) { |
| [clientSocket disconnect]; | ||
| for (GCDAsyncSocket *s in delegate.acceptedSockets) { | ||
| [s disconnect]; | ||
| } |
There was a problem hiding this comment.
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).
CI
ObjC Demo (macOS)job fails due to a compilation error inGCDAsyncSocket.m. The new server-side APIs (acceptOnPort:,acceptOnInterface:port:,acceptOnUrl:) also lacked any test/demo coverage.Build fixes
nw_endpoint_get_portreturn type: API returnsuint16_t, notconst char *. Removed incorrect pointer assignment andstrtoulparsing.isListeningatomicity mismatch: Class extension declarednonatomicbut public header declaredatomic readonly. Aligned toatomic.Server socket API demo (demo 6)
Added
DemoSocketDelegatesupport forsocket:didAcceptNewSocket:and a new interactive demo exercising all server-side APIs:acceptOnPort:with port 0 (system-assigned)didAcceptNewSocket:callbackdisconnectstops listener,isListening→ NOacceptOnInterface:port:on localhostacceptOnUrl:Unix Domain SocketacceptOnUrl:non-file URL rejectionCI
Updated
demo.ymlObjC 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.