• Open Menu Close Menu
  • Apple
  • Shopping Bag
  • Apple
  • Mac
  • iPad
  • iPhone
  • Watch
  • TV
  • Music
  • Support
  • Search apple.com
  • Shopping Bag

Lists

Open Menu Close Menu
  • Terms and Conditions
  • Lists hosted on this site
  • Email the Postmaster
  • Tips for posting to public mailing lists
Re: Can you pass a file descriptor to another process?
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Can you pass a file descriptor to another process?


  • Subject: Re: Can you pass a file descriptor to another process?
  • From: James Bucanek <email@hidden>
  • Date: Fri, 22 Aug 2014 14:37:57 -0700

I fell down the rabbit hole on this one, but I finally hit pay dirt—metaphorically speaking.

Thanks go to Jim Luther and Wim Lewis for their off-list help. Here's the final solution, with code, for everyone to laugh at.

First, it helps to understand my (sometimes overly complicated) environment. This is a backup utility. Every major high-level action (perform a backup, preform a restore, create a new archive, verify the integrity of an archive, etc.) is implemented as a subclass of the Command class. A Command object can be used directly (as an object), can be execute asynchronously in a background thread (in the same process), executed in a helper process (on the same machine), or executed in a helper process running on another computer system. In the first case, messages are sent directly to the object. In the remaining cases the client communicates with the task via Distributed Objects. Control and configuration of a Command object/task/process is abstracted through a CommandTaskConnection object, subclasses of which handle each of the different arrangements.

You'll also see a DDPath object in the code. This is just a wrapper for an immutable C-string/NSString/NSURL reference to a filesystem object. The class converts between the three and has some handy utility and info functions—nothing mysterious. I didn't include the source for this, 'cause its function should be obvious.

It should also be obvious that this is not a general purpose solution, but it could be the start of one.

The core of the solution is in a new DDLocalDatagramSocket class. This is the class that manages the creation of a local (UNIX domain) socket that can be optionally bound to a filesystem object (specified by a DDPath), and is where most of the "fun" code is.

//
// DDLocalDatagramSocket .h
//  Quantum Recall
//
//  Created by James Bucanek on 8/21/14.
//
//

#include <sys/socket.h>
#include <sys/un.h>
#import <Foundation/Foundation.h>

#import "DDPath.h"

// Written just to encapsulate a named socket.

@interface DDLocalDatagramSocket : NSObject <NSCopying,NSCoding>

+ (DDLocalDatagramSocket*)socketWithPath:(DDPath*)pathObject;

@property (readonly,atomic) DDPath* pathObject;
@property (readonly,atomic) int socket;
@property (readonly,atomic) BOOL bound;
@property (readonly,nonatomic) struct sockaddr_un socketAddress;
@property (readonly,nonatomic) const struct sockaddr_un* socketAddressPtr;
@property (readonly,nonatomic) socklen_t socketAddressSize;

- (void)bind;
- (void)close;
- (ssize_t)sendBytes:(const void*)bytes
              length:(size_t)length
           toAddress:(const struct sockaddr_un*)addr
                size:(socklen_t)size;
- (ssize_t)sendData:(NSData*)data toSocket:(DDLocalDatagramSocket*)socket;
- (void)sendData:(NSData*)data andFileDescriptor:(int)fd
        toSocket:(DDLocalDatagramSocket*)socket;
- (NSData*)receiveBytesMax:(size_t)maxLength;
- (NSData*)receiveBytesMax:(size_t)maxLength andFileDescriptor:(int*)returnedFD;

@end


//
//  DDLocalDatagramSocket.m
//  Quantum Recall
//
//  Created by James Bucanek on 8/21/14.
//
//

#import "DDLocalDatagramSocket.h"

#import "QRException.h"
#import "QRAssert.h"

@interface DDLocalDatagramSocket () // private
{
    DDPath*         pathObj;
    int             socketDescriptor;
}
@end

@implementation DDLocalDatagramSocket

+ (DDLocalDatagramSocket*)socketWithPath:(DDPath*)pathObject
{
    return [[[self alloc] initWithPathObject:pathObject] autorelease];
}

