From http://www.jwz.org/xscreensaver/xscreensaver-5.35.tar.gz
[xscreensaver] / hacks / webcollage-cocoa.m
1 /* xscreensaver, Copyright (c) 2006-2015 Jamie Zawinski <jwz@jwz.org>
2  *
3  * Permission to use, copy, modify, distribute, and sell this software and its
4  * documentation for any purpose is hereby granted without fee, provided that
5  * the above copyright notice appear in all copies and that both that
6  * copyright notice and this permission notice appear in supporting
7  * documentation.  No representations are made about the suitability of this
8  * software for any purpose.  It is provided "as is" without express or 
9  * implied warranty.
10  */
11
12  /* This is the Cocoa shim for webcollage.
13
14     It runs the webcollage perl script
15     (in "WebCollage.saver/Contents/Resources/webcollage")
16     at the end of a pipe; each time that script updates the image file on
17     disk it prints the file name, and this program loads and displays that
18    image.
19
20     The script uses "WebCollage.saver/Contents/Resources/webcollage-helper"
21     to paste the images together in the usual way.
22   */
23
24 #include <math.h>
25 #include <unistd.h>
26 #include <stdlib.h>
27 #include <signal.h>
28 #import <Cocoa/Cocoa.h>
29
30 #include "screenhack.h"
31
32
33 typedef struct {
34   Display *dpy;
35   Window window;
36   XWindowAttributes xgwa;
37   int delay;
38   pid_t pid;
39   FILE *pipe_fd;
40   XtInputId pipe_id;
41   Bool verbose_p;
42 } state;
43
44
45 /* Violating the cardinal rule of "don't use global variables",
46    but we need to get at these from the atexit() handler, and
47    the callback doesn't take a closure arg.  Because apparently
48    those hadn't been invented yet in the seventies.
49  */
50 static state *all_states[50] = { 0, };
51
52 static void webcollage_atexit (void);
53 static void signal_handler (int sig);
54
55
56 static void
57 subproc_cb (XtPointer closure, int *source, XtInputId *id)
58 {
59   /* state *st = (state *) closure; */
60   /* st->input_available_p = True; */
61 }
62
63
64 /* whether there is data available to be read on the file descriptor
65  */
66 static int
67 input_available_p (int fd)
68 {
69   struct timeval tv = { 0, };
70   fd_set fds;
71 # if 0
72   /* This breaks on BSD, which uses bzero() in the definition of FD_ZERO */
73   FD_ZERO (&fds);
74 # else
75   memset (&fds, 0, sizeof(fds));
76 # endif
77   FD_SET (fd, &fds);
78   return select (fd+1, &fds, NULL, NULL, &tv);
79 }
80
81
82 static void
83 display_image (Display *dpy, Window window, state *st, const char *file)
84 {
85   NSImage *image = [[NSImage alloc] 
86                      initWithContentsOfFile:
87                        [NSString stringWithCString: file
88                                           encoding: NSUTF8StringEncoding]];
89
90   if (! image) {
91     fprintf (stderr, "webcollage: failed to load \"%s\"\n", file);
92     return;
93   }
94
95   CGFloat w = [image size].width;
96   CGFloat h = [image size].height;
97   if (w <= 1 || h <= 1) {
98     fprintf (stderr, "webcollage: unparsable image \"%s\"\n", file);
99     [image release];
100     return;
101   }
102
103   jwxyz_draw_NSImage_or_CGImage (dpy, window, True, image, 0, 0);
104   [image release];
105 }
106
107
108 static void
109 open_pipe (state *st)
110 {
111   /* This mess is because popen() doesn't give us the pid.
112    */
113
114   pid_t forked;
115   int fds [2];
116   int in, out;
117   char buf[1024];
118
119   char *av[20];
120   int ac = 0;
121
122   int timeout   = get_integer_resource (st->dpy, "timeout", "Timeout");
123   int delay     = get_integer_resource (st->dpy, "delay",   "Delay");
124   float opacity = get_float_resource   (st->dpy, "opacity", "Opacity");
125   char *filter  = get_string_resource  (st->dpy, "filter",  "Filter");
126   char *filter2 = get_string_resource  (st->dpy, "filter2", "Filter2");
127
128   av[ac++] = strdup ("webcollage");
129   av[ac++] = strdup ("-cocoa");
130
131   av[ac++] = strdup ("-size");
132   sprintf (buf, "%dx%d", st->xgwa.width, st->xgwa.height);
133   av[ac++] = strdup (buf);
134
135   av[ac++] = strdup ("-timeout"); sprintf (buf, "%d", timeout);
136   av[ac++] = strdup (buf);
137   av[ac++] = strdup ("-delay");   sprintf (buf, "%d", delay);
138   av[ac++] = strdup (buf);
139   av[ac++] = strdup ("-opacity"); sprintf (buf, "%.2f", opacity);
140   av[ac++] = strdup (buf);
141
142   if (filter && *filter) {
143     av[ac++] = strdup ("-filter");
144     av[ac++] = filter;
145   }
146   if (filter2 && *filter2) {
147     av[ac++] = strdup ("-filter2");
148     av[ac++] = filter2;
149   }
150
151   av[ac] = 0;
152
153
154   if (st->verbose_p) {
155     fprintf (stderr, "webcollage: launching:");
156     int i;
157     for (i = 0; i < ac; i++)
158       fprintf (stderr, " %s", av[i]);
159     fprintf (stderr, "\n");
160   }
161
162
163   if (pipe (fds))
164     {
165       perror ("webcollage: error creating pipe");
166       exit (1);
167     }
168
169   in = fds [0];
170   out = fds [1];
171
172   switch ((int) (forked = fork ()))
173     {
174     case -1:
175       {
176         perror ("webcollage: couldn't fork");
177         exit (1);
178       }
179     case 0:
180       {
181         int stdout_fd = 1;
182
183         close (in);  /* don't need this one */
184
185         if (dup2 (out, stdout_fd) < 0)          /* pipe stdout */
186           {
187             perror ("could not dup() a new stdout:");
188             exit (1);
189           }
190
191         execvp (av[0], av);                     /* shouldn't return. */
192
193         if (errno != ENOENT)
194           {
195             /* Ignore "no such file or directory" errors, unless verbose.
196                Issue all other exec errors, though. */
197             sprintf (buf, "webcollage: %s", av[0]);
198             perror (buf);
199           }
200
201         exit (1);                              /* exits fork */
202         break;
203       }
204     default:
205       {
206         st->pipe_fd = fdopen (in, "r");
207         close (out);  /* don't need this one */
208       }
209     }
210
211   while (ac > 0)
212     free (av[--ac]);
213
214   if (! st->pipe_fd) abort();
215
216   st->pid = forked;
217   st->pipe_id =
218     XtAppAddInput (XtDisplayToApplicationContext (st->dpy), 
219                    fileno (st->pipe_fd),
220                    (XtPointer) (XtInputReadMask | XtInputExceptMask),
221                    subproc_cb, (XtPointer) st);
222
223   if (st->verbose_p)
224     fprintf (stderr, "webcollage: subprocess pid: %d\n", st->pid);
225 }
226
227
228 static void *
229 webcollage_init (Display *dpy, Window window)
230 {
231   state *st = (state *) calloc (1, sizeof(*st));
232   int i;
233   st->dpy = dpy;
234   st->window = window;
235   XGetWindowAttributes (st->dpy, st->window, &st->xgwa);
236
237   st->delay = 1000000;  /* check once a second */
238
239   // Log to syslog when FPS is turned on.
240   st->verbose_p = get_boolean_resource (dpy, "doFPS", "DoFPS");
241
242
243   static int done_once = 0;
244   if (! done_once) {
245     done_once = 1;
246
247     if (atexit (webcollage_atexit)) {   // catch calls to exit()
248       perror ("webcollage: atexit");
249       exit (-1);
250     }
251
252     int sigs[] = { SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT,
253                    SIGFPE, SIGBUS, SIGSEGV, SIGSYS, /*SIGPIPE,*/
254                    SIGALRM, SIGTERM };
255     for (i = 0; i < sizeof(sigs)/sizeof(*sigs); i++) {
256       if (signal (sigs[i], signal_handler)) {
257         perror ("webcollage: signal");
258         //exit (1);
259       }
260     }
261   }
262
263
264   open_pipe (st);
265
266   i = 0;
267   while (all_states[i]) i++;
268   all_states[i] = st;
269
270   return st;
271 }
272
273
274 static unsigned long
275 webcollage_draw (Display *dpy, Window window, void *closure)
276 {
277   state *st = (state *) closure;
278
279   if (! st->pipe_fd) 
280     exit (1);
281
282   if (! input_available_p (fileno (st->pipe_fd)))
283     return st->delay;
284
285   char buf[10240];
286   int n = read (fileno (st->pipe_fd),
287                 (void *) buf,
288                 sizeof(buf) - 1);
289   if (n <= 0)
290     {
291       XtRemoveInput (st->pipe_id);
292       st->pipe_id = 0;
293       // #### sometimes hangs -- pclose (st->pipe_fd);
294       st->pipe_fd = 0;
295
296       if (st->verbose_p)
297         fprintf (stderr, "webcollage: subprocess has exited: bailing.\n");
298
299       return st->delay * 10;
300     }
301
302   buf[n] = 0;
303   char *s = strchr (buf, '\n');
304   if (s) *s = 0;
305
306   const char *target = "COCOA LOAD ";
307   if (!strncmp (target, buf, strlen(target))) {
308     const char *file = buf + strlen(target);
309     display_image (dpy, window, st, file);
310   }
311
312   return st->delay;
313 }
314
315
316 static void
317 webcollage_reshape (Display *dpy, Window window, void *closure, 
318                     unsigned int w, unsigned int h)
319 {
320 }
321
322
323 static Bool
324 webcollage_event (Display *dpy, Window window, void *closure, XEvent *event)
325 {
326   return False;
327 }
328
329
330 static void
331 webcollage_atexit (void)
332 {
333   int i = 0;
334
335   if (all_states[0] && all_states[0]->verbose_p)
336     fprintf (stderr, "webcollage: atexit handler\n");
337
338   while (all_states[i]) {
339     state *st = all_states[i];
340     if (st->pid) {
341       if (st->verbose_p)
342         fprintf (stderr, "webcollage: kill %d\n", st->pid);
343       if (kill (st->pid, SIGTERM) < 0) {
344         fprintf (stderr, "webcollage: kill (%d, TERM): ", st->pid);
345         perror ("webcollage: kill");
346       }
347       st->pid = 0;
348     }
349     all_states[i] = 0;
350     i++;
351   }
352 }
353
354
355 static void
356 signal_handler (int sig)
357 {
358   if (all_states[0] && all_states[0]->verbose_p)
359     fprintf (stderr, "webcollage: signal %d\n", sig);
360   webcollage_atexit ();
361   exit (sig);
362 }
363
364
365 /* This is important because OSX doesn't actually kill the screen savers!
366    It just sends them a [ScreenSaverView stopAnimation] method.  Were
367    they to actually exit, the resultant SIGPIPE should reap the children,
368    but instead, we need to do it here.
369
370    On top of that, there's an atexit() handler because otherwise the
371    inferior perl script process was failing to die when SaverTester or
372    System Preferences exited.  I don't pretend to understand.
373
374    It still fails to clean up when I hit the stop button in Xcode.
375    WTF.
376  */
377 static void
378 webcollage_free (Display *dpy, Window window, void *closure)
379 {
380   state *st = (state *) closure;
381
382   if (st->verbose_p)
383     fprintf (stderr, "webcollage: free cb\n");
384
385   // Dammit dammit dammit!  Why won't this shit die when we pclose it!
386 //  killpg (0, SIGTERM);
387
388   webcollage_atexit();
389
390   if (st->pipe_id)
391     XtRemoveInput (st->pipe_id);
392
393   if (st->pipe_fd)
394     fclose (st->pipe_fd);
395
396   // Reap zombies.
397 # undef sleep
398   sleep (1);
399   int wait_status = 0;
400   waitpid (-1, &wait_status, 0);
401
402   free (st);
403 }
404
405
406 static const char *webcollage_defaults [] = {
407   ".background:         black",
408   ".foreground:         white",
409
410   "*timeout:            30",
411   "*delay:              2",
412   "*opacity:            0.85",
413   "*filter:             ",
414   "*filter2:            ",
415   0
416 };
417
418 static XrmOptionDescRec webcollage_options [] = {
419   { "-timeout",         ".timeout",     XrmoptionSepArg, 0 },
420   { "-delay",           ".delay",       XrmoptionSepArg, 0 },
421   { "-opacity",         ".opacity",     XrmoptionSepArg, 0 },
422   { "-filter",          ".filter",      XrmoptionSepArg, 0 },
423   { "-filter2",         ".filter2",     XrmoptionSepArg, 0 },
424   { 0, 0, 0, 0 }
425 };
426
427
428 XSCREENSAVER_MODULE ("WebCollage", webcollage)