The problem is that you're not running a server, which is confusing launchd and it thinks your "server" is dying unexpectedly (regardless of its exit code). You're asking launchd to host a server socket for you and spawn your app when there is a pending connection (if and only if your app is not already running and servicing connections). This isn't an "on demand" style server where launchd simply forks and executes multiple instances of your server image, handing it copies of client sockets.
When launchd gets a pending connection request on a socket or mach port that it's hosting for you, it:
- checks to see if you're running;
- if you're not running, it spawns you and expects you to check in to receive a copy of the /listen(2)ing socket/
- if you are running, it does nothing and expects you to handle yourself correctly
- if you terminate without accept(2)ing the pending connection before it times out, launchd assumes you died and re-starts you
For your server to work, you actually need to service the listen(2) socket you received from launchd and clear your kernel backlog. You need to call accept(2) for each pending connection before you can terminate, although you're not really meant to terminate.
What you should do is, if you wake up, check in with launchd and get a copy of your hosted socket. The start servicing the socket as usual (use whatever you normally do, kevent(2), select(2), or just serially service requests if they're low-frequency and really fast to service--hoping you'll always service them before your listen backlog fills). While you do that, you should catch SIGTERM. Launchd will send that signal after there are no more pending requests and it thinks you've been around long enough… that is, let launchd do its job and manage you. You should never have your server exit on its own unless it's an actual unrecoverable error condition or you know that by design your requests are going to be extremely long-running and extremely infrequent.
But whatever you do, before you exit (or even just hang around and wait), you must service all the pending requests on the socket(s) that you receive from launchd, including IPV6 sockets. If you fail to service the request, launchd may kill you and restart you anyway. If you don't want to get an IPV6 socket, configure that specifically in your launchd.plist file. Also remember, these are copies of descriptors that launchd manages, so just because you close your dup(2), it doesn't mean the pending connections are dealt with.