putty/macosx/osxsel.m

309 строки
8.7 KiB
Objective-C

/*
* osxsel.m: OS X implementation of the front end interface to uxsel.
*/
#import <Cocoa/Cocoa.h>
#include <unistd.h>
#include "putty.h"
#include "osxclass.h"
/*
* The unofficial Cocoa FAQ at
*
* http://www.alastairs-place.net/cocoa/faq.txt
*
* says that Cocoa has the native ability to be given an fd and
* tell you when it becomes readable, but cannot tell you when it
* becomes _writable_. This is unacceptable to PuTTY, which depends
* for correct functioning on being told both. Therefore, I can't
* use the Cocoa native mechanism.
*
* Instead, I'm going to resort to threads. I start a second thread
* whose job is to do selects. At the termination of every select,
* it posts a Cocoa event into the main thread's event queue, so
* that the main thread gets select results interleaved with other
* GUI operations. Communication from the main thread _to_ the
* select thread is performed by writing to a pipe whose other end
* is one of the file descriptors being selected on. (This is the
* only sensible way, because we have to be able to interrupt a
* select in order to provide a new fd list.)
*/
/*
* In more detail, the select thread must:
*
* - start off by listening to _just_ the pipe, waiting to be told
* to begin a select.
*
* - when it receives the `start' command, it should read the
* shared uxsel data (which is protected by a mutex), set up its
* select, and begin it.
*
* - when the select terminates, it should write the results
* (perhaps minus the inter-thread pipe if it's there) into
* shared memory and dispatch a GUI event to let the main thread
* know.
*
* - the main thread will then think about it, do some processing,
* and _then_ send a command saying `now restart select'. Before
* sending that command it might easily have tinkered with the
* uxsel structures, which is why it waited before sending it.
*
* - EOF on the inter-thread pipe, of course, means the process
* has finished completely, so the select thread terminates.
*
* - The main thread may wish to adjust the uxsel settings in the
* middle of a select. In this situation it first writes the new
* data to the shared memory area, then notifies the select
* thread by writing to the inter-thread pipe.
*
* So the upshot is that the sequence of operations performed in
* the select thread must be:
*
* - read a byte from the pipe (which may block)
*
* - read the shared uxsel data and perform a select
*
* - notify the main thread of interesting select results (if any)
*
* - loop round again from the top.
*
* This is sufficient. Notifying the select thread asynchronously
* by writing to the pipe will cause its select to terminate and
* another to begin immediately without blocking. If the select
* thread's select terminates due to network data, its subsequent
* pipe read will block until the main thread is ready to let it
* loose again.
*/
static int osxsel_pipe[2];
static NSLock *osxsel_inlock;
static fd_set osxsel_rfds_in;
static fd_set osxsel_wfds_in;
static fd_set osxsel_xfds_in;
static int osxsel_inmax;
static NSLock *osxsel_outlock;
static fd_set osxsel_rfds_out;
static fd_set osxsel_wfds_out;
static fd_set osxsel_xfds_out;
static int osxsel_outmax;
static int inhibit_start_select;
/*
* NSThread requires an object method as its thread procedure, so
* here I define a trivial holding class.
*/
@class OSXSel;
@interface OSXSel : NSObject
{
}
- (void)runThread:(id)arg;
@end
@implementation OSXSel
- (void)runThread:(id)arg
{
char c;
fd_set r, w, x;
int n, ret;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
while (1) {
/*
* Read one byte from the pipe.
*/
ret = read(osxsel_pipe[0], &c, 1);
if (ret <= 0)
return; /* terminate the thread */
/*
* Now set up the select data.
*/
[osxsel_inlock lock];
memcpy(&r, &osxsel_rfds_in, sizeof(fd_set));
memcpy(&w, &osxsel_wfds_in, sizeof(fd_set));
memcpy(&x, &osxsel_xfds_in, sizeof(fd_set));
n = osxsel_inmax;
[osxsel_inlock unlock];
FD_SET(osxsel_pipe[0], &r);
if (n < osxsel_pipe[0]+1)
n = osxsel_pipe[0]+1;
/*
* Perform the select.
*/
ret = select(n, &r, &w, &x, NULL);
/*
* Detect the one special case in which the only
* interesting fd was the inter-thread pipe. In that
* situation only we are interested - the main thread will
* not be!
*/
if (ret == 1 && FD_ISSET(osxsel_pipe[0], &r))
continue; /* just loop round again */
/*
* Write the select results to shared data.
*
* I _think_ we don't need this data to be lock-protected:
* it won't be read by the main thread until after we send
* a message indicating that we've finished writing it, and
* we won't start another select (hence potentially writing
* it again) until the main thread notifies us in return.
*
* However, I'm scared of multithreading and not totally
* convinced of my reasoning, so I'm going to lock it
* anyway.
*/
[osxsel_outlock lock];
memcpy(&osxsel_rfds_out, &r, sizeof(fd_set));
memcpy(&osxsel_wfds_out, &w, sizeof(fd_set));
memcpy(&osxsel_xfds_out, &x, sizeof(fd_set));
osxsel_outmax = n;
[osxsel_outlock unlock];
/*
* Post a message to the main thread's message queue
* telling it that select data is available.
*/
[NSApp postEvent:[NSEvent otherEventWithType:NSApplicationDefined
location:NSMakePoint(0,0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0]
atStart:NO];
}
[pool release];
}
@end
void osxsel_init(void)
{
uxsel_init();
if (pipe(osxsel_pipe) < 0) {
fatalbox("Unable to set up inter-thread pipe for select");
}
[NSThread detachNewThreadSelector:@selector(runThread:)
toTarget:[[[OSXSel alloc] init] retain] withObject:nil];
/*
* Also initialise (i.e. clear) the input fd_sets. Need not
* start a select just yet - the select thread will block until
* we have at least one fd for it!
*/
FD_ZERO(&osxsel_rfds_in);
FD_ZERO(&osxsel_wfds_in);
FD_ZERO(&osxsel_xfds_in);
osxsel_inmax = 0;
/*
* Initialise the mutex locks used to protect the data passed
* between threads.
*/
osxsel_inlock = [[[NSLock alloc] init] retain];
osxsel_outlock = [[[NSLock alloc] init] retain];
}
static void osxsel_start_select(void)
{
char c = 'g'; /* for `Go!' :-) but it's never used */
if (!inhibit_start_select)
write(osxsel_pipe[1], &c, 1);
}
int uxsel_input_add(int fd, int rwx)
{
/*
* Add the new fd to the appropriate input fd_sets, then write
* to the inter-thread pipe.
*/
[osxsel_inlock lock];
if (rwx & 1)
FD_SET(fd, &osxsel_rfds_in);
else
FD_CLR(fd, &osxsel_rfds_in);
if (rwx & 2)
FD_SET(fd, &osxsel_wfds_in);
else
FD_CLR(fd, &osxsel_wfds_in);
if (rwx & 4)
FD_SET(fd, &osxsel_xfds_in);
else
FD_CLR(fd, &osxsel_xfds_in);
if (osxsel_inmax < fd+1)
osxsel_inmax = fd+1;
[osxsel_inlock unlock];
osxsel_start_select();
/*
* We must return an `id' which will be passed back to us at
* the time of uxsel_input_remove. Since we have no need to
* store ids in that sense, we might as well go with the fd
* itself.
*/
return fd;
}
void uxsel_input_remove(int id)
{
/*
* Remove the fd from all the input fd_sets. In this
* implementation, the simplest way to do that is to call
* uxsel_input_add with rwx==0!
*/
uxsel_input_add(id, 0);
}
/*
* Function called in the main thread to process results. It will
* have to read the output fd_sets, go through them, call back to
* uxsel with the results, and then write to the inter-thread pipe.
*
* This function will have to be called from an event handler in
* osxmain.m, which will therefore necessarily contain a small part
* of this mechanism (along with calling osxsel_init).
*/
void osxsel_process_results(void)
{
int i;
/*
* We must write to the pipe to start a fresh select _even if_
* there were no changes. So for efficiency, we set a flag here
* which inhibits uxsel_input_{add,remove} from writing to the
* pipe; then once we finish processing, we clear the flag
* again and write a single byte ourselves. It's cleaner,
* because it wakes up the select thread fewer times.
*/
inhibit_start_select = TRUE;
[osxsel_outlock lock];
for (i = 0; i < osxsel_outmax; i++) {
if (FD_ISSET(i, &osxsel_xfds_out))
select_result(i, 4);
}
for (i = 0; i < osxsel_outmax; i++) {
if (FD_ISSET(i, &osxsel_rfds_out))
select_result(i, 1);
}
for (i = 0; i < osxsel_outmax; i++) {
if (FD_ISSET(i, &osxsel_wfds_out))
select_result(i, 2);
}
[osxsel_outlock unlock];
inhibit_start_select = FALSE;
osxsel_start_select();
}