Interactive UNIX command: a possible solution
Interactive UNIX command: a possible solution
- Subject: Interactive UNIX command: a possible solution
- From: Nicola Vitacolonna <email@hidden>
- Date: Sat, 5 Jan 2002 15:55:29 +0100
Thanks to your help, I devised the following solution to the problem of
writing a wrapper for an *interactive* UNIX command: I post it hoping to
be helpful.
The problem was: if the UNIX command does not do fflush(stdout) then its
output, being block buffered by default, is not sent to the pipe until
the program exits (or the buffer is full). If the UNIX command, however,
prints lines ending with '\n' (which is pretty common) it is possible to
use pseudo-terminals to have output line-buffered by default (of course,
I assume you are not to modify the command's source code). I was
suggested to use BSD calls in this case instead of NSTask and NSPipe. I
wanted a way to use NSTask together with pty's. I tried with BSD
openpty(), but it did not work. The following code, however, seems to
accomplish the job (my apologies if this was already posted or
discussed: I did not find anything in the archives):
/*
* Pseudo-terminal routines for 4.3BSD.
* The following code is (slighly adapted) from "UNIX Network
Programming"
* by W. Richard Stevens, published by Prentice Hall, 1990.
*/
char pty_name[12]; /* "/dev/[pt]tyXY" = 10 chars + null byte */
/* Creates master device */
/* Returns the file descriptor, or -1 on error */
-(int)ptyMaster
{
int i, master_fd;
char *ptr;
struct stat statbuff;
static char ptychar[] = "pqrs"; /* X */
static char hexdigit[] = "0123456789abcdef"; /* Y */
/*
* Open the master half - "/dev/pty[pqrs][0-9a-f]".
* There is no easy way to obtain an available minor device
* (similar to a streams clone open) - we have to try them
* all until we find an unused one.
*/
for (ptr = ptychar; *ptr != 0; ptr++) {
strcpy(pty_name, "/dev/ptyXY");
pty_name[8] = *ptr; /* X */
pty_name[9] = '0'; /* Y */
/*
* If this name, "/dev/ptyX0" doesn't even exist,
* then we can quit now. It means the system doesn't
* have /dev entries for this group of 16 ptys.
*/
if (stat(pty_name, &statbuff) < 0)
break;
for (i = 0; i < 16; i++) {
pty_name[9] = hexdigit[i]; /* 0-15 -> 0-9a-f */
if ( (master_fd = open(pty_name, O_RDWR)) >= 0) {
return(master_fd); /* got it, done */
}
}
}
return(-1); /* couldn't open master, assume all pty's are in use */
}
/*
* Open the slave half of a pseudo-terminal.
* Note that the master half of a pty is a single-open device,
* so there isn't a race condition between opening the master
* above and opening the slave below. The only way the slave
* open will fail is if someone has opened the slave without
* first opening the master.
*
* Receives the master devices's file descriptor
* Returns the file descriptor, or -1 on error
*/
-(int)ptySlave:(int)master_fd
{
int slave_fd;
pty_name[5] = 't'; /* change "/dev/ptyXY" to "/dev/ttyXY" */
if ( (slave_fd = open(pty_name, O_RDWR)) < 0) {
close(master_fd);
return(-1);
}
return(slave_fd);
}
If one passes pty_name as a parameter, this code might become a category
of NSTask. The way to use the previous code is as follows:
NSTask *aTask;
NSFileHandle *slaveHandle;
NSFileHandle *masterHandle;
int master, slave;
---------------------------------------------------------
aTask = [[NSTask alloc] init];
master = [self ptyMaster];
if (master>0) {
slave = [self ptySlave:master];
if (slave>0) {
slaveHandle = [[NSFileHandle alloc]
initWithFileDescriptor:slave];
masterHandle = [[NSFileHandle alloc]
initWithFileDescriptor:master];
[aTask setStandardOutput:slaveHandle];
[aTask setStandardInput:slaveHandle];
[masterHandle readInBackgroundAndNotify];
[...]
The method registered to receive NSFileHandleNotificationDataItem
notifications is as usual, and it must send a readInBackgroundAndNotify
message to masterHandle. Input to the UNIX command is also sent via
[masterHandle write
Data:myData]. Note that the standard input and
standard output of the task are set to be the slave device's file
descriptor.
Ok, this works *almost* flawlessly for me. I noticed that, from time to
time, some output may be lost and this seems to be related to the order
in which notifications arrive: if NSTaskDidTerminateNotification arrives
before NSFileHandleReadCompletionNotification output is lost (does
someone know why?), which does not happen when using NSPipe, so I
suggest to use this code only when interaction with the user is really
needed and to use NSPipe instead when dealing with a batch task.
Good Cocoa programming,
Nicola Vitacolonna