################################################################################ # # # Folding@Home Client Control (FAHControl) # # Copyright (C) 2016-2020 foldingathome.org # # Copyright (C) 2010-2016 Stanford University # # # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # This program 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 General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program. If not, see . # # # ################################################################################ import sys import time import re import traceback import platform import urllib import gtk import glib import pygtk pygtk.require("2.0") import pango import webbrowser import shlex import subprocess from wraplabel import WrapLabel # OSX integration if sys.platform == 'darwin': try: from gtk_osxapplication import * except: import gtkosx_application from gtkosx_application import Application as OSXApplication from gtkosx_application import gtkosx_application_get_resource_path \ as quartz_application_get_resource_path from fah import * from fah.db import * from fah.util import * def set_tree_view_font(widget, font): for widget in iterate_container(widget): if isinstance(widget, gtk.TreeView): widget.modify_font(font) def append_tree_entry(model, path, iter, selection): selection.append((path, iter)) def get_tree_selection(tree_view): selection = [] tree_view.get_selection().selected_foreach(append_tree_entry, selection) return selection def remove_tree_selection(tree_view, callback = None): for path, iter in get_tree_selection(tree_view): if callback is not None: callback(path, iter) tree_view.get_model().remove(iter) def osx_version(): """ returns osx version as tuple of integers """ if sys.platform != 'darwin': return None try: ver = tuple([int(x) for x in platform.mac_ver()[0].split('.')]) except Exception as e: print (e) darwin_ver = platform.release().split('.') ver = (10, int(darwin_ver[0]) - 4, int(darwin_ver[1])) return ver def osx_add_GtkApplicationDelegate_methods(): # GtkApplicationDelegate Category via PyObjC def applicationShouldHandleReopen_hasVisibleWindows_(self, app, flag): # reopen event or dock icon clicked # restore windows if hidden controller = FAHControl.instance if controller is not None: controller.restore() return True try: import objc cls = objc.lookUpClass('GtkApplicationDelegate') sig1 = '%s%s%s%s%s' % (objc._C_NSBOOL, objc._C_ID, objc._C_SEL, objc._C_ID, objc._C_NSBOOL) objc.classAddMethods(cls, [ objc.selector( applicationShouldHandleReopen_hasVisibleWindows_, signature = sig1) ]) except Exception as e: print (e) def osx_accel_window_close(accel_group, acceleratable, keyval, modifier): acceleratable.hide() return True def osx_accel_window_minimize(accel_group, acceleratable, keyval, modifier): acceleratable.iconify() return True def load_fahcontrol_db(): db = Database(os.path.join(get_home_dir(), 'FAHControl.db')) db.validate() return db class FAHControl(SingleAppServer): client_cols = 'name status status_color address'.split() # NOTE: These URLs are here rather than in the Glade file because the # Glade editor strips the '&'s on save. Even if you use '&' the # ampersands get striped when resaved. team_stats_links = [ ['Folding@home', 'https://stats.foldingathome.org/team/%(team)s'], ['Extreme Overclocking', 'http://folding.extremeoverclocking.com/' 'team_summary.php?t=%(team)s'], ['Kakao Stats', 'http://kakaostats.com/tsum.php?t=%(team)s'], ['[H]ard Folding', 'http://www.hardfolding.com/fh_stats/index.php' '?pz=101&tnum=%(team)s'], ['Custom', ''], ] donor_stats_links = [ ['Folding@home', 'https://stats.foldingathome.org/donor/%(donor)s'], ['Custom', ''], ] folding_power_levels = ['Light', 'Medium', 'Full'] instance = None def __init__(self, glade = 'FAHControl.glade'): SingleAppServer.__init__(self) self.__class__.instance = self # Vars self.clients = {} self.clientsByAddress = {} self.active_client = None self.client_is_online = False self.selected_clients = set() self.status_clear_time = None self.window_visible = False self.viewer = None self.last_db_flush = 0 self.last_clients_update = 0 self.error_dialog = None self.restore_dialogs = [] self.last_clock = None self.timer_id = None self.folding_power_changing = False # Open database try: self.db = load_fahcontrol_db() except Exception as e: print (e) sys.exit(1) # OSX integration if sys.platform == 'darwin': self.osx_app = OSXApplication() self.osx_app.set_use_quartz_accelerators(True) self.osx_version = osx_version() self.is_old_gtk = gtk.gtk_version < (2,24) osx_add_GtkApplicationDelegate_methods() # URI hook gtk.link_button_set_uri_hook(self.on_uri_hook, None) # Style settings = gtk.settings_get_default() self.system_theme = settings.get_property('gtk-theme-name') if sys.platform == 'darwin': # Load standard key bindings for Mac and disable mnemonics resources = quartz_application_get_resource_path() rcfile = os.path.join(resources, 'themes/Mac/gtk-2.0-key/gtkrc') if os.path.exists(rcfile): gtk.rc_parse(rcfile) rcfile = os.path.join(os.path.expanduser("~"), '.FAHClient/gtkrc') if os.path.exists(rcfile): gtk.rc_parse(rcfile) self.mono_font = pango.FontDescription('Monospace') small_font = pango.FontDescription('Sans 8') # Default icon gtk.window_set_default_icon(get_icon('small')) # Filter glade if len(glade) < 1024: glade = open(glade, 'r').read() glade = re.subn('class="GtkLabel" id="wlabel', 'class="WrapLabel" id="wlabel', glade)[0] if sys.platform == 'darwin': # glade editor strips accel modifiers. add if missing glade = re.subn('accelerator *key="comma" *signal', 'accelerator key="comma" modifiers="GDK_META_MASK" signal', glade)[0] # Build GUI self.builder = builder = gtk.Builder() try: builder.add_from_string(glade) except: self.error('Failed to load UI file: %s' % glade) sys.exit(1) # Main window self.window = builder.get_object('window') self.window.set_geometry_hints(None, 440, 256, -1, -1, 800, 512) set_tree_view_font(self.window, self.mono_font) self.status_bar = builder.get_object('status_bar') self.ppd_label = builder.get_object('ppd_label') self.time_label = builder.get_object('time_label') # Panes self.panes = WidgetMap(self.window, 'paned') for name, pane in self.panes.items(): prop = name + '_position' # Load current value if self.db.has(prop): value = int(self.db.get(prop)) if value and value < 100: value = 100 # mimimum if not hidden pane.set_position(value) pane.connect('notify::position', self.store_property, prop) # Dialogs self.preferences_dialog = builder.get_object('preferences_dialog') self.client_dialog = builder.get_object('client_dialog') self.options_dialog = builder.get_object('options_dialog') self.core_options_dialog = builder.get_object('core_options_dialog') self.slot_dialog = builder.get_object('slot_dialog') self.about_dialog = builder.get_object('about_dialog') self.configure_dialog = builder.get_object('configure_dialog') # Note: The order of these dialogs is important since they are restored # in this order. Child dialogs cannot be restored before their # parents so they must be last. See restore() below. self.dialogs = [ self.about_dialog, self.preferences_dialog, self.client_dialog, self.slot_dialog, self.options_dialog, self.core_options_dialog, self.configure_dialog] # Dialog & window sizes self.windows = { 'preferences': self.preferences_dialog, 'client': self.client_dialog, 'options': self.options_dialog, 'core_options': self.core_options_dialog, 'slot': self.slot_dialog, 'about': self.about_dialog, 'main': self.window, } for name, win in self.windows.items(): if self.db.has(name + '_width'): width = int(self.db.get(name + '_width')) else: width = -1 if self.db.has(name + '_height'): height = int(self.db.get(name + '_height')) else: height = -1 if name == 'main': if 0 < width and width < 600: width = 600 if 0 < height and height < 400: height = 400 if 100 <= width and 100 <= height: win.resize(width, height) win.connect('configure_event', self.store_dimensions, name) # Tool bar builder.get_object('toolbar1').modify_font(small_font) button = builder.get_object('viewer_button') button.get_image().set_from_pixbuf(get_viewer_icon('small')) # About Dialog icon = builder.get_object('about_icon') icon.set_from_pixbuf(get_icon('medium')) about_version = builder.get_object('about_version') about_version.set_markup('Version: %s' % version) # Preferences self.theme_list = self.load_themes() widget = builder.get_object('theme_list') for theme in self.theme_list: widget.append(theme) # Client list self.client_hpane = builder.get_object('client_hpane') self.client_notebook = builder.get_object('client_notebook') self.client_config_notebook = \ builder.get_object('client_config_notebook') self.client_config_notebook.set_current_page(1) self.client_tree = builder.get_object('client_tree_view') self.client_list = builder.get_object('client_list') self.client_label = builder.get_object('client_label') self.client_config_label = builder.get_object('client_config_label') self.client_tree.grab_focus() selection = self.client_tree.get_selection() selection.connect('changed', self.on_client_selection_changed) # Option lists self.option_tree, self.option_list = self.connect_option_view('') self.slot_option_tree, self.slot_option_list =\ self.connect_option_view('slot_') self.core_option_tree = builder.get_object('core_option_tree_view') self.core_option_list = builder.get_object('core_option_list') self.option_tree.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.slot_option_tree.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.core_option_tree.get_selection().set_mode(gtk.SELECTION_MULTIPLE) # Option dialog self.option_name_entry = builder.get_object('option_name_entry') self.option_value_entry = builder.get_object('option_value_entry') # Core option dialog self.core_option_entry = builder.get_object('core_option_entry') # Folding power self.folding_power_label = builder.get_object('folding_power_label') self.folding_power = builder.get_object('folding_power_hscale') for i in range(len(self.folding_power_levels)): level = self.folding_power_levels[i] markup = '%s' % level self.folding_power.add_mark(i, gtk.POS_BOTTOM, markup) # User info self.donor_info = builder.get_object('donor_info') self.team_info = builder.get_object('team_info') # Client stats self.client_ppd = builder.get_object('client_ppd') # Client config self.core_priority_low = builder.get_object('core_priority_low') # Proxy self.proxy_frame = builder.get_object('proxy_frame') self.proxy_auth_frame = builder.get_object('proxy_auth_frame') self.proxy_port = builder.get_object('proxy_port_entry') # Project self.project_frame = builder.get_object('project_frame') self.project_text = builder.get_object('project_text') self.project_label = builder.get_object('project_label') # Slot lists self.slot_status_tree = builder.get_object('slot_status_tree_view') self.slot_status_tree.get_selection().set_mode(gtk.SELECTION_SINGLE) self.slot_status_list = builder.get_object('slot_status_list') self.slot_tree = builder.get_object('slot_tree_view') self.slot_list = builder.get_object('slot_list') self.slot_menu = builder.get_object('slot_menu') self.idle_slot_item = builder.get_object('idle_slot_item') view_slot_item = builder.get_object('view_slot_item') view_slot_item.get_image().set_from_pixbuf(get_viewer_icon('tiny')) # Slot dialog self.slot_type_cpu = builder.get_object('slot_type_cpu') self.slot_type_gpu = builder.get_object('slot_type_gpu') # Queue list self.queue_tree = builder.get_object('queue_tree') self.queue_list = builder.get_object('queue_list') # Info self.info = builder.get_object('info_alignment') # Log self.log_view = builder.get_object('log_text_view') self.log_view.modify_font(self.mono_font) self.log = builder.get_object('log_buffer') self.log.create_mark('end', self.log.get_end_iter()) self.log_severity = builder.get_object('log_severity') self.log_slot_enable = builder.get_object('log_slot_enable') self.log_slot = builder.get_object('log_slot') self.log_unit_enable = builder.get_object('log_unit_enable') self.log_unit = builder.get_object('log_unit') self.log_follow = builder.get_object('log_follow') # Widget maps self.client_entries = WidgetMap(self.client_dialog, '_entry') self.client_option_widgets = \ WidgetMap(self.client_config_notebook, '_option') self.client_config_tabs = WidgetMap(self.client_config_notebook, '_tab') self.slot_option_widgets = WidgetMap(self.slot_dialog, '_option') roots = [self.window, self.client_dialog] self.preference_widgets = WidgetMap(self.preferences_dialog, '_pref') widget = builder.get_object('advanced_unit_frame') self.queue_widgets = WidgetMap(widget, None, 'queue_') # Stats prefs self.donor_stats_pref = self.preference_widgets['donor_stats'] self.team_stats_pref = self.preference_widgets['team_stats'] self.donor_stats_list = builder.get_object('donor_stats_links_list') map(self.donor_stats_list.append, self.donor_stats_links) self.team_stats_list = builder.get_object('team_stats_links_list') map(self.team_stats_list.append, self.team_stats_links) # OSX integration if sys.platform == 'darwin': # Setup dock menu self.osx_menu = builder.get_object('osx_tray_menu') if self.is_old_gtk: self.osx_app.set_dock_menu(self.osx_menu) else: self.osx_create_dock_menu(self.osx_menu) # Create application menu self.osx_menubar = gtk.MenuBar() self.osx_menubar.show_all() self.osx_app.set_menu_bar(self.osx_menubar) if self.is_old_gtk: self.osx_group = self.osx_app.add_app_menu_group() self.osx_menu.foreach(self.osx_add_to_menu) else: self.osx_create_app_menu(self.osx_menu) # Hide some widgets in OSX for name in ['ui_pref_frame', 'theme_pref', 'theme_label']: widget = builder.get_object(name) widget.set_property('visible', False) if self.osx_version >= (10,7): # remove broken window resize grip self.status_bar.set_property('has-resize-grip', False) self.time_label.set_property('xpad', 6) else: self.time_label.set_property('xpad', 2) # Validators EntryValidator(self, self.client_option_widgets['user'], r'^[!-~]+$', 'User name must be a non-empty string containing only ' 'alphanumeric characters, standard punctuation and ' 'no white-space.') self.passkey_validator = \ PasswordValidator(self, builder.get_object('passkey_option'), builder.get_object('passkey_reenter'), builder.get_object('passkey_valid_image'), builder.get_object('passkey_valid_text'), r'^[0-9a-fA-F]{0,32}$', 'The passkey must be a ' '32 character hexadecimal string.') self.password_validator = \ PasswordValidator(self, builder.get_object('password_option'), builder.get_object('password_reenter'), builder.get_object('password_valid_image'), builder.get_object('password_valid_text')) self.proxy_pass_validator = \ PasswordValidator(self, builder.get_object('proxy_pass_option'), builder.get_object('proxy_pass_reenter'), builder.get_object('proxy_pass_valid_image'), builder.get_object('proxy_pass_valid_text')) # Fix client port default port = builder.get_object('port_adjustment') port.set_value(36330) # Connect signals builder.connect_signals(self) self.builder = builder = None # Discard builder self.client_dialog.client = None # Load self.preferences_load() self.load_clients() # If we don't have any clients add the default client if not len(self.clients): client = Client(self, 'local', '127.0.0.1', 36330, '') self.add_client(client) self.save_clients() # Select first client self.select_first_client() # Start client notebook deactivated self.deactivate_client() self.window.connect('notify::is-active', self.on_window_is_active) # Main loop def run(self): self.quitting = False if sys.platform != 'darwin': self.check_clients() # Slightly faster load? self.restore() self.set_update_timer_interval(100) if sys.platform == 'darwin': # reduce updates to 2Hz after 30 seconds gobject.timeout_add(30000, self.set_update_timer_interval, 500) # OSX signals self.osx_app.connect('NSApplicationDidBecomeActive', self.app_did_become_active) self.osx_app.connect('NSApplicationWillTerminate', self.app_will_terminate) self.osx_app.connect('NSApplicationBlockTermination', self.app_should_block_terminate) self.osx_app.ready() if self.osx_version >= (10,7) and self.osx_version < (10,9): self.osx_window_focus_workaround() try: # add cmd-w and cmd-m to window # cmd-w would need to be same as cancel for dialogs ag = gtk.AccelGroup() self.window_accel_group = ag key, mod = gtk.accelerator_parse("w") ag.connect_group(key, mod, gtk.ACCEL_VISIBLE, osx_accel_window_close) key, mod = gtk.accelerator_parse("m") ag.connect_group(key, mod, gtk.ACCEL_VISIBLE, osx_accel_window_minimize) self.window.add_accel_group(ag) except Exception as e: print (e) gtk.main() # Util def osx_add_to_menu(self, widget): if isinstance(widget, gtk.SeparatorMenuItem): self.osx_group = self.osx_app.add_app_menu_group() elif isinstance(widget, gtk.MenuItem): name = widget.child.get_text() def activate_item(widget, target): target.emit('activate') item = gtk.MenuItem(name) item.show() item.connect('activate', activate_item, widget) self.osx_app.add_app_menu_item(self.osx_group, item) def osx_create_app_menu(self, widgets): i = 0 for widget in widgets: if not isinstance(widget, gtk.SeparatorMenuItem): def activate_item(widget, target): target.emit('activate') label = widget.get_label() item = gtk.MenuItem(label) item.show() item.connect('activate', activate_item, widget) self.osx_app.insert_app_menu_item(widget, i) i += 1 def osx_create_dock_menu(self, widgets): menu = gtk.Menu() for widget in widgets: if isinstance(widget, gtk.SeparatorMenuItem): item = gtk.SeparatorMenuItem() else: def activate_item(widget, target): target.emit('activate') label = widget.get_label() item = gtk.MenuItem(label) item.connect('activate', activate_item, widget) menu.append(item) menu.show_all() self.osx_app.set_dock_menu(menu) # retain menu, or it won't work self.osx_dock_menu = menu def osx_window_focus_workaround(self): # osx 10.7+, part of Trac #793, not resolved by gtk 2.24.10 # only thing that works is clicking FAHControl icon in Dock # this is the equivalent # must be backgrounded so app can process reopen event # and not deadlock waiting for osascript cmd = ['/usr/bin/osascript', '-e', 'tell app "FAHControl" to reopen'] try: subprocess.Popen(cmd) except Exception as e: print (e, ':', ' '.join(cmd)) def connect_option_cell(self, name, model, col): cell = self.builder.get_object(name) cell.connect('edited', self.on_option_edit, model, col) def connect_option_view(self, prefix): tree = self.builder.get_object(prefix + 'option_tree_view') model = self.builder.get_object(prefix + 'option_list') self.connect_option_cell(prefix + 'option_name_cell', model, 0) self.connect_option_cell(prefix + 'option_value_cell', model, 1) return tree, model # Timer functions def set_update_timer_interval(self, interval = None): if self.timer_id is not None: glib.source_remove(self.timer_id) self.timer_id = None if interval and int(interval) > 0: self.timer_id = gobject.timeout_add(interval, self.on_timer) return False # stop if timer callback def check_clients(self): # Make sure there is a selected client if not len(self.selected_clients): self.select_first_client() # Update clients for client in self.clients.values(): client.update(self) # (De)activate client if self.active_client: if not self.active_client.is_updated(): self.deactivate_client() else: self.activate_client() # Try to activate # Check if active and online if self.active_client and self.active_client.is_online(): self.client_notebook.set_sensitive(True) else: self.client_notebook.set_sensitive(False) # Update status bar if self.status_clear_time and self.status_clear_time < time.time(): self.status_bar.pop(0) self.status_clear_time = None def on_timer(self): try: # Update clock now = time.time() if self.last_clock and self.last_clock != now: s = time.strftime('UTC: %Y-%m-%dT%H:%M:%SZ', time.gmtime()) self.time_label.set_text(s) # Update ppd ppd = 0 for client in self.clients.values(): ppd += client.ppd label = 'Total Estimated Points Per Day: ' if int(ppd): label += '%d' % int(ppd) else: label += 'Unknown' self.ppd_label.set_text(label) self.last_clock = now self.check_clients() self.viewer_check() if self.exit_requested.isSet(): self.quit() if self.ping.isSet(): self.ping.clear() self.restore() if 2.5 < now - self.last_db_flush: self.last_db_flush = time.time() self.db.flush_queued() except: traceback.print_exc() return True # Keep running # Actions def quit(self): if self.quitting: return self.quitting = True gtk.main_quit() self.viewer_close() self.set_update_timer_interval(0) for client in self.clients.values(): client.close() try: self.db.flush_queued() except Exception as e: print (e) sys.exit(0) # Force shutdown def set_status(self, text): self.status_bar.pop(0) self.status_bar.push(0, text) self.status_clear_time = time.time() + 10 # OSX signals def app_did_become_active(self, app): self.restore() def app_will_terminate(self, app): # Calxalot: Probably don't need both this and should_block, but I can # imagine the app quitting without asking. Note that if we reach this, # we exit after quit() and never return to run() self.quit() def app_should_block_terminate(self, app): self.quit() return False # Preference methods def load_theme(self, theme): for name, rc in self.theme_list: if theme == name: print ('Loading theme %r' % theme) settings = gtk.settings_get_default() if rc is None: settings.set_property('gtk-theme-name', self.system_theme) gtk.rc_set_default_files([]) else: settings.set_property('gtk-theme-name', theme) gtk.rc_set_default_files([rc]) gtk.rc_reparse_all_for_settings(settings, True) gtk.rc_reset_styles(settings) break def load_themes(self): paths = get_theme_dirs() unique = set() list = [] default_rc = None for path in paths: if not os.path.exists(path): continue for name in os.listdir(path): if name in unique: continue rc = path + '/' + name + '/gtk-2.0/gtkrc' if os.path.exists(rc): unique.add(name) if sys.platform == 'win32' and \ name == 'Windows-Default' and default_rc is None: default_rc = rc else: list.append([name, rc]) list.sort(key = lambda x: x[0]) return [['Default', default_rc]] + list def get_pref(self, name): widget = self.preference_widgets[name] return get_widget_str_value(widget) def get_viz_render_mode(self): value = self.get_pref('viz_render_mode') if value == 'Space Filling': return 1 if value == 'Ball And Stick': return 2 if value == 'Stick': return 3 if value == 'Advanced Space Filling': return 4 if value == 'Advanced Ball And Stick': return 5 if value == 'Advanced Stick': return 6 if value == 'Cartoon Space Filling': return 7 if value == 'Cartoon Ball And Stick': return 8 return 1 def preferences_set(self): # URLs for pref in ['donor', 'team']: entry = self.preference_widgets[pref + '_stats'] combo = self.preference_widgets[pref + '_stats_link'] button = getattr(self, pref + '_info') link = self.get_pref(pref + '_stats_link') custom_uri = self.get_pref(pref + '_stats') entry.set_text(custom_uri) if link == 'Custom': button.set_uri(custom_uri) else: model = combo.get_model() iter = model.get_iter_first() while iter is not None: if model.get_value(iter, 0) == link: combo.set_active_iter(iter) button.set_uri(model.get_value(iter, 1)) break iter = model.iter_next(iter) def preferences_load(self): # Preferences dialog for name, widget in self.preference_widgets.items(): value = None if self.db.has(name): value = self.db.get(name) if name == 'theme': self.load_theme(value) elif name == 'theme': value = 'Default' elif name == 'viz_command': value = 'FAHViewer' elif name == 'viz_fullscreen': value = 'False' elif name == 'viz_width': value = '800' elif name == 'viz_height': value = '600' elif name == 'viz_render_mode': value = 'Advanced Space Filling' elif name == 'viz_cycle_snapshots': value = 'True' elif name == 'donor_stats': value = '' elif name == 'team_stats': value = '' elif name == 'donor_stats_link': value = 'Folding@home' elif name == 'team_stats_link': value = 'Folding@home' else: raise Exception('Unknown preference widget "%s"' % name) if value is not None: set_widget_str_value(widget, value) # Update self.preferences_set() def preferences_dialog_init(self): for name, widget in self.preference_widgets.items(): if self.db.has(name): value = self.db.get(name) set_widget_str_value(widget, value) for pref in ['donor', 'team']: entry = self.preference_widgets[pref + '_stats'] combo = self.preference_widgets[pref + '_stats_link'] entry.set_sensitive(combo.get_active_text() == 'Custom') def preferences_save(self): for name, widget in self.preference_widgets.items(): value = get_widget_str_value(widget) if value is None: self.db.clear(name, False) else: self.db.set(name, value, False) self.db.commit() # Client methods def select_first_client(self): iter = self.client_list.get_iter_first() if iter: self.client_tree.get_selection().select_iter(iter) def activate_client(self): if self.active_client: return if not len(self.selected_clients): return # Check that all selected clients are active for client in self.selected_clients: if not client.is_updated(): return # Activate client(s) for client in self.selected_clients: self.active_client = client self.active_client.update_status_ui(self) break # TODO only supporting one active client right now self.last_clients_update = 0 self.update_client_status() def deactivate_client(self): if self.active_client: self.active_client.reset_status_ui(self) self.active_client = None self.client_label.set_markup('Client: inactive') self.update_client_status() def update_client_status(self): if len(self.selected_clients): client = list(self.selected_clients)[0] text = 'Client: %s' % client.name status = client.get_status() color = status_to_color(status) text += ' %s' % (color, status) if self.active_client and self.active_client.config.get_running(): text += ' Running' else: text += ' Inactive' else: text = 'Client: no client selected' self.client_label.set_markup(text) if self.client_dialog.client is not None: self.client_config_label.set_markup(text) def save_clients(self): self.db.delete('clients') for client in self.clients.values(): client.save(self.db) self.db.commit() def update_client_list(self): # update all rows, whether selected/active or not try: iter = self.client_list.get_iter_first() while iter is not None: name = self.client_list.get_value(iter, 0) client = self.clients.get(name) if client is not None: path = self.client_list.get_path(iter) row = client.get_row(self) for i in range(len(row)): self.client_list.set(iter, i, row[i]) self.client_list.row_changed(path, iter) iter = self.client_list.iter_next(iter) except Exception as e: print (e) return False # no timer repeat def resort_client_list(self): ibyname_old = {} iter = self.client_list.get_iter_first() i = 0 while iter is not None: name = self.client_list.get_value(iter, 0) ibyname_old[name] = i iter = self.client_list.iter_next(iter) i += 1 new_order = [] for client in self.sorted_clients(): name = client.name i = ibyname_old.get(name) if i is None: print ('unable to resort client list: unknown name %s' % name) return new_order.append(i) self.client_list.reorder(new_order) return False # don't repeat if timer callback def sorted_clients(self, unsorted_clients = None): if unsorted_clients is None: unsorted_clients = self.clients.values() # pre-sort by client.name clients = sorted(unsorted_clients, key=lambda c: c.name) # sort local clients first group0 = [] # client "local" (should only be one, currently) group1 = [] # other is_local clients (should not be any, currently) group2 = [] # localhost clients starting with "local" group3 = [] # other localhost clients group4 = [] # remote clients (and any local referenced by host name) for client in clients: is_local = client.is_local() is_local_addr = client.address in ['localhost','127.0.0.1'] if is_local and client.name == 'local': group0.append(client) elif is_local: group1.append(client) elif is_local_addr and client.name.startswith('local'): group2.append(client) elif is_local_addr: group3.append(client) else: group4.append(client) return group0 + group1 + group2 + group3 + group4 def load_clients(self): clients = [] for row in self.db.select('clients', orderby = 'name'): client = Client(self, row['name'], row['address'], int(row['port']), row['password']) clients.append(client) for client in self.sorted_clients(clients): self.add_client(client) def clear_clients(self): for client in self.clients: client.close() self.clients.clear() self.clientsByName.clear() self.client_list.clear() def save_client_config(self, client): try: options, slots = client.config.get_changes(self) if not (options or slots != ([], [], [])): return True # Validate passkey if not self.passkey_validator.is_good(): raise Exception('Passkey is invalid') # Validate password if not self.password_validator.is_good(): raise Exception('Client password is invalid') # Validate proxy password if not self.proxy_pass_validator.is_good(): raise Exception('Proxy password is invalid') self.deactivate_client() self.set_status('Saving...') for client in self.selected_clients: # TODO check returned error count client.save_config(options, slots) client.update(self) # Slightly faster save # Update password if changed on client if 'password' in options: client.set_password(options['password']) if 'password!' in options: client.set_password('') return True except Exception, msg: self.set_status('Save Failed') self.error(msg) return False def check_duplicate_client_name(self, name): if name in self.clients: self.error('Client with name "%s" is already in client list' % name) return True return False def check_duplicate_client_address(self, address): if address in self.clientsByAddress: self.error('Client with address "%s" is already in client list' % address) return True return False def update_client(self, client, name, address, port, password): reload = False old_name = client.name # Check for duplicates if client.name != name: if self.check_duplicate_client_name(name): return False new_address = Client.make_address(address, port) if client.get_address() != new_address: if self.check_duplicate_client_address(new_address): return False # Update if client.name != name: del self.clients[client.name] client.name = name self.clients[name] = client if client.get_address() != new_address: del self.clientsByAddress[client.get_address()] client.set_address(address, port) self.clientsByAddress[new_address] = client reload = True if client.get_password() != password: client.set_password(password) reload = True # Update client row row = client.get_row(self) # Find client in client_list iter = self.client_list.get_iter_first() while iter is not None: if old_name == self.client_list.get_value(iter, 0): path = self.client_list.get_path(iter) for i in range(len(row)): self.client_list.set(iter, i, row[i]) self.client_list.row_changed(path, iter) break iter = self.client_list.iter_next(iter) # Reload if reload: client.reconnect() self.deactivate_client() return True def add_client(self, client): name = client.name address = client.get_address() # Check for duplicates if self.check_duplicate_client_name(name): return False if self.check_duplicate_client_address(address): return False # Add it self.clients[name] = client self.clientsByAddress[address] = client self.client_list.append(client.get_row(self)) return True def remove_client(self, client): client.close() del self.clients[client.name] del self.clientsByAddress[client.get_address()] if client is self.active_client: self.active_client = None def remove_client_path(self, path, iter): name = self.client_list.get(iter, 0)[0] self.remove_client(self.clients[name]) def get_selected_clients(self): selection = get_tree_selection(self.client_tree) names = map(lambda item: self.client_list.get(item[1], 0)[0], selection) return set(map(self.clients.get, names)) def edit_client(self, client): self.client_dialog.client = client self.update_client_status() client.load_dialog(self) if self.active_client and self.active_client.is_online(): for name, widget in self.client_config_tabs.items(): widget.show() self.client_dialog.config_hidden = False else: for name, widget in self.client_config_tabs.items(): if name != 'connection': widget.hide() self.client_dialog.config_hidden = True self.open_dialog(self.client_dialog) def open_dialog(self, dialog): # Hack to make WrapLabel work dims = dialog.get_size() dialog.resize(dims[0] + 1, dims[1] + 1) dialog.present() dialog.resize(*dims) # Slot methods def get_selected_slot_ids(self): selection = get_tree_selection(self.slot_status_tree) ids = map(lambda x: int(self.slot_status_list.get(x[1], 0)[0]), selection) return ids # Window methods def get_visible_dialogs(self): dialogs = [] for dialog in self.dialogs: if dialog.flags() & gtk.MAPPED: dialogs.append(dialog) return dialogs def hide_all_windows(self): self.restore_dialogs = self.get_visible_dialogs() for dialog in self.restore_dialogs: dialog.hide() self.window.hide() self.window_visible = False def restore(self): self.window.present() self.window.deiconify() self.window_visible = True if sys.platform == 'darwin': # restore osx minimized dialogs for dialog in self.get_visible_dialogs(): dialog.present() # Restore dialogs for dialog in self.restore_dialogs: dialog.present() self.restore_dialogs = [] # Messages def close_error_dialog(self, dialog, id = None, data = None): dialog.destroy() self.error_dialog = None def error(self, message, buttons = gtk.BUTTONS_OK, on_response = None, on_response_data = None): message = str(message) # log to terminal window if sys.exc_info()[2]: traceback.print_exc() print ('ERROR: %s' % message) # Don't open more than one if self.error_dialog is not None: return False if sys.exc_info()[1]: message += '\n%s' % sys.exc_info()[1] # create an error message dialog and display modally to the user dialog = \ gtk.MessageDialog(None, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, buttons, message) dialog.connect('close', self.close_error_dialog) if on_response is not None: dialog.connect('response', on_response, on_response_data) dialog.connect('response', self.close_error_dialog) dialog.set_transient_for(self.window) self.error_dialog = dialog dialog.show() return True # Property signals def store_property(self, widget, property, name): self.db.set(name, widget.get_property(property.name), queue = True) def store_dimensions(self, widget, event, name): x, y, width, height = widget.get_allocation() if 0 <= width and 0 <= height: self.db.set(name + '_width', width, queue = True); self.db.set(name + '_height', height, queue = True); # Action signals def on_quit(self, widget, data = None): self.quit() def on_preferences(self, widget, data = None): # OSX crashes with out this, but it's a good idea anyway if not self.window_visible: self.restore() if self.get_visible_dialogs(): return self.preferences_dialog_init() self.preferences_dialog.present() def viewer_check(self): if self.viewer is not None and self.viewer.poll() is not None: if self.viewer.returncode and sys.platform == 'darwin': self.error('Failed to launch viewer:\n\n' + self.viewer.stderr.read()) self.viewer = None # Viewer exited def viewer_close(self): if self.viewer is not None: try: self.viewer.kill() self.viewer.wait() # Note: This could cause a hang kill() fails except: pass self.viewer = None def on_viewer(self, widget, data = None): self.viewer_close() # Get preferences command = self.get_pref('viz_command') fullscreen = parse_bool(self.get_pref('viz_fullscreen')) width = self.get_pref('viz_width') height = self.get_pref('viz_height') mode = self.get_viz_render_mode() cycle_snapshots = self.get_pref('viz_cycle_snapshots') # Create command line cmd = shlex.split(command) if not (len(cmd) and len(cmd[0])): cmd = ['FAHViewer'] if sys.platform == 'darwin': cmd = ['/usr/bin/open', '-a', cmd[0], '--args'] + cmd[1:] if fullscreen: cmd.append('--fullscreen') cmd.append('--width=' + width) cmd.append('--height=' + height) cmd.append('--mode=%d' % mode) cmd.append('--cycle-snapshots=' + cycle_snapshots) if self.active_client and self.active_client.is_online(): address = self.active_client.address port = self.active_client.port cmd.append('--connect=%s:%d' % (address, port)) password = self.active_client.password if password: cmd.append('--password="%s"' % password) slot = self.active_client.get_selected_slot(self) if slot is not None: cmd.append('--slot=%d' % slot.id) debug = True if debug: print (cmd) try: if sys.platform == 'darwin': self.viewer = subprocess.Popen(cmd, cwd = get_home_dir(), bufsize = 4096, stderr=subprocess.PIPE) else: self.viewer = subprocess.Popen(cmd, cwd = get_home_dir()) except Exception: self.error('Failed to launch viewer with command:\n\n' + ' '.join(cmd)) def on_about(self, widget, data = None): # OSX crashes with out this, but it's a good idea anyway if not self.window_visible: self.restore() if self.get_visible_dialogs(): return False self.open_dialog(self.about_dialog) def on_about_close(self, widget, data = None): self.about_dialog.hide() return True # Cancel event # Window signals def on_window_destroy(self, widget, data = None): if sys.platform == 'darwin': self.hide_all_windows() return True # prevent destroy self.quit() def on_window_delete(self, widget, event, data = None): return self.on_window_destroy(widget) def on_window_is_active(self, window, *args): try: if window.is_active(): self.update_client_list() except Exception as e: print (e) # Preferences signals def on_preferences_ok(self, widget, data = None): self.preferences_set() self.preferences_save() self.preferences_dialog.hide() def on_preferences_cancel(self, widget, data = None): # Reset theme if self.db.has('theme'): current_theme = self.db.get('theme') else: current_theme = 'Default' if self.get_pref('theme') != current_theme: self.load_theme(current_theme) # FIXME workaround for defect: get_pref takes from dialog widgets self.preferences_dialog_init() self.preferences_dialog.hide() return True # Cancel event def on_theme_pref_changed(self, widget, data = None): iter = widget.get_active_iter() theme = widget.get_model().get_value(iter, 0) self.load_theme(theme) # Proxy signals def on_proxy_enable_toggled(self, widget, data = None): self.proxy_frame.set_sensitive(widget.get_active()) self.proxy_auth_frame.set_sensitive(widget.get_active()) # Client list signals def on_client_add_button_clicked(self, widget, data = None): if self.get_visible_dialogs(): return # Make client name for i in xrange(sys.maxint): name = 'client%d' % i if not name in self.clients: break self.client_entries['name'].set_text(name) self.client_dialog.client = None text = 'Configure New Client Connection' self.client_config_label.set_markup(text) # Reset dialog self.client_entries['name'].set_sensitive(True) self.client_entries['address'].set_sensitive(True) for name, widget in self.client_config_tabs.items(): if name != 'connection': widget.hide() self.open_dialog(self.client_dialog) def on_client_remove_button_clicked(self, widget, data = None): remove_tree_selection(self.client_tree, self.remove_client_path) self.save_clients() def on_configure(self, widget, data = None): if self.get_visible_dialogs(): return selection = get_tree_selection(self.client_tree) if not len(selection): return name = self.client_list.get(selection[0][1], 0)[0] client = self.clients[name] self.edit_client(client) def on_client_tree_view_row_activated(self, widget, path, col, data = None): if self.get_visible_dialogs(): return iter = self.client_list.get_iter(path) name = self.client_list.get(iter, 0)[0] client = self.clients[name] self.edit_client(client) def on_client_selection_changed(self, widget, data = None): self.deactivate_client() self.selected_clients = self.get_selected_clients() # Modify selection for client in self.clients.values(): client.set_selected(client in self.selected_clients) self.update_client_status() if len(self.clients): self.activate_client() self.update_client_list() gobject.timeout_add(5000, self.update_client_list) # temporarily increase update rate for faster switch if sys.platform == 'darwin': self.set_update_timer_interval(100) gobject.timeout_add(10000, self.set_update_timer_interval, 500) # Client options list signals def on_client_options_add_button_clicked(self, widget, data = None): self.option_present(self.option_list, self.client_dialog) def on_client_options_remove_button_clicked(self, widget, data = None): remove_tree_selection(self.option_tree) # Core options list signals def on_core_options_add_button_clicked(self, widget, data = None): self.core_option_entry.set_text('') self.core_options_dialog.set_transient_for(self.client_dialog) self.core_options_dialog.present() def on_core_options_remove_button_clicked(self, widget, data = None): remove_tree_selection(self.core_option_tree) def on_core_options_ok(self, widget, data = None): option = self.core_option_entry.get_text().strip() if not option: self.error('Invalid option') return self.core_option_list.append([option]) self.core_options_dialog.hide() def on_core_options_cancel(self, widget, data = None): self.core_options_dialog.hide() return True # Cancel event # Client dialog signals def on_client_ok(self, widget, data = None): name = self.client_entries['name'].get_text() address = self.client_entries['address'].get_text() port = self.client_entries['port'].get_text() password = self.client_entries['password'].get_text() if not name: self.error('Invalid name') return if not address: self.error('Invalid address') return if not port: self.error('Invalid port') return port = int(port) if self.client_dialog.client: # Existing client client = self.client_dialog.client config_hidden = self.client_dialog.config_hidden # Save client options if config_hidden or self.save_client_config(client): # Save client connection if self.update_client(client, name, address, port, password): self.save_clients() self.client_dialog.hide() self.resort_client_list() else: # New client client = Client(self, name, address, port, password) if self.add_client(client): self.save_clients() self.client_dialog.hide() self.resort_client_list() def on_client_cancel(self, widget, data = None): self.client_dialog.hide() return True # Cancel event # Folding power signals def on_fold_button_clicked(self, widget, data = None): self.active_client.unpause() def on_pause_button_clicked(self, widget, data = None): self.active_client.pause() def on_finish_button_clicked(self, widget, data = None): self.active_client.finish() def on_folding_power_change_value(self, widget, scroll, value, data = None): # Clamp slider to integer increments value = int(round(value)) if self.folding_power.get_value() != value: self.folding_power.set_value(value) return True def on_folding_power_value_changed(self, widget, data = None): if not self.folding_power_changing and self.active_client: power = self.folding_power_levels[int(widget.get_value())] self.active_client.set_power(power) def on_folding_power_button_press(self, widget, data = None): self.folding_power_changing = True def on_folding_power_button_release(self, widget, data = None): if self.active_client: power = self.folding_power_levels[int(widget.get_value())] self.active_client.set_power(power) self.folding_power_changing = False # User status signals def on_team_stats_link_changed(self, widget, data = None): iter = widget.get_active_iter() model = widget.get_model() text = model.get_value(iter, 0) self.team_stats_pref.set_sensitive(text == 'Custom') def on_donor_stats_link_changed(self, widget, data = None): iter = widget.get_active_iter() model = widget.get_model() text = model.get_value(iter, 0) self.donor_stats_pref.set_sensitive(text == 'Custom') # Queue tree signals def on_queue_tree_cursor_changed(self, widget, data = None): if self.active_client: self.active_client.config.select_queue_slot(self) # Slot list signals def on_slot_add_button_clicked(self, widget, data = None): SlotConfig.clear_dialog(self) self.slot_dialog.slot_iter = None self.open_dialog(self.slot_dialog) def on_slot_remove_button_clicked(self, widget, data = None): remove_tree_selection(self.slot_tree) def on_slot_edit_button_clicked(self, widget, data = None): selection = get_tree_selection(self.slot_tree) if selection: iter = selection[0][1] slot = self.slot_list.get(iter, 2)[0].slot slot.load_dialog(self) self.slot_dialog.slot_iter = iter self.open_dialog(self.slot_dialog) def on_slot_tree_view_row_activated(self, widget, path, col, data = None): iter = self.slot_list.get_iter(path) slot = self.slot_list.get(iter, 2)[0].slot slot.load_dialog(self) self.slot_dialog.slot_iter = iter self.open_dialog(self.slot_dialog) # Slot dialog signals def on_slot_ok(self, widget, data = None): if self.slot_dialog.slot_iter is None: slot = SlotConfig() slot.save_dialog(self) slot.add_to_ui(self) # Add to list else: iter = self.slot_dialog.slot_iter id, type, wrapper = self.slot_list.get(iter, 0, 1, 2) slot = wrapper.slot slot.save_dialog(self) if slot.type != type: self.slot_list.set(iter, 1, slot.type) self.slot_dialog.hide() def on_slot_cancel(self, widget, data = None): self.slot_dialog.hide() return True # Cancel event # Slot options list signals def on_slot_options_add_button_clicked(self, widget, data = None): self.option_present(self.slot_option_list, self.slot_dialog) def on_slot_options_remove_button_clicked(self, widget, data = None): remove_tree_selection(self.slot_option_tree) # Options edit def check_option_name(self, name): if not re.match(r'^[a-zA-Z][\w-]*$', name): self.error('Invalid option name.') return False return True def on_option_edit(self, cell, path, text, model, column): if column == 0 and not self.check_option_name(text): return if model[path][column] != text: model[path][column] = text def on_core_option_cell_edited(self, cell, path, text, data = None): if not text: self.error('Invalid option') return if self.core_option_list[path][0] != text: self.core_option_list[path][0] = text # Options dialog signals def option_present(self, model, parent): self.option_name_entry.set_text('') self.option_value_entry.set_text('') self.options_dialog.option_model = model self.options_dialog.set_transient_for(parent) self.options_dialog.present() def on_options_ok(self, widget, data = None): name = self.option_name_entry.get_text().strip() value = self.option_value_entry.get_text() if not self.check_option_name(name): return self.options_dialog.option_model.append([name, value]) self.options_dialog.hide() def on_options_cancel(self, widget, data = None): self.options_dialog.hide() return True # Cancel event # Configure dialog signals def on_configure_ok(self, widget, data = None): self.configure_dialog.hide() if self.active_client: # Select identity tab self.client_config_notebook.set_current_page(1) self.edit_client(self.active_client) return True # Cancel event def on_configure_cancel(self, widget, data = None): self.configure_dialog.hide() if self.active_client: # Fold anonymously self.active_client.conn.queue_command('save') return True # Cancel event # Slot status tree signals def on_slot_status_tree_cursor_changed(self, widget, data = None): if self.active_client: self.active_client.config.select_slot(self) def on_slot_status_tree_view_button_release_event(self, widget, event, data = None): if event.button != 3: return idle = self.active_client.get_selected_slot(self).idle self.idle_slot_item.set_active(idle) self.slot_menu.popup(None, None, None, button = event.button, activate_time = event.time, data = data) def on_unpause_slot_item_activate(self, widget, data = None): for id in self.get_selected_slot_ids(): self.active_client.unpause(id) def on_pause_slot_item_activate(self, widget, data = None): for id in self.get_selected_slot_ids(): self.active_client.pause(id) def on_idle_slot_item_toggled(self, widget, data = None): for id in self.get_selected_slot_ids(): if widget.get_active(): self.active_client.on_idle(id) else: self.active_client.always_on(id) def on_finish_slot_item_activate(self, widget, data = None): for id in self.get_selected_slot_ids(): self.active_client.finish(id) # Log signals def on_download_log_clicked(self, widget, data = None): self.active_client.refresh_log() self.log.set_text('') def on_copy_log_clicked(self, widget, data = None): log = self.log text = log.get_text(log.get_start_iter(), log.get_end_iter()) gtk.Clipboard().set_text(text) def on_clear_log_clicked(self, widget, data = None): if self.active_client: self.active_client.config.log_clear(self) def on_update_log(self, widget, data = None): if self.active_client: self.active_client.config.update_log(self) # Scale value formatting def on_cpu_usage_scale_format_value(self, widget, value, data = None): return '%d%%' % value def on_checkpoint_scale_format_value(self, widget, value, data = None): return '%d min.' % value def on_uri_hook(self, widget, url, data = None): keys = {'donor': urllib.quote(self.donor_info.get_label()), 'team': urllib.quote(self.team_info.get_label())} webbrowser.open(url % keys)