1ddda1ca |
1 | /* |
2 | * osxsel.m: OS X implementation of the front end interface to uxsel. |
3 | */ |
4 | |
5 | #import <Cocoa/Cocoa.h> |
6 | #include <unistd.h> |
7 | #include "putty.h" |
8 | #include "osxclass.h" |
9 | |
10 | /* |
11 | * The unofficial Cocoa FAQ at |
12 | * |
13 | * http://www.alastairs-place.net/cocoa/faq.txt |
14 | * |
15 | * says that Cocoa has the native ability to be given an fd and |
16 | * tell you when it becomes readable, but cannot tell you when it |
17 | * becomes _writable_. This is unacceptable to PuTTY, which depends |
18 | * for correct functioning on being told both. Therefore, I can't |
19 | * use the Cocoa native mechanism. |
20 | * |
21 | * Instead, I'm going to resort to threads. I start a second thread |
22 | * whose job is to do selects. At the termination of every select, |
23 | * it posts a Cocoa event into the main thread's event queue, so |
24 | * that the main thread gets select results interleaved with other |
25 | * GUI operations. Communication from the main thread _to_ the |
26 | * select thread is performed by writing to a pipe whose other end |
27 | * is one of the file descriptors being selected on. (This is the |
28 | * only sensible way, because we have to be able to interrupt a |
29 | * select in order to provide a new fd list.) |
30 | */ |
31 | |
32 | /* |
33 | * In more detail, the select thread must: |
34 | * |
35 | * - start off by listening to _just_ the pipe, waiting to be told |
36 | * to begin a select. |
37 | * |
38 | * - when it receives the `start' command, it should read the |
39 | * shared uxsel data (which is protected by a mutex), set up its |
40 | * select, and begin it. |
41 | * |
42 | * - when the select terminates, it should write the results |
43 | * (perhaps minus the inter-thread pipe if it's there) into |
44 | * shared memory and dispatch a GUI event to let the main thread |
45 | * know. |
46 | * |
47 | * - the main thread will then think about it, do some processing, |
48 | * and _then_ send a command saying `now restart select'. Before |
49 | * sending that command it might easily have tinkered with the |
50 | * uxsel structures, which is why it waited before sending it. |
51 | * |
52 | * - EOF on the inter-thread pipe, of course, means the process |
53 | * has finished completely, so the select thread terminates. |
54 | * |
55 | * - The main thread may wish to adjust the uxsel settings in the |
56 | * middle of a select. In this situation it first writes the new |
57 | * data to the shared memory area, then notifies the select |
58 | * thread by writing to the inter-thread pipe. |
59 | * |
60 | * So the upshot is that the sequence of operations performed in |
61 | * the select thread must be: |
62 | * |
63 | * - read a byte from the pipe (which may block) |
64 | * |
65 | * - read the shared uxsel data and perform a select |
66 | * |
67 | * - notify the main thread of interesting select results (if any) |
68 | * |
69 | * - loop round again from the top. |
70 | * |
71 | * This is sufficient. Notifying the select thread asynchronously |
72 | * by writing to the pipe will cause its select to terminate and |
73 | * another to begin immediately without blocking. If the select |
74 | * thread's select terminates due to network data, its subsequent |
75 | * pipe read will block until the main thread is ready to let it |
76 | * loose again. |
77 | */ |
78 | |
79 | static int osxsel_pipe[2]; |
80 | |
81 | static NSLock *osxsel_inlock; |
82 | static fd_set osxsel_rfds_in; |
83 | static fd_set osxsel_wfds_in; |
84 | static fd_set osxsel_xfds_in; |
85 | static int osxsel_inmax; |
86 | |
87 | static NSLock *osxsel_outlock; |
88 | static fd_set osxsel_rfds_out; |
89 | static fd_set osxsel_wfds_out; |
90 | static fd_set osxsel_xfds_out; |
91 | static int osxsel_outmax; |
92 | |
93 | static int inhibit_start_select; |
94 | |
95 | /* |
96 | * NSThread requires an object method as its thread procedure, so |
97 | * here I define a trivial holding class. |
98 | */ |
99 | @class OSXSel; |
100 | @interface OSXSel : NSObject |
101 | { |
102 | } |
103 | - (void)runThread:(id)arg; |
104 | @end |
105 | @implementation OSXSel |
106 | - (void)runThread:(id)arg |
107 | { |
108 | char c; |
109 | fd_set r, w, x; |
110 | int n, ret; |
111 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
112 | |
113 | while (1) { |
114 | /* |
115 | * Read one byte from the pipe. |
116 | */ |
117 | ret = read(osxsel_pipe[0], &c, 1); |
118 | |
119 | if (ret <= 0) |
120 | return; /* terminate the thread */ |
121 | |
122 | /* |
123 | * Now set up the select data. |
124 | */ |
125 | [osxsel_inlock lock]; |
126 | memcpy(&r, &osxsel_rfds_in, sizeof(fd_set)); |
127 | memcpy(&w, &osxsel_wfds_in, sizeof(fd_set)); |
128 | memcpy(&x, &osxsel_xfds_in, sizeof(fd_set)); |
129 | n = osxsel_inmax; |
130 | [osxsel_inlock unlock]; |
131 | FD_SET(osxsel_pipe[0], &r); |
132 | if (n < osxsel_pipe[0]+1) |
133 | n = osxsel_pipe[0]+1; |
134 | |
135 | /* |
136 | * Perform the select. |
137 | */ |
138 | ret = select(n, &r, &w, &x, NULL); |
139 | |
140 | /* |
141 | * Detect the one special case in which the only |
142 | * interesting fd was the inter-thread pipe. In that |
143 | * situation only we are interested - the main thread will |
144 | * not be! |
145 | */ |
146 | if (ret == 1 && FD_ISSET(osxsel_pipe[0], &r)) |
147 | continue; /* just loop round again */ |
148 | |
149 | /* |
150 | * Write the select results to shared data. |
151 | * |
152 | * I _think_ we don't need this data to be lock-protected: |
153 | * it won't be read by the main thread until after we send |
154 | * a message indicating that we've finished writing it, and |
155 | * we won't start another select (hence potentially writing |
156 | * it again) until the main thread notifies us in return. |
157 | * |
158 | * However, I'm scared of multithreading and not totally |
159 | * convinced of my reasoning, so I'm going to lock it |
160 | * anyway. |
161 | */ |
162 | [osxsel_outlock lock]; |
163 | memcpy(&osxsel_rfds_out, &r, sizeof(fd_set)); |
164 | memcpy(&osxsel_wfds_out, &w, sizeof(fd_set)); |
165 | memcpy(&osxsel_xfds_out, &x, sizeof(fd_set)); |
166 | osxsel_outmax = n; |
167 | [osxsel_outlock unlock]; |
168 | |
169 | /* |
170 | * Post a message to the main thread's message queue |
171 | * telling it that select data is available. |
172 | */ |
173 | [NSApp postEvent:[NSEvent otherEventWithType:NSApplicationDefined |
174 | location:NSMakePoint(0,0) |
175 | modifierFlags:0 |
176 | timestamp:0 |
177 | windowNumber:0 |
178 | context:nil |
179 | subtype:0 |
180 | data1:0 |
181 | data2:0] |
182 | atStart:NO]; |
183 | } |
184 | |
185 | [pool release]; |
186 | } |
187 | @end |
188 | |
189 | void osxsel_init(void) |
190 | { |
191 | uxsel_init(); |
192 | |
193 | if (pipe(osxsel_pipe) < 0) { |
194 | fatalbox("Unable to set up inter-thread pipe for select"); |
195 | } |
196 | [NSThread detachNewThreadSelector:@selector(runThread:) |
197 | toTarget:[[[OSXSel alloc] init] retain] withObject:nil]; |
198 | /* |
199 | * Also initialise (i.e. clear) the input fd_sets. Need not |
200 | * start a select just yet - the select thread will block until |
201 | * we have at least one fd for it! |
202 | */ |
203 | FD_ZERO(&osxsel_rfds_in); |
204 | FD_ZERO(&osxsel_wfds_in); |
205 | FD_ZERO(&osxsel_xfds_in); |
206 | osxsel_inmax = 0; |
207 | /* |
208 | * Initialise the mutex locks used to protect the data passed |
209 | * between threads. |
210 | */ |
211 | osxsel_inlock = [[[NSLock alloc] init] retain]; |
212 | osxsel_outlock = [[[NSLock alloc] init] retain]; |
213 | } |
214 | |
215 | static void osxsel_start_select(void) |
216 | { |
217 | char c = 'g'; /* for `Go!' :-) but it's never used */ |
218 | |
219 | if (!inhibit_start_select) |
220 | write(osxsel_pipe[1], &c, 1); |
221 | } |
222 | |
223 | int uxsel_input_add(int fd, int rwx) |
224 | { |
225 | /* |
226 | * Add the new fd to the appropriate input fd_sets, then write |
227 | * to the inter-thread pipe. |
228 | */ |
229 | [osxsel_inlock lock]; |
230 | if (rwx & 1) |
231 | FD_SET(fd, &osxsel_rfds_in); |
232 | else |
233 | FD_CLR(fd, &osxsel_rfds_in); |
234 | if (rwx & 2) |
235 | FD_SET(fd, &osxsel_wfds_in); |
236 | else |
237 | FD_CLR(fd, &osxsel_wfds_in); |
238 | if (rwx & 4) |
239 | FD_SET(fd, &osxsel_xfds_in); |
240 | else |
241 | FD_CLR(fd, &osxsel_xfds_in); |
242 | if (osxsel_inmax < fd+1) |
243 | osxsel_inmax = fd+1; |
244 | [osxsel_inlock unlock]; |
245 | osxsel_start_select(); |
246 | |
247 | /* |
248 | * We must return an `id' which will be passed back to us at |
249 | * the time of uxsel_input_remove. Since we have no need to |
250 | * store ids in that sense, we might as well go with the fd |
251 | * itself. |
252 | */ |
253 | return fd; |
254 | } |
255 | |
256 | void uxsel_input_remove(int id) |
257 | { |
258 | /* |
259 | * Remove the fd from all the input fd_sets. In this |
260 | * implementation, the simplest way to do that is to call |
261 | * uxsel_input_add with rwx==0! |
262 | */ |
263 | uxsel_input_add(id, 0); |
264 | } |
265 | |
266 | /* |
267 | * Function called in the main thread to process results. It will |
268 | * have to read the output fd_sets, go through them, call back to |
269 | * uxsel with the results, and then write to the inter-thread pipe. |
270 | * |
271 | * This function will have to be called from an event handler in |
272 | * osxmain.m, which will therefore necessarily contain a small part |
273 | * of this mechanism (along with calling osxsel_init). |
274 | */ |
275 | void osxsel_process_results(void) |
276 | { |
277 | int i; |
278 | |
279 | /* |
280 | * We must write to the pipe to start a fresh select _even if_ |
281 | * there were no changes. So for efficiency, we set a flag here |
282 | * which inhibits uxsel_input_{add,remove} from writing to the |
283 | * pipe; then once we finish processing, we clear the flag |
284 | * again and write a single byte ourselves. It's cleaner, |
285 | * because it wakes up the select thread fewer times. |
286 | */ |
287 | inhibit_start_select = TRUE; |
288 | |
289 | [osxsel_outlock lock]; |
290 | |
291 | for (i = 0; i < osxsel_outmax; i++) { |
292 | if (FD_ISSET(i, &osxsel_xfds_out)) |
293 | select_result(i, 4); |
294 | } |
295 | for (i = 0; i < osxsel_outmax; i++) { |
296 | if (FD_ISSET(i, &osxsel_rfds_out)) |
297 | select_result(i, 1); |
298 | } |
299 | for (i = 0; i < osxsel_outmax; i++) { |
300 | if (FD_ISSET(i, &osxsel_wfds_out)) |
301 | select_result(i, 2); |
302 | } |
303 | |
304 | [osxsel_outlock unlock]; |
305 | |
306 | inhibit_start_select = FALSE; |
307 | osxsel_start_select(); |
308 | } |