674 lines
21 KiB
Python
674 lines
21 KiB
Python
'''
|
|
Folding@Home Client Control (FAHControl)
|
|
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 <http://www.gnu.org/licenses/>.
|
|
'''
|
|
|
|
import sys
|
|
import gtk
|
|
import traceback
|
|
import re
|
|
|
|
from fah.util import parse_bool
|
|
from fah.util import status_to_color
|
|
from fah.util import get_span_markup
|
|
from fah.util import get_widget_str_value
|
|
from fah.util import set_widget_str_value
|
|
from fah import SlotConfig
|
|
|
|
|
|
def get_option_mods(old_options, new_options):
|
|
changes = {}
|
|
|
|
# Deleted
|
|
for name in old_options:
|
|
if name not in new_options:
|
|
changes[name + '!'] = None
|
|
|
|
# Added and modified
|
|
for name, value in new_options.items():
|
|
if name not in old_options or value != old_options[name]:
|
|
changes[name] = value
|
|
|
|
return changes
|
|
|
|
def get_buffer_text(buffer):
|
|
return buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter())
|
|
|
|
|
|
def get_model_column(model, iter, column):
|
|
if iter is not None: return model.get_value(iter, column)
|
|
|
|
|
|
def get_selected_tree_column(tree, column):
|
|
selection = tree.get_selection()
|
|
model = tree.get_model()
|
|
if selection is not None:
|
|
return get_model_column(model, selection.get_selected()[1], column)
|
|
|
|
|
|
def get_active_combo_column(combo, column):
|
|
return get_model_column(combo.get_model(), combo.get_active_iter(), column)
|
|
|
|
|
|
class ClientConfig:
|
|
queue_cols = ('id state statecolor percentdone percent').split()
|
|
|
|
def __init__(self):
|
|
self.last_updated = 0
|
|
self.queue = []
|
|
self.queue_map = {}
|
|
self.slots = []
|
|
self.options = {}
|
|
self.core_options = {}
|
|
self.info = []
|
|
self.log = []
|
|
self.log_append_count = 0
|
|
self.tooltip = ''
|
|
self.last_log_filter = ''
|
|
self.log_filter_re = None
|
|
self.updating = False
|
|
|
|
|
|
def get(self, name):
|
|
if name in self.options: return self.options[name]
|
|
def set(self, name, value): self.options[name] = value
|
|
def have(self, name):
|
|
return name in self.options and self.options[name] is not None
|
|
|
|
|
|
def get_prcg(self, row):
|
|
return '%s (%s, %s, %s)' % (
|
|
row['project'], row['run'], row['clone'], row['gen'])
|
|
|
|
|
|
def update_power(self, app):
|
|
power = self.get('power').lower()
|
|
for i in range(len(app.folding_power_levels)):
|
|
if power == app.folding_power_levels[i].lower():
|
|
app.folding_power.set_value(i)
|
|
|
|
|
|
def update_ppd(self, app, ppd):
|
|
if ppd: s = '%d' % int(ppd)
|
|
else: s = 'Unknown'
|
|
app.client_ppd.set_text(s)
|
|
|
|
|
|
def update_queue(self, queue):
|
|
self.queue = queue
|
|
self.queue_map = {}
|
|
for values in self.queue:
|
|
self.queue_map[values['id']] = values
|
|
|
|
|
|
def update_user_info(self, app):
|
|
# User
|
|
user = self.options['user']
|
|
app.donor_info.set_label(user)
|
|
|
|
# Team
|
|
team = self.options['team']
|
|
app.team_info.set_label(team)
|
|
|
|
|
|
def reset_user_info(self, app):
|
|
app.donor_info.set_label('')
|
|
app.team_info.set_label('')
|
|
|
|
|
|
def get_selected_queue_entry(self, app):
|
|
return get_selected_tree_column(app.queue_tree, 1)
|
|
|
|
|
|
def get_selected_slot(self, app):
|
|
id = get_selected_tree_column(app.slot_status_tree, 0)
|
|
if id is not None:
|
|
id = int(id)
|
|
for slot in self.slots:
|
|
if slot.id == id: return slot
|
|
|
|
def update_queue_ui(self, app):
|
|
if not self.queue:
|
|
app.queue_list.clear()
|
|
return
|
|
|
|
# Save selections
|
|
selected = self.get_selected_queue_entry(app)
|
|
selected_row = None
|
|
log_filter_selected = get_active_combo_column(app.log_unit, 1)
|
|
log_filter_row = None
|
|
|
|
# Clear queue wo/ updating log filter
|
|
self.updating = True
|
|
try:
|
|
app.queue_list.clear()
|
|
finally:
|
|
self.updating = False
|
|
|
|
# Reload queue list
|
|
for values in sorted(self.queue, lambda x, y: cmp(x['id'], y['id'])):
|
|
unit_id = values['unit']
|
|
queue_id = values['id']
|
|
status = values['state'].title()
|
|
color = status_to_color(status)
|
|
status = get_span_markup(status, color)
|
|
progress = values['percentdone']
|
|
percent = float(progress[:-1])
|
|
eta = values['eta']
|
|
if eta == '0.00 secs': eta = 'Unknown'
|
|
credit = values['creditestimate']
|
|
if float(credit) == 0: credit = 'Unknown'
|
|
|
|
prcg = self.get_prcg(values)
|
|
iter = app.queue_list.append([unit_id, queue_id, status, color,
|
|
progress, percent, eta, credit, prcg])
|
|
|
|
if queue_id == selected: selected_row = iter
|
|
if queue_id == log_filter_selected: log_filter_row = iter
|
|
|
|
# Select the first item if nothing is selected
|
|
if selected_row is None: selected_row = app.queue_list.get_iter_first()
|
|
if log_filter_row is None:
|
|
log_filter_row = app.queue_list.get_iter_first()
|
|
|
|
# Restore selections
|
|
app.queue_tree.get_selection().select_iter(selected_row)
|
|
app.log_unit.set_active_iter(log_filter_row)
|
|
|
|
|
|
def update_work_unit_info(self, app):
|
|
if not self.queue:
|
|
self.reset_work_unit_info(app)
|
|
return
|
|
|
|
# Get selected queue entry
|
|
selected = self.get_selected_queue_entry(app)
|
|
if selected is None: return
|
|
entry = self.queue_map[selected]
|
|
|
|
# Load info
|
|
for name, value in entry.items():
|
|
if name in app.queue_widgets:
|
|
if (name in ['basecredit', 'creditestimate', 'ppd'] and \
|
|
float(value) == 0) or value == '<invalid>' or \
|
|
value == '0.00 secs': value = 'Unknown'
|
|
|
|
widget = app.queue_widgets[name]
|
|
set_widget_str_value(widget, value)
|
|
|
|
# Status
|
|
status = entry['state'].title()
|
|
color = status_to_color(status)
|
|
status = get_span_markup(status, color)
|
|
widget = app.queue_widgets['state']
|
|
widget.set_markup(status)
|
|
|
|
# PRCG
|
|
prcg = '%s (%s, %s, %s)' % (
|
|
entry['project'], entry['run'], entry['clone'], entry['gen'])
|
|
set_widget_str_value(app.queue_widgets['prcg'], prcg)
|
|
|
|
# Links
|
|
for name in ['cs', 'ws', 'project']:
|
|
widget = app.queue_widgets[name]
|
|
value = str(entry[name])
|
|
|
|
if name in ['cs', 'ws']:
|
|
uri = 'http://fah-web.stanford.edu/logs/%s.log.html' % value
|
|
else:
|
|
uri = 'http://fah-web.stanford.edu/cgi-bin/fahproject.' \
|
|
'overusingIPswillbebanned?p=' + value
|
|
|
|
widget.set_uri(uri)
|
|
|
|
|
|
def select_slot(self, app):
|
|
# Get selected slot
|
|
slot = self.get_selected_slot(app)
|
|
if slot is None: return
|
|
|
|
# Get associated queue ID
|
|
first_id = None
|
|
first_running_id = None
|
|
for entry in self.queue:
|
|
if int(entry['slot']) == slot.id:
|
|
if first_id is None: first_id = entry['unit']
|
|
if entry['state'].upper() in ['RUNNING', 'FINISHING'] and \
|
|
first_running_id is None:
|
|
first_running_id = entry['unit']
|
|
|
|
if first_running_id is not None: unit_id = first_running_id
|
|
else: unit_id = first_id
|
|
|
|
if unit_id is not None:
|
|
# Find unit_id in the queue list entry and select row
|
|
list = app.queue_list
|
|
iter = list.get_iter_first()
|
|
while iter is not None:
|
|
if list.get_value(iter, 0) == unit_id:
|
|
app.queue_tree.get_selection().select_iter(iter)
|
|
break
|
|
iter = list.iter_next(iter)
|
|
|
|
# Update the UI
|
|
self.update_work_unit_info(app)
|
|
|
|
|
|
def select_queue_slot(self, app):
|
|
# Get unit ID of selected queue entry
|
|
selected = self.get_selected_queue_entry(app)
|
|
if selected is None: return
|
|
|
|
# Get associated slot ID
|
|
entry = self.queue_map[selected]
|
|
slot = int(entry['slot'])
|
|
|
|
# Find and select the slot
|
|
list = app.slot_status_list
|
|
iter = list.get_iter_first()
|
|
while iter is not None:
|
|
if int(list.get_value(iter, 0)) == slot:
|
|
app.slot_status_tree.get_selection().select_iter(iter)
|
|
break
|
|
iter = list.iter_next(iter)
|
|
|
|
# Update the UI
|
|
self.update_work_unit_info(app)
|
|
|
|
|
|
def reset_work_unit_info(self, app):
|
|
for widget in app.queue_widgets.values():
|
|
set_widget_str_value(widget, None)
|
|
|
|
|
|
def update_info(self, app):
|
|
port = app.info
|
|
|
|
# Clear
|
|
for child in port.get_children(): port.remove(child)
|
|
|
|
# Alignment
|
|
align = gtk.Alignment(0, 0, 1, 1)
|
|
align.set_padding(4, 4, 4, 4)
|
|
port.add(align)
|
|
|
|
# Vertical box
|
|
vbox = gtk.VBox()
|
|
align.add(vbox)
|
|
|
|
for category in self.info:
|
|
name = category[0]
|
|
category = category[1:]
|
|
|
|
# Frame
|
|
frame = gtk.Frame('<b>%s</b>' % name)
|
|
frame.set_shadow_type(gtk.SHADOW_ETCHED_IN)
|
|
frame.get_label_widget().set_use_markup(True)
|
|
vbox.pack_start(frame, False)
|
|
|
|
# Alignment
|
|
align = gtk.Alignment(0, 0, 1, 1)
|
|
align.set_padding(0, 0, 12, 0)
|
|
frame.add(align)
|
|
|
|
# Table
|
|
table = gtk.Table(len(category), 2)
|
|
table.set_col_spacing(0, 5)
|
|
align.add(table)
|
|
|
|
row = 0
|
|
for name, value in category:
|
|
# Name
|
|
label = gtk.Label('<b>%s</b>' % name)
|
|
label.set_use_markup(True)
|
|
label.set_alignment(1, 0.5)
|
|
table.attach(label, 0, 1, row, row + 1, gtk.FILL, gtk.FILL)
|
|
|
|
# Value
|
|
if value.startswith('http://'):
|
|
label = gtk.LinkButton(value, value)
|
|
label.set_relief(gtk.RELIEF_NONE)
|
|
label.set_property('can-focus', False)
|
|
|
|
else: label = gtk.Label(value)
|
|
|
|
label.set_alignment(0, 0.5)
|
|
label.modify_font(app.mono_font)
|
|
table.attach(label, 1, 2, row, row + 1, yoptions = gtk.FILL)
|
|
|
|
row += 1
|
|
|
|
port.realize()
|
|
port.show_all()
|
|
|
|
|
|
def update_options(self, app):
|
|
used = set()
|
|
|
|
for name, widget in app.client_option_widgets.items():
|
|
name = name.replace('_', '-')
|
|
used.add(name)
|
|
|
|
try:
|
|
set_widget_str_value(widget, self.options[name])
|
|
|
|
except: # Don't let one bad widget kill everything
|
|
print 'WARNING: failed to set widget "%s"' % name
|
|
|
|
# Setup passkey and password entries
|
|
app.passkey_validator.set_good()
|
|
app.password_validator.set_good()
|
|
app.proxy_pass_validator.set_good()
|
|
|
|
# Set folding power
|
|
if 'power' in self.options:
|
|
used.add('power')
|
|
self.update_power(app)
|
|
|
|
# Set proxy options
|
|
if 'proxy-enable' in self.options:
|
|
proxy_enable = parse_bool(self.get('proxy-enable'))
|
|
app.proxy_frame.set_sensitive(proxy_enable)
|
|
app.proxy_auth_frame.set_sensitive(proxy_enable)
|
|
|
|
if self.have('proxy'):
|
|
proxy = self.get('proxy')
|
|
if ':' in proxy: proxy_addr, proxy_port = proxy.split(':', 1)
|
|
else: proxy_addr, proxy_port = proxy, '8080'
|
|
set_widget_str_value(app.client_option_widgets['proxy'], proxy_addr)
|
|
set_widget_str_value(app.proxy_port, proxy_port)
|
|
|
|
# Set core priority radio button
|
|
core_idle = not self.have('core-priority') or \
|
|
self.get('core-priority') == 'idle'
|
|
app.client_option_widgets['core_priority'].set_active(core_idle)
|
|
app.core_priority_low.set_active(not core_idle)
|
|
|
|
# Extra core options
|
|
app.core_option_list.clear()
|
|
if self.have('extra-core-args'):
|
|
used.add('extra-core-args')
|
|
|
|
args = self.get('extra-core-args').split()
|
|
for arg in args: app.core_option_list.append([arg])
|
|
|
|
# Remaining options
|
|
app.option_list.clear()
|
|
for name, value in self.options.items():
|
|
if name not in used:
|
|
app.option_list.append([name, value])
|
|
|
|
|
|
def update_status_slots(self, app):
|
|
# Save selection
|
|
selected = get_selected_tree_column(app.slot_status_tree, 0)
|
|
if selected is not None: selected = selected
|
|
selected_row = None
|
|
log_filter_selected = get_active_combo_column(app.log_slot, 0)
|
|
log_filter_row = None
|
|
|
|
# Clear list wo/ updating log filter
|
|
self.updating = True
|
|
try:
|
|
app.slot_status_list.clear()
|
|
finally:
|
|
self.updating = False
|
|
|
|
# Reload list
|
|
for slot in self.slots:
|
|
id = '%02d' % slot.id
|
|
status = slot.status.title()
|
|
color = status_to_color(status)
|
|
if status == 'Paused' and slot.reason:
|
|
status += ':' + slot.reason
|
|
status = get_span_markup(status, color)
|
|
description = slot.description.replace('"', '')
|
|
iter = app.slot_status_list.append((id, status, color, description))
|
|
|
|
if id == selected: selected_row = iter
|
|
if id == log_filter_selected: log_filter_row = iter
|
|
|
|
# Selected the first item if nothing is selected
|
|
if selected_row is None:
|
|
selected_row = app.slot_status_list.get_iter_first()
|
|
if log_filter_row is None:
|
|
log_filter_row = app.slot_status_list.get_iter_first()
|
|
|
|
# Restore selections
|
|
if selected_row is not None:
|
|
app.slot_status_tree.get_selection().select_iter(selected_row)
|
|
if log_filter_row is not None:
|
|
app.log_slot.set_active_iter(log_filter_row)
|
|
|
|
|
|
def update_slots_ui(self, app):
|
|
app.slot_list.clear()
|
|
for slot in self.slots:
|
|
slot.add_to_ui(app)
|
|
|
|
|
|
def scroll_log_to_end(self, app):
|
|
if not app.log_follow.get_active(): return
|
|
mark = app.log.get_mark('end')
|
|
app.log.move_mark(mark, app.log.get_end_iter())
|
|
app.log_view.scroll_mark_onscreen(mark)
|
|
|
|
|
|
def log_clear(self, app):
|
|
app.log.set_text('')
|
|
self.log = []
|
|
|
|
|
|
def log_filter_str(self, app):
|
|
f = []
|
|
|
|
# Severity
|
|
if app.log_severity.get_active():
|
|
f.append(r'((WARNING)|(W )|(ERROR)|(E ))')
|
|
|
|
# Unit
|
|
if app.log_unit_enable.get_active():
|
|
id = get_active_combo_column(app.log_unit, 1)
|
|
f.append(r'WU%s' % id)
|
|
|
|
# Slot
|
|
if app.log_slot_enable.get_active():
|
|
id = get_active_combo_column(app.log_slot, 0)
|
|
f.append(r'FS%s' % id)
|
|
|
|
if len(f):
|
|
f = map(lambda x: '.*(^|:)%s' % x, f)
|
|
return '(^\*)|(%s):' % ''.join(f)
|
|
|
|
return None
|
|
|
|
|
|
def log_filter(self, line):
|
|
return self.log_filter_re is None or \
|
|
self.log_filter_re.match(line) is not None
|
|
|
|
|
|
def log_add_lines(self, app, lines):
|
|
filtered = filter(self.log_filter, lines)
|
|
|
|
if len(filtered):
|
|
text = '\n'.join(filtered)
|
|
app.log.insert(app.log.get_end_iter(), text + '\n')
|
|
self.scroll_log_to_end(app)
|
|
|
|
|
|
def log_add(self, app, text):
|
|
# TODO deal with split lines
|
|
lines = []
|
|
for line in text.split('\n'):
|
|
if not line: continue
|
|
lines.append(line)
|
|
self.log.append(line)
|
|
|
|
self.log_add_lines(app, lines)
|
|
|
|
|
|
def update_log(self, app):
|
|
if self.updating: return # Don't refilter during updates
|
|
|
|
# Check if filter has changed
|
|
log_filter = self.log_filter_str(app)
|
|
if log_filter == self.last_log_filter: return
|
|
|
|
# Update filter
|
|
self.last_log_filter = log_filter
|
|
if log_filter is not None: self.log_filter_re = re.compile(log_filter)
|
|
else: self.log_filter_re = None
|
|
|
|
# Reload log
|
|
app.log.set_text('')
|
|
self.log_add_lines(app, self.log)
|
|
|
|
|
|
def update_status_ui(self, app):
|
|
self.update_queue_ui(app)
|
|
self.update_status_slots(app)
|
|
self.update_work_unit_info(app)
|
|
app.update_client_status() # TODO this should probably be moved here
|
|
|
|
|
|
def reset_status_ui(self, app):
|
|
self.reset_work_unit_info(app)
|
|
app.queue_list.clear()
|
|
app.slot_status_list.clear()
|
|
app.log.set_text('')
|
|
|
|
|
|
def get_running(self):
|
|
for unit in self.queue:
|
|
if unit['state'].upper() == 'RUNNING': return True
|
|
return False
|
|
|
|
|
|
def get_option_changes(self, app):
|
|
used = set()
|
|
options = {}
|
|
|
|
used.add('power') # Don't set power here
|
|
|
|
# Proxy options
|
|
used.add('proxy')
|
|
proxy_addr = get_widget_str_value(app.client_option_widgets['proxy'])
|
|
proxy_port = get_widget_str_value(app.proxy_port)
|
|
proxy = '%s:%s' % (proxy_addr, proxy_port)
|
|
if self.get('proxy') != proxy: options['proxy'] = proxy
|
|
|
|
# Core priority radio button
|
|
used.add('core-priority')
|
|
if app.client_option_widgets['core_priority'].get_active():
|
|
if self.have('core-priority') and \
|
|
self.get('core-priority') != 'idle':
|
|
options['core-priority'] = 'idle'
|
|
elif self.get('core-priority') != 'low':
|
|
options['core-priority'] = 'low'
|
|
|
|
# Extra core options
|
|
used.add('extra-core-args')
|
|
if self.have('extra-core-args'):
|
|
old_args = self.get('extra-core-args').split()
|
|
else: old_args = []
|
|
|
|
new_args = []
|
|
def add_arg(model, path, iter, data):
|
|
new_args.append(model.get(iter, 0)[0])
|
|
app.core_option_list.foreach(add_arg, None)
|
|
|
|
if old_args != new_args:
|
|
if new_args: options['extra-core-args'] = ' '.join(new_args)
|
|
else: options['extra-core-args!'] = None
|
|
|
|
# Extra options
|
|
def check_option(model, path, iter, data):
|
|
name, value = model.get(iter, 0, 1)
|
|
used.add(name)
|
|
if self.get(name) != value: options[name] = value
|
|
|
|
app.option_list.foreach(check_option, None)
|
|
|
|
# Main options
|
|
for name, widget in app.client_option_widgets.items():
|
|
name = name.replace('_', '-')
|
|
if name in used: continue
|
|
value = self.get(name)
|
|
used.add(name)
|
|
|
|
try:
|
|
value = get_widget_str_value(widget)
|
|
old_value = self.get(name)
|
|
if value == '' and old_value is None: value = None
|
|
if value != old_value:
|
|
if value is None: options[name + '!'] = None
|
|
else: options[name] = value
|
|
|
|
except Exception, e: # Don't let one bad widget kill everything
|
|
print 'WARNING: failed to save widget "%s": %s' % (name, e)
|
|
|
|
# Removed options
|
|
for name in self.options:
|
|
if not name in used:
|
|
options[name + '!'] = None
|
|
|
|
return options
|
|
|
|
|
|
def get_slot_changes(self, app):
|
|
# Get new slots
|
|
new_slots = []
|
|
def add_slot(model, path, iter, data = None):
|
|
new_slots.append(model.get(iter, 2)[0].slot)
|
|
app.slot_list.foreach(add_slot)
|
|
|
|
# Get old slot IDs
|
|
old_slot_map = {}
|
|
for slot in self.slots: old_slot_map[slot.id] = slot
|
|
|
|
# Get new slot IDs
|
|
new_slot_ids = set()
|
|
for slot in new_slots: new_slot_ids.add(slot.id)
|
|
|
|
# Find deleted slot IDs
|
|
deleted = []
|
|
for id in old_slot_map:
|
|
if id not in new_slot_ids: deleted.append(id)
|
|
|
|
# Find added and modified slots
|
|
added = []
|
|
modified = []
|
|
for slot in new_slots:
|
|
# Added
|
|
if slot.id == -1: added.append((slot.type, slot.options))
|
|
else:
|
|
old_slot = old_slot_map[slot.id]
|
|
options = get_option_mods(old_slot.options, slot.options)
|
|
if options or old_slot.type != slot.type:
|
|
modified.append((slot.id, slot.type, options))
|
|
|
|
return (deleted, added, modified)
|
|
|
|
|
|
def get_changes(self, app):
|
|
return self.get_option_changes(app), self.get_slot_changes(app)
|
|
|
|
|
|
def has_changes(self, app):
|
|
options, slots = self.get_changes(app)
|
|
return options or slots != ([], [], [])
|