On selecting a slot programmatically the same code like selecting it manually by user must be called to also select the corresponding queue item.
664 lines
21 KiB
Python
664 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)
|
|
|
|
|
|
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)
|
|
else: app.queue_tree.get_selection().unselect_all()
|
|
|
|
|
|
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)
|
|
self.select_slot(app)
|
|
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)
|
|
text = text.decode('utf-8', 'ignore')
|
|
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 != ([], [], [])
|