- (id)initWithPathObject:(DDPath*)path
{
    self = [super init];
    if (self!=nil)
        {
        pathObj = [path retain];                // (can be nil)
        socketDescriptor = -1;                  // default: not created

        if (path!=nil)
            {
// Construct and cache the socket address for this local file system object BetaAssert(path.path.length<104,@"path too long to be used as a socket address (%@)",path.path); _socketAddress.sun_family = AF_LOCAL; // this is a local (UNIX domain) socket strncpy(_socketAddress.sun_path,path.cPath,sizeof(_socketAddress.sun_path)); // copy name _socketAddress.sun_path[sizeof(_socketAddress.sun_path)-1] = '\0'; // ensure it's terminated
            // Calculate and save the length of the address structure
            _socketAddressSize = (socklen_t)(SUN_LEN(&_socketAddress));
            }
        }
    return self;
}

- (id)copyWithZone:(NSZone*)zone
{
DDLocalDatagramSocket* copy = [[[self class] allocWithZone:zone] initWithPathObject:pathObj];
    copy->socketDescriptor = socketDescriptor;
    copy->_bound = _bound;
    return copy;
}

- (id)initWithCoder:(NSCoder*)decoder
{
// Initialize the object from the DDPath (which will derive its socket address
    //  and length) and then fill in the socket descriptor and state.

// IMPORTANT NOTE: The socket descriptor is not valid outside the process it was // created in. The int, however, can be passed around, and it's // perfectly valid to copy an open DDLocalDatagramSocket object // to another process and then back to its original one, where it will
    //                  still be usable.

    // Reconstruct the object from the data stream
    if ([decoder allowsKeyedCoding])
        {
self = [self initWithPathObject:[decoder decodeObjectForKey:@"path"]];
        if (self!=nil)
            {
            socketDescriptor = [decoder decodeIntForKey:@"socket"];
            _bound = [decoder decodeBoolForKey:@"bound"];
            }
        }
    else
        {
        self = [self initWithPathObject:[decoder decodeObject]];
        if (self!=nil)
            {
[decoder decodeValueOfObjCType:@encode(int) at:&socketDescriptor];
            [decoder decodeValueOfObjCType:@encode(BOOL) at:&_bound];
            }
        }
    return self;
}

- (void)encodeWithCoder:(NSCoder*)encoder
{
    // See note in -initWithCoder:
    if ([encoder allowsKeyedCoding])
        {
        [encoder encodeObject:pathObj forKey:@"path"];
        [encoder encodeInt:socketDescriptor forKey:@"socket"];
        [encoder encodeBool:_bound forKey:@"bound"];
        }
    else
        {
        [encoder encodeObject:pathObj];
        [encoder encodeValueOfObjCType:@encode(int) at:&socketDescriptor];
        [encoder encodeValueOfObjCType:@encode(BOOL) at:&_bound];
        }
}

- (void)dealloc
{
    [self close];

    [pathObj release];
    [super dealloc];
}

#pragma mark Properties

- (int)socket
{
    if (socketDescriptor<0)
        {
        // Lazily create an anonymous socket
        socketDescriptor = socket(PF_LOCAL,SOCK_DGRAM,0);
        if (socketDescriptor<0)
@throw [QRException ioErrnoWithReason:@"cannot create local socket"];
        }
    return socketDescriptor;
}

- (const struct sockaddr_un*)socketAddressPtr
{
    return &_socketAddress;
}

#pragma mark Socket

- (void)bind
{
    BetaAssert(pathObj!=nil,@"cannot bind an anonymous socket");
    @try {
        if (!_bound)
            {
if (bind(self.socket,(struct sockaddr*)&_socketAddress,_socketAddressSize)!=0) @throw [[QRException ioErrnoWithReason:@"cannot bind socket to path"]
                        setPathObject:pathObj];
            _bound = YES;
            }
        }
    @catch (NSException *exception) {
        [self close];
        @throw exception;
        }
}

- (void)close
{
    if (socketDescriptor>=0)
        {
        close(socketDescriptor);
        socketDescriptor = -1;
        if (_bound)
[pathObj tryToDeleteFile]; // delete the socket file (ignoring any errors)
        _bound = NO;
        }
}

