1 // This file is part of BOINC.
2 // http://boinc.berkeley.edu
3 // Copyright (C) 2010 University of California
4 //
5 // BOINC is free software; you can redistribute it and/or modify it
6 // under the terms of the GNU Lesser General Public License
7 // as published by the Free Software Foundation,
8 // either version 3 of the License, or (at your option) any later version.
9 //
10 // BOINC is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13 // See the GNU Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public License
16 // along with BOINC.  If not, see <http://www.gnu.org/licenses/>.
17 //
18 //
19 // This is a XScreenSaver compatible BOINC screensaver for Unix/X11.
20 //
21 // To use this screensaver, please add the following to the 'programs'
22 // preference in your .xscreensaver file:
23 //
24 // GL:  "boincscr -root  \n\"
25 //
26 // If your BOINC directory differs from /var/lib/boinc-client, you can use
27 // the -boinc_dir command line argument.
28 //
29 // When run, this screensaver connects to the BOINC client via RPC, asks for
30 // graphics providing tasks and starts a random graphics application. The window
31 // created by the graphics appliacation is then searched for using X11 window
32 // properties, such as "WM_COMMAND". Not every graphics application seems to
33 // support this, but this method has been successfully tested with Einstein@Home
34 // and climateprediction.net. When the graphics application window
35 // has been found, it will be embedded into the XScreenSaver-provided
36 // fullscreen-window, the root window, the preview window or a newly created
37 // window, depending on the environment, using the XEMBED method.
38 
39 // These headers must be included before other
40 // headers to avoid compiler errors.
41 #include "gui_rpc_client.h"
42 #include "boinc_api.h"
43 
44 #include <cstdio>
45 #include <cstdlib>
46 #include <ctime>
47 #include <iostream>
48 #include <limits>
49 #include <sstream>
50 #include <string>
51 
52 #include <pthread.h>
53 #include <unistd.h>
54 
55 extern "C" {
56 #include <xcb/xcb.h>
57 #include <xcb/xcb_atom.h>
58 }
59 
60 /// A screensaver window class.
61 /** Creates a window in the size of the given parent.
62     When not in windowed mode, it is overwrite-redirected.
63     It shows the text "screensaver loading" when redrwan.
64     A client window may be xembedded into it, which will also be resized.
65 */
66 class scr_window {
67 private:
68   /// X server connection
69   xcb_connection_t *con;
70 
71   /// X screen
72   xcb_screen_t *scr;
73 
74   /// window id
75   xcb_window_t win;
76 
77   /// text graphics context id
78   xcb_gcontext_t txt_gc;
79 
80   /// text font
81   xcb_font_t font;
82 
83   /// parent window size;
84   uint16_t width;
85   uint16_t height;
86 
87   /// client window
88   xcb_window_t client_win;
89 
90   /// text to be drawn
91   std::string text;
92 
93   /// Small helper function to convert std::string to xcb_char2b_t*
94   /** Remember to delete[] the returned string.
95    */
char2b(std::string str)96   xcb_char2b_t *char2b(std::string str) {
97     xcb_char2b_t *s = new xcb_char2b_t[str.size()];
98     if (!s) return NULL;
99     for (unsigned int c = 0; c < str.size(); c++) {
100         s[c].byte1 = '\0';
101         s[c].byte2 = str[c];
102     }
103     return s;
104 }
105 
106 public:
107   /// Constructs the screensaver window.
108   /** \param connection connection to a X sever
109       \param screen screen, where the window will be created
110       \param parent parent window (0 for screen->root)
111       \param windowed means no fullscreen
112   */
scr_window(xcb_connection_t * connection,xcb_screen_t * screen,xcb_window_t parent,bool windowed)113   scr_window(xcb_connection_t *connection, xcb_screen_t *screen,
114              xcb_window_t parent, bool windowed)
115     : con(connection), scr(screen), client_win(0)
116   {
117     uint32_t mask;
118     uint32_t values[3];
119     xcb_void_cookie_t cookie;
120     xcb_generic_error_t *error = NULL;
121     if(!parent) parent = scr->root;
122 
123     // use parent window size when not in windowed mode
124     if(!windowed) {
125         xcb_get_geometry_cookie_t geo_cookie = xcb_get_geometry(con, parent);
126         xcb_get_geometry_reply_t *reply =
127           xcb_get_geometry_reply(con, geo_cookie, &error);
128         if(error) {
129             std::cerr << "Could not get parent window geometry." << std::endl;
130             exit(1);
131         }
132         width = reply->width;
133         height = reply->height;
134         free(reply);
135     } else {
136         // use some defaults in windowed mode
137         width = 640;
138         height = 480;
139     }
140 
141     if(windowed) {
142         // create a black maybe override-redirected window
143         // and register for expose and resize events.
144         mask = XCB_CW_BACK_PIXEL | XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK;
145         values[0] = scr->black_pixel;
146         values[1] = !windowed; // only if in fullscreen mode, otherwise normal window
147         values[2] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY;
148         win = xcb_generate_id(con);
149         cookie = xcb_create_window_checked(
150             con, XCB_COPY_FROM_PARENT, win,
151             parent, 0, 0, width, height, 0,
152             XCB_WINDOW_CLASS_INPUT_OUTPUT,
153             scr->root_visual, mask, values
154         );
155         error = xcb_request_check(con, cookie);
156         if(error) {
157             std::cerr << "Could not create window." << std::endl;
158             exit(1);
159         }
160 
161         // map the window on the screen
162         xcb_map_window(con, win);
163         xcb_flush(con);
164     } else {
165         // directly use the parent window
166         win = parent;
167 
168         // cahnge window attributes like above
169         mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
170         values[0] = scr->black_pixel;
171         values[1] = XCB_EVENT_MASK_EXPOSURE;
172         cookie = xcb_change_window_attributes(con, win, mask, values);
173 
174         error = xcb_request_check(con, cookie);
175         if(error) {
176             std::cerr << "Could not configure window." << std::endl;
177             exit(1);
178         }
179     }
180 
181     // open a font. "fixed" should hopefully be available everywhere
182     font = xcb_generate_id(con);
183     std::string font_name = "fixed";
184     cookie = xcb_open_font_checked(con, font, font_name.size(),
185                                    font_name.c_str());
186     error = xcb_request_check(con, cookie);
187     if(error) {
188         std::cerr << "Could not open font " << font_name << "." << std::endl;
189         exit(1);
190     }
191 
192     // allocate white text graphics context with above font
193     txt_gc = xcb_generate_id(con);
194     mask = XCB_GC_FOREGROUND | XCB_GC_FONT;
195     values[0] = scr->white_pixel;
196     values[1] = font;
197     cookie = xcb_create_gc_checked(con, txt_gc, win, mask, values);
198     error = xcb_request_check(con, cookie);
199     if(error) {
200         std::cerr << "Could not create graphics context." << std::endl;
201         exit(1);
202     }
203   }
204 
205   /// Destructor
~scr_window()206   ~scr_window() {
207     // clean up
208     xcb_unmap_window(con, win);
209     xcb_destroy_window(con, win);
210   }
211 
212   /// Returns the window id.
get_window_id()213   xcb_window_t get_window_id() {
214     return win;
215   }
216 
217   /// Sets the text to be drawn
set_text(std::string txt)218   void set_text(std::string txt) {
219     text = txt;
220     redraw();
221   }
222 
223   /// Redraws the window.
224   /** Draws a black background with white text.
225       Should be calld when an expose event is received.
226   */
redraw()227   void redraw() {
228     // convert the text to be displayed.
229     xcb_char2b_t *str = char2b(text);
230 
231     // get the dimensions of the text
232     xcb_query_text_extents_cookie_t cookie =
233       xcb_query_text_extents(con, font, text.size(), str);
234     delete[] str;
235 
236     xcb_generic_error_t *error;
237     xcb_query_text_extents_reply_t *reply =
238       xcb_query_text_extents_reply(con, cookie, &error);
239     if(error)
240       {
241         std::cerr << "Could not query text extents." << std::endl;
242         exit(1);
243       }
244 
245     // draw the text in the middle of the window.
246     xcb_image_text_8(con, text.size(), win, txt_gc,
247                      width/2 - reply->overall_width/2, height/2, text.c_str());
248 
249     free(reply);
250     xcb_flush(con);
251   }
252 
253   /// Notifies the window on resizes and resizes the client, if any.
254   /** Should be called when a configure notify event is received.
255    */
resize(uint16_t w,uint16_t h)256   void resize(uint16_t w, uint16_t h) {
257     width = w;
258     height = h;
259 
260     // resize client window, if any.
261     if(client_win) {
262         // moving the client back to (0, 0) is required when maximizing
263         uint32_t values[4] = { 0, 0, width, height };
264         uint32_t mask = XCB_CONFIG_WINDOW_X |XCB_CONFIG_WINDOW_Y |
265           XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT;
266         xcb_void_cookie_t cookie =
267           xcb_configure_window_checked(con, client_win, mask, values);
268 
269         xcb_generic_error_t *error = xcb_request_check(con, cookie);
270         if(error)
271           {
272             std::cerr << "Could not resize client." << std::endl;
273             exit(1);
274         }
275     }
276 }
277 
278   /// Xembeds a X window
xembed(xcb_window_t client)279   void xembed(xcb_window_t client) {
280     client_win = client;
281     uint32_t values[4];
282     uint32_t mask;
283     xcb_void_cookie_t cookie;
284     xcb_generic_error_t *error;
285 
286     // order of the following operations is important here!
287 
288     // move window below other windows
289     values[0] = XCB_STACK_MODE_BELOW;
290     mask = XCB_CONFIG_WINDOW_STACK_MODE;
291     cookie = xcb_configure_window_checked(con, client_win, mask, values);
292 
293     error = xcb_request_check(con, cookie);
294     if(error) {
295         std::cerr << "Could not change client attributes." << std::endl;
296         exit(1);
297     }
298 
299     // reparent client
300     cookie = xcb_reparent_window_checked(con, client_win, win, 0, 0);
301 
302     error = xcb_request_check(con, cookie);
303     if(error) {
304         std::cerr << "Could not reparent client." << std::endl;
305         exit(1);
306     }
307 
308     // move and resize client window
309     values[0] = 0;
310     values[1] = 0;
311     values[2] = width;
312     values[3] = height;
313     mask = XCB_CONFIG_WINDOW_X |XCB_CONFIG_WINDOW_Y |
314       XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT;
315     cookie = xcb_configure_window_checked(con, client_win, mask, values);
316 
317     error = xcb_request_check(con, cookie);
318     if(error) {
319         std::cerr << "Could not resize client." << std::endl;
320         exit(1);
321     }
322 
323     // make client overwrite-redirected
324     values[0] = true;
325     mask = XCB_CW_OVERRIDE_REDIRECT;
326     cookie = xcb_change_window_attributes(con, client_win, mask, values);
327 
328     error = xcb_request_check(con, cookie);
329     if(error) {
330         std::cerr << "Could not change client attributes." << std::endl;
331         exit(1);
332     }
333   }
334 };
335 
336 xcb_connection_t *con;
337 scr_window *window;
338 
339 /// X event loop
event_loop(void *)340 void *event_loop(void*) {
341   xcb_generic_event_t *event;
342 
343   // wait for X events and process them
344   while((event = xcb_wait_for_event(con))) {
345       switch(event->response_type & ~0x80) {
346         case XCB_EXPOSE:
347           {
348             xcb_expose_event_t *expose_event
349               = reinterpret_cast<xcb_expose_event_t*>(event);
350 
351             // ignore the expose event, if there are more waiting.
352             if(!expose_event->count && window && window->get_window_id() ==
353                expose_event->window) window->redraw();
354             break;
355           }
356         case XCB_CONFIGURE_NOTIFY:
357           {
358             xcb_configure_notify_event_t *configure_notify_event
359               = reinterpret_cast<xcb_configure_notify_event_t*>(event);
360 
361             if(window && window->get_window_id() ==
362                configure_notify_event->window)
363               window->resize(configure_notify_event->width,
364                              configure_notify_event->height);
365             break;
366           }
367         default:
368           // ignore
369           break;
370         }
371       free(event);
372    }
373    pthread_exit(0);
374 }
375 
376 /// Program entry point.
main(int argc,char * argv[])377 int main(int argc, char *argv[]) {
378   unsigned long int window_id = 0;
379   bool windowed = true;
380   std::string boinc_wd = "/var/lib/boinc-client";
381 
382   // parse command line
383   for(int c = 0; c < argc; c++) {
384       std::string option = argv[c];
385       if(option == "-window-id" && argv[c+1])
386         sscanf(argv[++c], "%lx", &window_id);
387       else if(option == "-root")
388         windowed = false;
389       else if (option == "-window")
390         windowed = true;
391       else if (option == "-boinc_dir")
392         if(argv[++c])
393           boinc_wd = argv[c];
394     }
395 
396   // if no -window-id command line argument is given,
397   // look for the XSCREENSAVER_WINDOW environment variable
398   if(!window_id) {
399       char *xssw = getenv("XSCREENSAVER_WINDOW");
400       if(xssw && *xssw) {
401           unsigned long int id = 0;
402           char c;
403           if (sscanf(xssw, "0x%lx %c", &id, &c) == 1 ||
404               sscanf(xssw, "%lu %c", &id, &c) == 1)
405             window_id = id;
406       }
407   }
408 
409   // connect to the X server using $DISPLAY
410   int screen_num = 0;
411   con = xcb_connect(NULL, &screen_num);
412 
413   if(!con) {
414       std::cerr << "Cannot connect to your X server." << std::endl
415                 << "Please check if it's running and whether your DISPLAY "
416                 << "environment variable is set correctly." << std::endl;
417       return 1;
418    }
419 
420   // get default screen
421   xcb_screen_t *screen = NULL;
422   for(xcb_screen_iterator_t it = xcb_setup_roots_iterator(xcb_get_setup(con));
423       it.rem; screen_num--, xcb_screen_next(&it))
424     if(!screen_num) screen = it.data;
425 
426   // create screensaver window
427   window = new scr_window(con, screen, window_id, windowed);
428   window->set_text("screensaver loading");
429 
430   // start the X event loop
431   pthread_t thread;
432   if(pthread_create(&thread, NULL, event_loop, NULL)) {
433       std::cerr << "Could not create a thread." << std::endl;
434       exit(1);
435   }
436 
437   // try to connect
438   RPC_CLIENT *rpc = new RPC_CLIENT;
439   if(rpc->init(NULL)) {
440       window->set_text("boinc not running");
441       pthread_join(thread, NULL);
442       return 0;
443   }
444 
445   // get results that support graphics
446   RESULTS results;
447   while(true) {
448       int suspend_reason = 0;
449       rpc->get_screensaver_tasks(suspend_reason, results);
450       if(results.results.empty()) sleep(10);
451       else break;
452   }
453 
454   srandom(time(NULL));
455   std::string graphics_cmd = "graphics_app";
456   // the loop skips projects that do not yet
457   // support the graphics_app soft link.
458   while(graphics_cmd == "graphics_app") {
459       // select a random result
460       int n = random() % results.results.size();
461       RESULT *result = results.results[n];
462 
463       // change to slot dir
464       std::stringstream stream;
465       stream << boinc_wd << "/slots/" << result->slot << "/";
466       std::string slot_dir = stream.str();
467       if(chdir(slot_dir.c_str())) {
468           perror("chdir");
469           exit(1);
470       }
471 
472       // resolve graphics_app soft link
473       boinc_resolve_filename_s(graphics_cmd.c_str(), graphics_cmd);
474   }
475 
476   // fork and...
477   pid_t pid = fork();
478   if(pid == -1) {
479       perror("fork");
480       exit(1);
481   }
482 
483   // ...spawn graphics app
484   if(!pid) // child
485     if(execl(graphics_cmd.c_str(), graphics_cmd.c_str(), NULL)) {
486         perror("exec");
487         exit(1);
488     }
489 
490   // look for our graphics app
491   // do this 10 times, every 1/2 seconds, then give up.
492   //
493   xcb_window_t client = 0;
494   for(int n = 0; n < 10; n++) {
495       // get list of x clients
496       xcb_intern_atom_cookie_t cookie0=xcb_intern_atom(
497             con, 0, strlen("_NET_CLIENT_LIST"), "_NET_CLIENT_LIST"
498       );
499       xcb_intern_atom_reply_t *reply0=xcb_intern_atom_reply(con, cookie0, NULL);
500 
501       xcb_get_property_cookie_t cookie =
502         xcb_get_property(con, 0, screen->root, reply0->atom, XCB_ATOM_WINDOW, 0,
503                          std::numeric_limits<uint32_t>::max());
504 
505       xcb_generic_error_t  *error;
506       xcb_get_property_reply_t *reply =
507         xcb_get_property_reply(con, cookie, &error);
508       if(error) {
509           std::cerr << "Could not get client list." << std::endl;
510           exit(1);
511       }
512 
513       xcb_window_t *clients =
514         static_cast<xcb_window_t*>(xcb_get_property_value(reply));
515 
516       // check if one of them is our graphics app
517       for(unsigned int c = 0; c < reply->length; c++) {
518           xcb_get_property_reply_t *reply2;
519 
520           // check WM_COMMAND
521           cookie = xcb_get_property(con, 0, clients[c], XCB_ATOM_WM_COMMAND, XCB_ATOM_STRING,
522                                     0, std::numeric_limits<uint32_t>::max());
523           reply2 = xcb_get_property_reply(con, cookie, &error);
524           if(!error) {  // ignore errors
525               char *command = static_cast<char*>(xcb_get_property_value(reply2));
526 
527               if(command && graphics_cmd == command) {
528                   client = clients[c];
529                   break;
530               }
531 
532               free(reply2);
533           }
534 
535           // check WM_CLASS
536           cookie = xcb_get_property(con, 0, clients[c], XCB_ATOM_WM_CLASS, XCB_ATOM_STRING,
537                                     0, std::numeric_limits<uint32_t>::max());
538           reply2 = xcb_get_property_reply(con, cookie, &error);
539           if(!error) {  // ignore errors
540               char *clas = static_cast<char*>(xcb_get_property_value(reply2));
541 
542               size_t pos = graphics_cmd.find_last_of('/');
543               std::string executable;
544               if(pos == std::string::npos) executable = graphics_cmd;
545               else executable = graphics_cmd.substr(pos + 1);
546 
547               if(clas && executable == clas) {
548                   client = clients[c];
549                   break;
550                 }
551 
552               free(reply2);
553           }
554 
555           // More checks are possible, but a single method for all graphics
556           // applications would be preferred, such as WM_CLASS = "BOINC".
557       }
558 
559       free(reply);
560 
561       if(client) break;
562 
563       usleep(500000);
564   }
565 
566   // if the client window was found, xembed it
567   if(client)
568     window->xembed(client);
569 
570   pthread_join(thread, NULL);
571 
572   delete window;
573   xcb_disconnect(con);
574   return 0;
575 }
576