Re: Can you pass a file descriptor to another process?
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