boinc/clientscr/screensaver_x11.cpp

600 lines
18 KiB
C++

// This file is part of BOINC.
// http://boinc.berkeley.edu
// Copyright (C) 2010 University of California
//
// BOINC is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
//
// BOINC is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with BOINC. If not, see <http://www.gnu.org/licenses/>.
//
//
// An XScreenSaver compatible BOINC screensaver for Unix/X11.
//
// To use this screensaver, add the following to the 'programs'
// preference in your .xscreensaver file:
//
// GL: "boincscr -root \n\"
//
// If your BOINC directory differs from /var/lib/boinc, you can use
// the -boinc_dir command line argument.
//
// When run, this screensaver connects to the BOINC client via RPC, asks for
// graphics providing tasks and starts a random graphics application. The window
// created by the graphics application is then searched for using X11 window
// properties, such as "WM_COMMAND". Not every graphics application seems to
// support this, but this method has been successfully tested with Einstein@Home
// and climateprediction.net. When the graphics application window
// has been found, it will be embedded into the XScreenSaver-provided
// fullscreen-window, the root window, the preview window or a newly created
// window, depending on the environment, using the XEMBED method.
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <limits>
#include <sstream>
#include <string>
#include <pthread.h>
#include <unistd.h>
extern "C" {
#include <xcb/xcb.h>
#include <xcb/xcb_atom.h>
}
#include "gui_rpc_client.h"
#include "boinc_api.h"
// A screensaver window class.
// Creates a window in the size of the given parent.
// When not in windowed mode, it is overwrite-redirected.
// It shows the text "screensaver loading" when redrawn.
// A client window may be xembedded into it, which will also be resized.
//
class scr_window {
private:
// X server connection
xcb_connection_t *con;
// X screen
xcb_screen_t *scr;
// window id
xcb_window_t win;
// text graphics context id
xcb_gcontext_t txt_gc;
// text font
xcb_font_t font;
// parent window size;
uint16_t width;
uint16_t height;
// client window
xcb_window_t client_win;
// text to be drawn
std::string text;
// convert std::string to xcb_char2b_t*
// Remember to delete[] the returned string.
//
xcb_char2b_t *char2b(std::string str) {
xcb_char2b_t *s = new xcb_char2b_t[str.size()];
if (!s) return NULL;
for (unsigned int c = 0; c < str.size(); c++) {
s[c].byte1 = '\0';
s[c].byte2 = str[c];
}
return s;
}
public:
// Constructs the screensaver window.
// \param connection connection to a X sever
// \param screen screen, where the window will be created
// \param parent parent window (0 for screen->root)
// \param windowed means no fullscreen
//
scr_window(xcb_connection_t *connection, xcb_screen_t *screen,
xcb_window_t parent, bool windowed
): con(connection), scr(screen), client_win(0)
{
uint32_t mask;
uint32_t values[3];
xcb_void_cookie_t cookie;
xcb_generic_error_t *error = NULL;
if (!parent) parent = scr->root;
// use parent window size when not in windowed mode
if (!windowed) {
xcb_get_geometry_cookie_t geo_cookie = xcb_get_geometry(con, parent);
xcb_get_geometry_reply_t *reply =
xcb_get_geometry_reply(con, geo_cookie, &error);
if (error) {
std::cerr << "Could not get parent window geometry." << std::endl;
exit(1);
}
width = reply->width;
height = reply->height;
free(reply);
} else {
// use some defaults in windowed mode
width = 640;
height = 480;
}
if (windowed) {
// create a black maybe override-redirected window
// and register for expose and resize events.
mask = XCB_CW_BACK_PIXEL | XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK;
values[0] = scr->black_pixel;
values[1] = !windowed; // only if in fullscreen mode, otherwise normal window
values[2] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY;
win = xcb_generate_id(con);
cookie = xcb_create_window_checked(
con, XCB_COPY_FROM_PARENT, win, parent, 0, 0, width, height, 0,
XCB_WINDOW_CLASS_INPUT_OUTPUT, scr->root_visual, mask, values
);
error = xcb_request_check(con, cookie);
if (error) {
std::cerr << "Could not create window." << std::endl;
exit(1);
}
// map the window on the screen
xcb_map_window(con, win);
xcb_flush(con);
} else {
// directly use the parent window
win = parent;
// change window attributes like above
mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
values[0] = scr->black_pixel;
values[1] = XCB_EVENT_MASK_EXPOSURE;
cookie = xcb_change_window_attributes(con, win, mask, values);
error = xcb_request_check(con, cookie);
if (error) {
std::cerr << "Could not configure window." << std::endl;
exit(1);
}
}
// open a font. "fixed" should hopefully be available everywhere
font = xcb_generate_id(con);
std::string font_name = "fixed";
cookie = xcb_open_font_checked(
con, font, font_name.size(), font_name.c_str()
);
error = xcb_request_check(con, cookie);
if (error) {
std::cerr << "Could not open font " << font_name << "." << std::endl;
exit(1);
}
// allocate white text graphics context with above font
txt_gc = xcb_generate_id(con);
mask = XCB_GC_FOREGROUND | XCB_GC_FONT;
values[0] = scr->white_pixel;
values[1] = font;
cookie = xcb_create_gc_checked(con, txt_gc, win, mask, values);
error = xcb_request_check(con, cookie);
if (error) {
std::cerr << "Could not create graphics context." << std::endl;
exit(1);
}
}
// Destructor
~scr_window() {
// clean up
xcb_unmap_window(con, win);
xcb_destroy_window(con, win);
}
// Returns the window id.
xcb_window_t get_window_id() {
return win;
}
// Sets the text to be drawn
void set_text(std::string txt) {
text = txt;
redraw();
}
// Redraws the window.
// Draws a black background with white text.
// Should be called when an expose event is received.
//
void redraw() {
// convert the text to be displayed.
xcb_char2b_t *str = char2b(text);
// get the dimensions of the text
xcb_query_text_extents_cookie_t cookie =
xcb_query_text_extents(con, font, text.size(), str)
;
delete[] str;
xcb_generic_error_t *error;
xcb_query_text_extents_reply_t *reply =
xcb_query_text_extents_reply(con, cookie, &error)
;
if (error) {
std::cerr << "Could not query text extents." << std::endl;
exit(1);
}
// draw the text in the middle of the window.
xcb_image_text_8(con, text.size(), win, txt_gc,
width/2 - reply->overall_width/2, height/2, text.c_str()
);
free(reply);
xcb_flush(con);
}
// Notifies the window on resizes and resizes the client, if any.
// Should be called when a configure notify event is received.
//
void resize(uint16_t w, uint16_t h) {
width = w;
height = h;
// resize client window, if any.
if (client_win) {
// moving the client back to (0, 0) is required when maximizing
uint32_t values[4] = { 0, 0, width, height };
uint32_t mask = XCB_CONFIG_WINDOW_X |XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT
;
xcb_void_cookie_t cookie =
xcb_configure_window_checked(con, client_win, mask, values
);
xcb_generic_error_t *error = xcb_request_check(con, cookie);
if (error) {
std::cerr << "Could not resize client." << std::endl;
exit(1);
}
}
}
// Xembeds a X window
void xembed(xcb_window_t client) {
client_win = client;
uint32_t values[4];
uint32_t mask;
xcb_void_cookie_t cookie;
xcb_generic_error_t *error;
// order of the following operations is important here!
// move window below other windows
values[0] = XCB_STACK_MODE_BELOW;
mask = XCB_CONFIG_WINDOW_STACK_MODE;
cookie = xcb_configure_window_checked(con, client_win, mask, values);
error = xcb_request_check(con, cookie);
if (error) {
std::cerr << "Could not change client attributes." << std::endl;
exit(1);
}
// reparent client
cookie = xcb_reparent_window_checked(con, client_win, win, 0, 0);
error = xcb_request_check(con, cookie);
if (error) {
std::cerr << "Could not reparent client." << std::endl;
exit(1);
}
// move and resize client window
values[0] = 0;
values[1] = 0;
values[2] = width;
values[3] = height;
mask = XCB_CONFIG_WINDOW_X |XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT
;
cookie = xcb_configure_window_checked(con, client_win, mask, values);
error = xcb_request_check(con, cookie);
if (error) {
std::cerr << "Could not resize client." << std::endl;
exit(1);
}
// make client overwrite-redirected
values[0] = true;
mask = XCB_CW_OVERRIDE_REDIRECT;
cookie = xcb_change_window_attributes(con, client_win, mask, values);
error = xcb_request_check(con, cookie);
if (error) {
std::cerr << "Could not change client attributes." << std::endl;
exit(1);
}
}
};
xcb_connection_t *con;
scr_window *window;
// X event loop
void *event_loop(void*) {
xcb_generic_event_t *event;
// wait for X events and process them
while((event = xcb_wait_for_event(con))) {
switch(event->response_type & ~0x80) {
case XCB_EXPOSE:
{
xcb_expose_event_t *expose_event
= reinterpret_cast<xcb_expose_event_t*>(event)
;
// ignore the expose event, if there are more waiting.
if (!expose_event->count
&& window
&& window->get_window_id() == expose_event->window
) {
window->redraw();
}
break;
}
case XCB_CONFIGURE_NOTIFY:
{
xcb_configure_notify_event_t *configure_notify_event
= reinterpret_cast<xcb_configure_notify_event_t*>(event)
;
if (window && window->get_window_id() == configure_notify_event->window) {
window->resize(configure_notify_event->width,
configure_notify_event->height
);
}
break;
}
default:
// ignore
break;
}
free(event);
}
pthread_exit(0);
}
// Program entry point.
int main(int argc, char *argv[]) {
unsigned long int window_id = 0;
bool windowed = true;
std::string boinc_wd = "/var/lib/boinc";
// parse command line
for (int c = 0; c < argc; c++) {
std::string option = argv[c];
if (option == "-window-id" && argv[c+1]) {
sscanf(argv[++c], "%lx", &window_id);
} else if (option == "-root") {
windowed = false;
} else if (option == "-window") {
windowed = true;
} else if (option == "-boinc_dir") {
if (argv[++c]) {
boinc_wd = argv[c];
}
}
}
// if no -window-id command line argument is given,
// look for the XSCREENSAVER_WINDOW environment variable
if (!window_id) {
char *xssw = getenv("XSCREENSAVER_WINDOW");
if (xssw && *xssw) {
unsigned long int id = 0;
char c;
if (sscanf(xssw, "0x%lx %c", &id, &c) == 1 ||
sscanf(xssw, "%lu %c", &id, &c) == 1
) {
window_id = id;
}
}
}
// connect to the X server using $DISPLAY
int screen_num = 0;
con = xcb_connect(NULL, &screen_num);
if (!con) {
std::cerr << "Cannot connect to your X server." << std::endl
<< "Please check if it's running and whether your DISPLAY "
<< "environment variable is set correctly." << std::endl
;
return 1;
}
// get default screen
xcb_screen_t *screen = NULL;
for (xcb_screen_iterator_t it = xcb_setup_roots_iterator(xcb_get_setup(con));
it.rem; screen_num--, xcb_screen_next(&it)
) {
if (!screen_num) screen = it.data;
}
// create screensaver window
window = new scr_window(con, screen, window_id, windowed);
window->set_text("screensaver loading");
// start the X event loop
pthread_t thread;
if (pthread_create(&thread, NULL, event_loop, NULL)) {
std::cerr << "Could not create a thread." << std::endl;
exit(1);
}
// try to connect
RPC_CLIENT *rpc = new RPC_CLIENT;
if (rpc->init(NULL)) {
window->set_text("boinc not running");
pthread_join(thread, NULL);
return 0;
}
// get results that support graphics
RESULTS results;
while(true) {
int suspend_reason = 0;
rpc->get_screensaver_tasks(suspend_reason, results);
if (results.results.empty()) sleep(10);
else break;
}
srandom(time(NULL));
std::string graphics_cmd = "graphics_app";
// the loop skips projects that do not yet
// support the graphics_app soft link.
while(graphics_cmd == "graphics_app") {
// select a random result
int n = random() % results.results.size();
RESULT *result = results.results[n];
// change to slot dir
std::stringstream stream;
stream << boinc_wd << "/slots/" << result->slot << "/";
std::string slot_dir = stream.str();
if (chdir(slot_dir.c_str())) {
perror("chdir");
exit(1);
}
// resolve graphics_app soft link
boinc_resolve_filename_s(graphics_cmd.c_str(), graphics_cmd);
}
// fork and...
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(1);
}
// ...spawn graphics app
if (!pid) { // child
if (execl(graphics_cmd.c_str(), graphics_cmd.c_str(), NULL)) {
perror("exec");
exit(1);
}
}
// look for our graphics app
// do this 10 times, every 1/2 seconds, then give up.
//
xcb_window_t client = 0;
for (int n = 0; n < 10; n++) {
// get list of x clients
xcb_intern_atom_cookie_t cookie0=xcb_intern_atom(
con, 0, strlen("_NET_CLIENT_LIST"), "_NET_CLIENT_LIST"
);
xcb_intern_atom_reply_t *reply0=xcb_intern_atom_reply(con, cookie0, NULL);
xcb_get_property_cookie_t cookie = xcb_get_property(
con, 0, screen->root, reply0->atom, XCB_ATOM_WINDOW, 0,
std::numeric_limits<uint32_t>::max()
);
xcb_generic_error_t *error;
xcb_get_property_reply_t *reply = xcb_get_property_reply(con, cookie, &error);
if (error) {
std::cerr << "Could not get client list." << std::endl;
exit(1);
}
xcb_window_t *clients =
static_cast<xcb_window_t*>(xcb_get_property_value(reply));
// check if one of them is our graphics app
for (unsigned int c = 0; c < reply->length; c++) {
xcb_get_property_reply_t *reply2;
// check WM_COMMAND
cookie = xcb_get_property(
con, 0, clients[c], XCB_ATOM_WM_COMMAND, XCB_ATOM_STRING,
0, std::numeric_limits<uint32_t>::max()
);
reply2 = xcb_get_property_reply(con, cookie, &error);
if (!error) { // ignore errors
char *command = static_cast<char*>(xcb_get_property_value(reply2));
if (command && graphics_cmd == command) {
client = clients[c];
break;
}
free(reply2);
}
// check WM_CLASS
cookie = xcb_get_property(
con, 0, clients[c], XCB_ATOM_WM_CLASS, XCB_ATOM_STRING,
0, std::numeric_limits<uint32_t>::max()
);
reply2 = xcb_get_property_reply(con, cookie, &error);
if (!error) { // ignore errors
char *clas = static_cast<char*>(xcb_get_property_value(reply2));
size_t pos = graphics_cmd.find_last_of('/');
std::string executable;
if (pos == std::string::npos) {
executable = graphics_cmd;
} else {
executable = graphics_cmd.substr(pos + 1);
}
if (clas && executable == clas) {
client = clients[c];
break;
}
free(reply2);
}
// More checks are possible, but a single method for all graphics
// applications would be preferred, such as WM_CLASS = "BOINC".
}
free(reply);
if (client) break;
usleep(500000);
}
// if the client window was found, xembed it
if (client) {
window->xembed(client);
}
pthread_join(thread, NULL);
delete window;
xcb_disconnect(con);
return 0;
}