- (ssize_t)sendBytes:(const void*)bytes
           length:(size_t)length
        toAddress:(const struct sockaddr_un*)addr
             size:(socklen_t)size
{
    // Send the data to the given address
    BetaAssert(size!=0,@"destination requires an address");
ssize_t sentBytes = sendto(self.socket,bytes,length,0,(const struct sockaddr*)addr,size);
    if (sentBytes<0)
        {
        @throw [QRException ioErrnoWithReason:@"sendto failed"];
        }
    else if ((size_t)sentBytes!=length)
        {
@throw [[[QRException exceptionWithName:kIPCException reason:@"not all data sent"]
                 setLength:sentBytes]
                setDecimalDetail:length key:kDetailsExpected];
        }

    return  sentBytes;
}

- (ssize_t)sendData:(NSData*)data toSocket:(DDLocalDatagramSocket*)socket
{
    return [self sendBytes:data.bytes
                    length:data.length
                 toAddress:socket.socketAddressPtr
                      size:socket.socketAddressSize];
}

- (void)sendData:(NSData*)data andFileDescriptor:(int)fd toSocket:(DDLocalDatagramSocket*)toSocket
{
// Similar to -sendData:toSocket:, but it uses the sendmsg() function to
    //  piggyback a control structure that transfers the file descriptor
    //  to the receiving process.
BetaAssert(data.length!=0,@"data must contain something, even if ignored");

    // Construct a msghdr
    struct msghdr msg;
    bzero(&msg,sizeof(msg));
    // The first part is the address of the receiver
    BetaAssert(toSocket.bound,@"destination socket must be bound");
    msg.msg_name = (void*)toSocket.socketAddressPtr;
    msg.msg_namelen = toSocket.socketAddressSize;
// The second part is the raw data to send, defined by an array of iovec structs
    struct iovec iov;                       // iovec[1]
    iov.iov_base = (void*)data.bytes;
    iov.iov_len = data.length;
msg.msg_iov = &iov; // point message header at iov array
    msg.msg_iovlen = 1;
// The third part is the special control message containing the file descriptor
    union {
        struct cmsghdr  header;
        char            raw[CMSG_SPACE(sizeof(fd))];
    } cmsg;
    bzero(&cmsg,sizeof(cmsg));
    cmsg.header.cmsg_len = CMSG_LEN(sizeof(fd));
    cmsg.header.cmsg_level = SOL_SOCKET;
    cmsg.header.cmsg_type = SCM_RIGHTS;
memmove(CMSG_DATA(cmsg.raw),&fd,sizeof(fd)); // copy fd data into control structure msg.msg_control = &cmsg.header; // point message header at control structure
    msg.msg_controllen = cmsg.header.cmsg_len;

    // Send the message and the file descriptor
    ssize_t sentBytes = sendmsg(self.socket,&msg,0);
    if (sentBytes<0)
        {
        @throw [QRException ioErrnoWithReason:@"sendmsg failed"];
        }
    else if ((size_t)sentBytes!=data.length)
        {
@throw [[[QRException exceptionWithName:kIPCException reason:@"not all data sent"]
                 setLength:sentBytes]
                setDecimalDetail:data.length key:kDetailsExpected];
        }
}


- (NSData*)receiveBytesMax:(size_t)maxLength
{
    // Receive a message on the active socket, up to maxLength bytes.
    // Return the results in an NSData object.
    // The address of the sender is discarded.
    //  If the receiver needs to know the address of the sender (in order
    //  to reply), then someone has some programming to do...

    if (!_bound)
// The receiving socket must be bound to its filesystem object or it can't receive anything
        [self bind];

NSMutableData* data = [NSMutableData dataWithLength:maxLength]; // allocate a receive buffer ssize_t receivedBytes = recvfrom(self.socket,(void*)data.bytes,maxLength,0,NULL,NULL);
    if (receivedBytes<0)
        @throw [[[QRException ioErrnoWithReason:@"failed to receive data"]
                 setLength:maxLength]
                setPathObject:pathObj];
    else if (receivedBytes==0)
@throw [QRException exceptionWithName:kIPCException reason:@"socked closed or EOF"];

data.length = receivedBytes; // trim data object to actual bytes received
    return data;
}

- (NSData*)receiveBytesMax:(size_t)maxLength andFileDescriptor:(int*)returnedFD
{
    // Reads a message block and a file descriptor, passed with the message
// in a special control structure. Like -receiveBytesMax:, this discards the
    //  sender's address.
    BetaParameterAssert(maxLength>0);
    BetaParameterAssert(returnedFD!=NULL);

    if (!_bound)
        [self bind];

    // Buffer for received data
    NSMutableData* data = [NSMutableData dataWithLength:maxLength];

    // Construct a msghdr
    struct msghdr msg;
    bzero(&msg,sizeof(msg));
    // This method ignores the sender's address
    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    // Set up the iocvec array that will point to the receive buffer(s).
    struct iovec iov;                       // iovec[1]
    iov.iov_base = (void*)data.bytes;
    iov.iov_len = maxLength;
msg.msg_iov = &iov; // point message header at iov array
    msg.msg_iovlen = 1;
    // Allocate a structure to recevied the control message
    union {
        struct cmsghdr  header;
        char            raw[CMSG_SPACE(sizeof(int))];
    } cmsg;
    bzero(&cmsg,sizeof(cmsg));
    msg.msg_control = cmsg.raw;
    msg.msg_controllen = sizeof(cmsg);

    // <MontyPython>Wait for it ...</MontyPython>
    ssize_t receivedBytes = recvmsg(self.socket,&msg,0);
    if (receivedBytes<0)
@throw [QRException ioErrnoWithReason:@"error receiving data and file descriptor"];
    else if (receivedBytes==0)
@throw [QRException exceptionWithName:kIPCException reason:@"socked closed or EOF"];
    else if (msg.msg_flags&(MSG_TRUNC|MSG_CTRUNC))
@throw [QRException exceptionWithName:kIPCException reason:@"received message incomplete"];

struct cmsghdr* controlMsg = CMSG_FIRSTHDR(&msg); // should be &cmsg, but this is the official method
    if (controlMsg!=NULL && controlMsg->cmsg_type==SCM_RIGHTS)
        {
// Control message contains the file descriptor; copy it to the int provided by the sender
        memmove(returnedFD,CMSG_DATA(controlMsg),sizeof(int));
        }
    else
        {
// Something went wrong: the message didn't include the correct control message @throw [QRException exceptionWithName:kIPCException reason:@"message did not include file descriptor"];
        }

data.length = iov.iov_len; // trim data object to actual bytes received
    return data;
}

@end


In the CommandTaskConnection class (the client's interface to a running Command object), I implemented a new -dupFileDescriptor: method. This is the high-level method a client calls to duplicate a local file descriptor in preparation for using it in the Command's process. In the CommandTaskConnection subclasses where the Command object is in the same process, this method simply returns dup(fd). For Commands running in a remote process, here's the new code:

- (int)dupFileDescriptor:(int)fd
{
// Special utility to duplicate a file descriptor so the duplicate is usable in the
    //  command's process.
// Note: The returned file descriptor is *only* valid in the command's process, and can
    //       only be useful in configuring the remote command.
// UNIX domain sockets provide a special mechanism for exchanging a file descriptor with another
    //  process, encapsulated in DDLocalDatagramSocket.
NSNumber* fdValue = [self valueFromCommand:^id(id<CommandMessaging> command) { // First tell the remote command to create a local socket and tell us where it is DDLocalDatagramSocket* commandSocket = [(Command*)command makeTemporayDatagramSocket]; // Create an anonymous socket on this side that will be used to send the file descriptor DDLocalDatagramSocket* localSocket = [DDLocalDatagramSocket socketWithPath:nil]; // Now it's possible to send the file descriptor from our socket to the command's socket [localSocket sendData:[NSMutableData dataWithLength:1] /* dummy message */
            andFileDescriptor:fd
                     toSocket:commandSocket];
// Finally, tell the remote command to read the file descriptor that's waiting for it int dupFD = [(Command*)command receiveFileDescriptorViaSocket:commandSocket];
        // return the duplicated file descriptor to the sender
        return [NSNumber numberWithInt:dupFD];
        }];
    return fdValue.intValue;
}

The client first requests that the Command object create and bind a new local datagram socket (in /tmp) and returns the socket reference to the caller. The caller can then use that to write the data it wants to transfer to that socket. The final step is to then tell the Command object it has a message waiting and to go get it.

That work is performed by these two Command methods: -makeTemporayDatagramSocket and -receiveFileDescriptorViaSocket:

- (bycopy DDLocalDatagramSocket*)makeTemporayDatagramSocket
{
    // Sent by a remote client to prepare this command to receive data
    //  from another process via a UNIX domain datagram socket.
// This method creates a socket and binds it to a random, temporary, file
    //  and returns that to the sender. The sender must then write its data
// to that socket and then tell this process to read the data and destroy the socket. // This four-step dance makes the whole thing thread-safe and avoids any local state variables.

// Currently, this method is only used to transfer file descriptors with
    //  -receiveFileDescriptorViaSocket:.

    // Create a randomly named local datagram socket in the /tmp directory
DDPath* tmpPath = [DDPath tempPathWithPrefix:@"fd" extension:@"socket"]; DDLocalDatagramSocket* socket = [DDLocalDatagramSocket socketWithPath:tmpPath]; // bind the socket to the path; this will create the object in the filesystem
    [socket bind];
// Return the socket object to the sender; the sender will use this as its address
    return socket;
}

- (int)receiveFileDescriptorViaSocket:(bycopy DDLocalDatagramSocket*)localSocket
{
// Retrieve the translated file descriptor integer from the local datagram socket
    //  which has, or will be, sent to the given socket.
    int fd;
    (void)[localSocket receiveBytesMax:1 andFileDescriptor:&fd];
    // Close the socket and delete the socket file
    [localSocket close];
    // Return the duplicated file descriptor to the sender
    return fd;
}


So that's the bulk of the solution. I can now dup() a file descriptor in such a way that it is always usable in the process that the Command object is running in. (Well, except for the remote network version, which I've ignored for now.)

To see this in action, the following method handles the command line option to redirect the helper's log file output to a different file (which would otherwise be written to ~/Library/Logs/...):

- (void)logRedirectOption:(NSString *)argument
{
    // --logfile [!]/Path/to/logfile.log | - | -stdout | -stderr

    // Redirect the log file output to a different file
    //  if argument is "-" or "-stdout", redirect to stdout
    //  if argument is "-stderr", redirect to stderr
    //  if argument begins with a '!', overwrite the output file
    int redirectFD;
    BOOL newFile = NO;
    if ([argument isEqualToString:@"-stderr"])
        {
        // Redirect log output to stderr
        redirectFD = STDERR_FILENO;
        }
if ([argument isEqualToString:@"-"] || [argument isEqualToString:@"-stdout"])
        {
        // Redirect log to stdout
        redirectFD = STDOUT_FILENO;
        }
    else
        {
        // Open/Overwrite a file
        int flags = O_WRONLY|O_CREAT|O_APPEND;
        if (argument.length>1 && [argument characterAtIndex:0]=='!')
            {
// If path to log file starts with "!" overwrite the destination file
            flags = O_WRONLY|O_CREAT|O_TRUNC;
            argument = [argument substringFromIndex:1];
            }
        DDPath* path = [DDPath realPath:argument];
redirectFD = open(path.cPath,flags,(S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH));
        if (redirectFD<0)
            [[[[QRException ioErrnoWithReason:@"cannot open file"]
               setPathObject:path]
              setHexDetail:flags key:@"flags"]
             raise];
        newFile = YES;
        }
    // Duplicate the file descriptor so it's usable in the command
    int dupFD = [self.connection dupFileDescriptor:redirectFD];
    // Tell the command to write its log output to the file
    [self.connection performOnCommand:^(id<CommandMessaging> command) {
        [command redirectLogPathToFileDescriptor:dupFD];
        }];
    if (newFile)
        close (redirectFD);         // it's now safe to close the new file
}

Enjoy,

James






_______________________________________________
Do not post admin requests to the list. They will be ignored.
Filesystem-dev mailing list      (email@hidden)
Help/Unsubscribe/Update your Subscription:

This email sent to email@hidden


References: 
 >Re: Can you pass a file descriptor to another process? (From: Ken Hornstein <email@hidden>)
 >Re: Can you pass a file descriptor to another process? (From: James Bucanek <email@hidden>)
 >Re: Can you pass a file descriptor to another process? (From: "Quinn \"The Eskimo!\"" <email@hidden>)

  • Prev by Date: Re: Can you pass a file descriptor to another process?
  • Previous by thread: Re: Can you pass a file descriptor to another process?
  • Next by thread: Re: Can you pass a file descriptor to another process?
  • Index(es):
    • Date
    • Thread