@cryptotaxi247 / netdata-1 / commits / 703c82448

Add a Python module for interacting with BOINC.

This will be used for a BOINC plugin for netdata. The project this originally came form is now functionally dead (hasn't seen any activity for 3+ years), but it still works. I've made some changes to clean it up a bit for Netdata: * Changed the main class's initializer so it takes a port and a hostname separately instead of as a single string. * Removed the `if __name__ == '__main__'` part, as it's not going to be used here. * Merged the two files into one so it's easier to import sanely. * Silenced the complaints about missing attributes (they show up _all the time_, even for valid responses from the BOINC software).

Austin S. Hemmelgarn committed Jun 4, 2018 at 15:02 UTC 703c824485831ffb20810ec8c1b587efdf50b888
1 file changed +763
python.d/python_modules/third_party/boinc_client.py new
+763
@@ -0,0 +1,763 @@
1 +#!/usr/bin/env python
2 +# -*- coding: utf-8 -*-
3 +#
4 +# client.py - Somewhat higher-level GUI_RPC API for BOINC core client
5 +#
6 +# Copyright (C) 2013 Rodrigo Silva (MestreLion) <linux@rodrigosilva.com>
7 +# Copyright (C) 2017 Austin S. Hemmelgarn
8 +#
9 +# SPDX-License-Identifier: GPL-3.0
10 +
11 +# Based on client/boinc_cmd.cpp
12 +
13 +import hashlib
14 +import socket
15 +import time
16 +from functools import total_ordering
17 +from xml.etree import ElementTree
18 +
19 +GUI_RPC_PASSWD_FILE = "/var/lib/boinc/gui_rpc_auth.cfg"
20 +
21 +GUI_RPC_HOSTNAME = None # localhost
22 +GUI_RPC_PORT = 31416
23 +GUI_RPC_TIMEOUT = 30
24 +
25 +class Rpc(object):
26 + ''' Class to perform GUI RPC calls to a BOINC core client.
27 + Usage in a context manager ('with' block) is recommended to ensure
28 + disconnect() is called. Using the same instance for all calls is also
29 + recommended so it reuses the same socket connection
30 + '''
31 + def __init__(self, hostname="", port=0, timeout=0, text_output=False):
32 + self.hostname = hostname
33 + self.port = port
34 + self.timeout = timeout
35 + self.sock = None
36 + self.text_output = text_output
37 +
38 + @property
39 + def sockargs(self):
40 + return (self.hostname, self.port, self.timeout)
41 +
42 + def __enter__(self): self.connect(*self.sockargs); return self
43 + def __exit__(self, *args): self.disconnect()
44 +
45 + def connect(self, hostname="", port=0, timeout=0):
46 + ''' Connect to (hostname, port) with timeout in seconds.
47 + Hostname defaults to None (localhost), and port to 31416
48 + Calling multiple times will disconnect previous connection (if any),
49 + and (re-)connect to host.
50 + '''
51 + if self.sock:
52 + self.disconnect()
53 +
54 + self.hostname = hostname or GUI_RPC_HOSTNAME
55 + self.port = port or GUI_RPC_PORT
56 + self.timeout = timeout or GUI_RPC_TIMEOUT
57 +
58 + self.sock = socket.create_connection(self.sockargs[0:2], self.sockargs[2])
59 +
60 + def disconnect(self):
61 + ''' Disconnect from host. Calling multiple times is OK (idempotent)
62 + '''
63 + if self.sock:
64 + self.sock.close()
65 + self.sock = None
66 +
67 + def call(self, request, text_output=None):
68 + ''' Do an RPC call. Pack and send the XML request and return the
69 + unpacked reply. request can be either plain XML text or a
70 + xml.etree.ElementTree.Element object. Return ElementTree.Element
71 + or XML text according to text_output flag.
72 + Will auto-connect if not connected.
73 + '''
74 + if text_output is None:
75 + text_output = self.text_output
76 +
77 + if not self.sock:
78 + self.connect(*self.sockargs)
79 +
80 + if not isinstance(request, ElementTree.Element):
81 + request = ElementTree.fromstring(request)
82 +
83 + # pack request
84 + end = '\003'
85 + req = "<boinc_gui_rpc_request>\n%s\n</boinc_gui_rpc_request>\n%s" \
86 + % (ElementTree.tostring(request).replace(' />','/>'), end)
87 +
88 + try:
89 + self.sock.sendall(req)
90 + except (socket.error, socket.herror, socket.gaierror, socket.timeout):
91 + raise
92 +
93 + req = ""
94 + while True:
95 + try:
96 + buf = self.sock.recv(8192)
97 + if not buf:
98 + raise socket.error("No data from socket")
99 + except socket.error:
100 + raise
101 + n = buf.find(end)
102 + if not n == -1: break
103 + req += buf
104 + req += buf[:n]
105 +
106 + # unpack reply (remove root tag, ie: first and last lines)
107 + req = '\n'.join(req.strip().rsplit('\n')[1:-1])
108 +
109 + if text_output:
110 + return req
111 + else:
112 + return ElementTree.fromstring(req)
113 +
114 +def setattrs_from_xml(obj, xml, attrfuncdict={}):
115 + ''' Helper to set values for attributes of a class instance by mapping
116 + matching tags from a XML file.
117 + attrfuncdict is a dict of functions to customize value data type of
118 + each attribute. It falls back to simple int/float/bool/str detection
119 + based on values defined in __init__(). This would not be needed if
120 + Boinc used standard RPC protocol, which includes data type in XML.
121 + '''
122 + if not isinstance(xml, ElementTree.Element):
123 + xml = ElementTree.fromstring(xml)
124 + for e in list(xml):
125 + if hasattr(obj, e.tag):
126 + attr = getattr(obj, e.tag)
127 + attrfunc = attrfuncdict.get(e.tag, None)
128 + if attrfunc is None:
129 + if isinstance(attr, bool): attrfunc = parse_bool
130 + elif isinstance(attr, int): attrfunc = parse_int
131 + elif isinstance(attr, float): attrfunc = parse_float
132 + elif isinstance(attr, str): attrfunc = parse_str
133 + elif isinstance(attr, list): attrfunc = parse_list
134 + else: attrfunc = lambda x: x
135 + setattr(obj, e.tag, attrfunc(e))
136 + else:
137 + pass
138 + #print "class missing attribute '%s': %r" % (e.tag, obj)
139 + return obj
140 +
141 +
142 +def parse_bool(e):
143 + ''' Helper to convert ElementTree.Element.text to boolean.
144 + Treat '<foo/>' (and '<foo>[[:blank:]]</foo>') as True
145 + Treat '0' and 'false' as False
146 + '''
147 + if e.text is None:
148 + return True
149 + else:
150 + return bool(e.text) and not e.text.strip().lower() in ('0', 'false')
151 +
152 +
153 +def parse_int(e):
154 + ''' Helper to convert ElementTree.Element.text to integer.
155 + Treat '<foo/>' (and '<foo></foo>') as 0
156 + '''
157 + # int(float()) allows casting to int a value expressed as float in XML
158 + return 0 if e.text is None else int(float(e.text.strip()))
159 +
160 +
161 +def parse_float(e):
162 + ''' Helper to convert ElementTree.Element.text to float. '''
163 + return 0.0 if e.text is None else float(e.text.strip())
164 +
165 +
166 +def parse_str(e):
167 + ''' Helper to convert ElementTree.Element.text to string. '''
168 + return "" if e.text is None else e.text.strip()
169 +
170 +
171 +def parse_list(e):
172 + ''' Helper to convert ElementTree.Element to list. For now, simply return
173 + the list of root element's children
174 + '''
175 + return list(e)
176 +
177 +
178 +class Enum(object):
179 + UNKNOWN = -1 # Not in original API
180 +
181 + @classmethod
182 + def name(cls, value):
183 + ''' Quick-and-dirty fallback for getting the "name" of an enum item '''
184 +
185 + # value as string, if it matches an enum attribute.
186 + # Allows short usage as Enum.name("VALUE") besides Enum.name(Enum.VALUE)
187 + if hasattr(cls, str(value)):
188 + return cls.name(getattr(cls, value, None))
189 +
190 + # value not handled in subclass name()
191 + for k, v in cls.__dict__.items():
192 + if v == value:
193 + return k.lower().replace('_', ' ')
194 +
195 + # value not found
196 + return cls.name(Enum.UNKNOWN)
197 +
198 +
199 +class NetworkStatus(Enum):
200 + ''' Values of "network_status" '''
201 + ONLINE = 0 #// have network connections open
202 + WANT_CONNECTION = 1 #// need a physical connection
203 + WANT_DISCONNECT = 2 #// don't have any connections, and don't need any
204 + LOOKUP_PENDING = 3 #// a website lookup is pending (try again later)
205 +
206 + @classmethod
207 + def name(cls, v):
208 + if v == cls.UNKNOWN: return "unknown"
209 + elif v == cls.ONLINE: return "online" # misleading
210 + elif v == cls.WANT_CONNECTION: return "need connection"
211 + elif v == cls.WANT_DISCONNECT: return "don't need connection"
212 + elif v == cls.LOOKUP_PENDING: return "reference site lookup pending"
213 + else: return super(NetworkStatus, cls).name(v)
214 +
215 +
216 +class SuspendReason(Enum):
217 + ''' bitmap defs for task_suspend_reason, network_suspend_reason
218 + Note: doesn't need to be a bitmap, but keep for compatibility
219 + '''
220 + NOT_SUSPENDED = 0 # Not in original API
221 + BATTERIES = 1
222 + USER_ACTIVE = 2
223 + USER_REQ = 4
224 + TIME_OF_DAY = 8
225 + BENCHMARKS = 16
226 + DISK_SIZE = 32
227 + CPU_THROTTLE = 64
228 + NO_RECENT_INPUT = 128
229 + INITIAL_DELAY = 256
230 + EXCLUSIVE_APP_RUNNING = 512
231 + CPU_USAGE = 1024
232 + NETWORK_QUOTA_EXCEEDED = 2048
233 + OS = 4096
234 + WIFI_STATE = 4097
235 + BATTERY_CHARGING = 4098
236 + BATTERY_OVERHEATED = 4099
237 +
238 + @classmethod
239 + def name(cls, v):
240 + if v == cls.UNKNOWN: return "unknown reason"
241 + elif v == cls.BATTERIES: return "on batteries"
242 + elif v == cls.USER_ACTIVE: return "computer is in use"
243 + elif v == cls.USER_REQ: return "user request"
244 + elif v == cls.TIME_OF_DAY: return "time of day"
245 + elif v == cls.BENCHMARKS: return "CPU benchmarks in progress"
246 + elif v == cls.DISK_SIZE: return "need disk space - check preferences"
247 + elif v == cls.NO_RECENT_INPUT: return "no recent user activity"
248 + elif v == cls.INITIAL_DELAY: return "initial delay"
249 + elif v == cls.EXCLUSIVE_APP_RUNNING: return "an exclusive app is running"
250 + elif v == cls.CPU_USAGE: return "CPU is busy"
251 + elif v == cls.NETWORK_QUOTA_EXCEEDED: return "network bandwidth limit exceeded"
252 + elif v == cls.OS: return "requested by operating system"
253 + elif v == cls.WIFI_STATE: return "not connected to WiFi network"
254 + elif v == cls.BATTERY_CHARGING: return "battery is recharging"
255 + elif v == cls.BATTERY_OVERHEATED: return "battery is overheated"
256 + else: return super(SuspendReason, cls).name(v)
257 +
258 +
259 +class RunMode(Enum):
260 + ''' Run modes for CPU, GPU, network,
261 + controlled by Activity menu and snooze button
262 + '''
263 + ALWAYS = 1
264 + AUTO = 2
265 + NEVER = 3
266 + RESTORE = 4
267 + #// restore permanent mode - used only in set_X_mode() GUI RPC
268 +
269 + @classmethod
270 + def name(cls, v):
271 + # all other modes use the fallback name
272 + if v == cls.AUTO: return "according to prefs"
273 + else: return super(RunMode, cls).name(v)
274 +
275 +
276 +class CpuSched(Enum):
277 + ''' values of ACTIVE_TASK::scheduler_state and ACTIVE_TASK::next_scheduler_state
278 + "SCHEDULED" is synonymous with "executing" except when CPU throttling
279 + is in use.
280 + '''
281 + UNINITIALIZED = 0
282 + PREEMPTED = 1
283 + SCHEDULED = 2
284 +
285 +
286 +class ResultState(Enum):
287 + ''' Values of RESULT::state in client.
288 + THESE MUST BE IN NUMERICAL ORDER
289 + (because of the > comparison in RESULT::computing_done())
290 + see html/inc/common_defs.inc
291 + '''
292 + NEW = 0
293 + #// New result
294 + FILES_DOWNLOADING = 1
295 + #// Input files for result (WU, app version) are being downloaded
296 + FILES_DOWNLOADED = 2
297 + #// Files are downloaded, result can be (or is being) computed
298 + COMPUTE_ERROR = 3
299 + #// computation failed; no file upload
300 + FILES_UPLOADING = 4
301 + #// Output files for result are being uploaded
302 + FILES_UPLOADED = 5
303 + #// Files are uploaded, notify scheduling server at some point
304 + ABORTED = 6
305 + #// result was aborted
306 + UPLOAD_FAILED = 7
307 + #// some output file permanent failure
308 +
309 +
310 +class Process(Enum):
311 + ''' values of ACTIVE_TASK::task_state '''
312 + UNINITIALIZED = 0
313 + #// process doesn't exist yet
314 + EXECUTING = 1
315 + #// process is running, as far as we know
316 + SUSPENDED = 9
317 + #// we've sent it a "suspend" message
318 + ABORT_PENDING = 5
319 + #// process exceeded limits; send "abort" message, waiting to exit
320 + QUIT_PENDING = 8
321 + #// we've sent it a "quit" message, waiting to exit
322 + COPY_PENDING = 10
323 + #// waiting for async file copies to finish
324 +
325 +
326 +class _Struct(object):
327 + ''' base helper class with common methods for all classes derived from
328 + BOINC's C++ structs
329 + '''
330 + @classmethod
331 + def parse(cls, xml):
332 + return setattrs_from_xml(cls(), xml)
333 +
334 + def __str__(self, indent=0):
335 + buf = '%s%s:\n' % ('\t' * indent, self.__class__.__name__)
336 + for attr in self.__dict__:
337 + value = getattr(self, attr)
338 + if isinstance(value, list):
339 + buf += '%s\t%s [\n' % ('\t' * indent, attr)
340 + for v in value: buf += '\t\t%s\t\t,\n' % v
341 + buf += '\t]\n'
342 + else:
343 + buf += '%s\t%s\t%s\n' % ('\t' * indent,
344 + attr,
345 + value.__str__(indent+2)
346 + if isinstance(value, _Struct)
347 + else repr(value))
348 + return buf
349 +
350 +
351 +@total_ordering
352 +class VersionInfo(_Struct):
353 + def __init__(self, major=0, minor=0, release=0):
354 + self.major = major
355 + self.minor = minor
356 + self.release = release
357 +
358 + @property
359 + def _tuple(self):
360 + return (self.major, self.minor, self.release)
361 +
362 + def __eq__(self, other):
363 + return isinstance(other, self.__class__) and self._tuple == other._tuple
364 +
365 + def __ne__(self, other):
366 + return not self.__eq__(other)
367 +
368 + def __gt__(self, other):
369 + if not isinstance(other, self.__class__):
370 + return NotImplemented
371 + return self._tuple > other._tuple
372 +
373 + def __str__(self):
374 + return "%d.%d.%d" % (self.major, self.minor, self.release)
375 +
376 + def __repr__(self):
377 + return "%s%r" % (self.__class__.__name__, self._tuple)
378 +
379 +
380 +class CcStatus(_Struct):
381 + def __init__(self):
382 + self.network_status = NetworkStatus.UNKNOWN
383 + self.ams_password_error = False
384 + self.manager_must_quit = False
385 +
386 + self.task_suspend_reason = SuspendReason.UNKNOWN #// bitmap
387 + self.task_mode = RunMode.UNKNOWN
388 + self.task_mode_perm = RunMode.UNKNOWN #// same, but permanent version
389 + self.task_mode_delay = 0.0 #// time until perm becomes actual
390 +
391 + self.network_suspend_reason = SuspendReason.UNKNOWN
392 + self.network_mode = RunMode.UNKNOWN
393 + self.network_mode_perm = RunMode.UNKNOWN
394 + self.network_mode_delay = 0.0
395 +
396 + self.gpu_suspend_reason = SuspendReason.UNKNOWN
397 + self.gpu_mode = RunMode.UNKNOWN
398 + self.gpu_mode_perm = RunMode.UNKNOWN
399 + self.gpu_mode_delay = 0.0
400 +
401 + self.disallow_attach = False
402 + self.simple_gui_only = False
403 +
404 +
405 +class HostInfo(_Struct):
406 + def __init__(self):
407 + self.timezone = 0 #// local STANDARD time - UTC time (in seconds)
408 + self.domain_name = ""
409 + self.ip_addr = ""
410 + self.host_cpid = ""
411 +
412 + self.p_ncpus = 0 #// Number of CPUs on host
413 + self.p_vendor = "" #// Vendor name of CPU
414 + self.p_model = "" #// Model of CPU
415 + self.p_features = ""
416 + self.p_fpops = 0.0 #// measured floating point ops/sec of CPU
417 + self.p_iops = 0.0 #// measured integer ops/sec of CPU
418 + self.p_membw = 0.0 #// measured memory bandwidth (bytes/sec) of CPU
419 + #// The above are per CPU, not total
420 + self.p_calculated = 0.0 #// when benchmarks were last run, or zero
421 + self.p_vm_extensions_disabled = False
422 +
423 + self.m_nbytes = 0 #// Size of memory in bytes
424 + self.m_cache = 0 #// Size of CPU cache in bytes (L1 or L2?)
425 + self.m_swap = 0 #// Size of swap space in bytes
426 +
427 + self.d_total = 0 #// Total disk space on volume containing
428 + #// the BOINC client directory.
429 + self.d_free = 0 #// how much is free on that volume
430 +
431 + self.os_name = "" #// Name of operating system
432 + self.os_version = "" #// Version of operating system
433 +
434 + #// the following is non-empty if VBox is installed
435 + self.virtualbox_version = ""
436 +
437 + self.coprocs = [] # COPROCS
438 +
439 + # The following are currently unused (not in RPC XML)
440 + self.serialnum = "" #// textual description of coprocessors
441 +
442 + @classmethod
443 + def parse(cls, xml):
444 + if not isinstance(xml, ElementTree.Element):
445 + xml = ElementTree.fromstring(xml)
446 +
447 + # parse main XML
448 + hostinfo = super(HostInfo, cls).parse(xml)
449 +
450 + # parse each coproc in coprocs list
451 + aux = []
452 + for c in hostinfo.coprocs:
453 + aux.append(Coproc.parse(c))
454 + hostinfo.coprocs = aux
455 +
456 + return hostinfo
457 +
458 +
459 +class Coproc(_Struct):
460 + ''' represents a set of identical coprocessors on a particular computer.
461 + Abstract class;
462 + objects will always be a derived class (COPROC_CUDA, COPROC_ATI)
463 + Used in both client and server.
464 + '''
465 + def __init__(self):
466 + self.type = "" #// must be unique
467 + self.count = 0 #// how many are present
468 + self.peak_flops = 0.0
469 + self.used = 0.0 #// how many are in use (used by client)
470 + self.have_cuda = False #// True if this GPU supports CUDA on this computer
471 + self.have_cal = False #// True if this GPU supports CAL on this computer
472 + self.have_opencl = False #// True if this GPU supports openCL on this computer
473 + self.available_ram = 0
474 + self.specified_in_config = False
475 + #// If true, this coproc was listed in cc_config.xml
476 + #// rather than being detected by the client.
477 +
478 + #// the following are used in both client and server for work-fetch info
479 + self.req_secs = 0.0
480 + #// how many instance-seconds of work requested
481 + self.req_instances = 0.0
482 + #// client is requesting enough jobs to use this many instances
483 + self.estimated_delay = 0
484 + #// resource will be saturated for this long
485 +
486 + self.opencl_device_count = 0
487 + self.last_print_time = 0.0
488 +
489 + #self.opencl_prop = None # OPENCL_DEVICE_PROP
490 +
491 +
492 +class Result(_Struct):
493 + ''' Also called "task" in some contexts '''
494 + def __init__(self):
495 + # Names and values follow lib/gui_rpc_client.h @ RESULT
496 + # Order too, except when grouping contradicts client/result.cpp
497 + # RESULT::write_gui(), then XML order is used.
498 +
499 + self.name = ""
500 + self.wu_name = ""
501 + self.version_num = 0
502 + #// identifies the app used
503 + self.plan_class = ""
504 + self.project_url = "" # from PROJECT.master_url
505 + self.report_deadline = 0.0 # seconds since epoch
506 + self.received_time = 0.0 # seconds since epoch
507 + #// when we got this from server
508 + self.ready_to_report = False
509 + #// we're ready to report this result to the server;
510 + #// either computation is done and all the files have been uploaded
511 + #// or there was an error
512 + self.got_server_ack = False
513 + #// we've received the ack for this result from the server
514 + self.final_cpu_time = 0.0
515 + self.final_elapsed_time = 0.0
516 + self.state = ResultState.NEW
517 + self.estimated_cpu_time_remaining = 0.0
518 + #// actually, estimated elapsed time remaining
519 + self.exit_status = 0
520 + #// return value from the application
521 + self.suspended_via_gui = False
522 + self.project_suspended_via_gui = False
523 + self.edf_scheduled = False
524 + #// temporary used to tell GUI that this result is deadline-scheduled
525 + self.coproc_missing = False
526 + #// a coproc needed by this job is missing
527 + #// (e.g. because user removed their GPU board).
528 + self.scheduler_wait = False
529 + self.scheduler_wait_reason = ""
530 + self.network_wait = False
531 + self.resources = ""
532 + #// textual description of resources used
533 +
534 + #// the following defined if active
535 + # XML is generated in client/app.cpp ACTIVE_TASK::write_gui()
536 + self.active_task = False
537 + self.active_task_state = Process.UNINITIALIZED
538 + self.app_version_num = 0
539 + self.slot = -1
540 + self.pid = 0
541 + self.scheduler_state = CpuSched.UNINITIALIZED
542 + self.checkpoint_cpu_time = 0.0
543 + self.current_cpu_time = 0.0
544 + self.fraction_done = 0.0
545 + self.elapsed_time = 0.0
546 + self.swap_size = 0
547 + self.working_set_size_smoothed = 0.0
548 + self.too_large = False
549 + self.needs_shmem = False
550 + self.graphics_exec_path = ""
551 + self.web_graphics_url = ""
552 + self.remote_desktop_addr = ""
553 + self.slot_path = ""
554 + #// only present if graphics_exec_path is
555 +
556 + # The following are not in original API, but are present in RPC XML reply
557 + self.completed_time = 0.0
558 + #// time when ready_to_report was set
559 + self.report_immediately = False
560 + self.working_set_size = 0
561 + self.page_fault_rate = 0.0
562 + #// derived by higher-level code
563 +
564 + # The following are in API, but are NEVER in RPC XML reply. Go figure
565 + self.signal = 0
566 +
567 + self.app = None # APP*
568 + self.wup = None # WORKUNIT*
569 + self.project = None # PROJECT*
570 + self.avp = None # APP_VERSION*
571 +
572 + @classmethod
573 + def parse(cls, xml):
574 + if not isinstance(xml, ElementTree.Element):
575 + xml = ElementTree.fromstring(xml)
576 +
577 + # parse main XML
578 + result = super(Result, cls).parse(xml)
579 +
580 + # parse '<active_task>' children
581 + active_task = xml.find('active_task')
582 + if active_task is None:
583 + result.active_task = False # already the default after __init__()
584 + else:
585 + result.active_task = True # already the default after main parse
586 + result = setattrs_from_xml(result, active_task)
587 +
588 + #// if CPU time is nonzero but elapsed time is zero,
589 + #// we must be talking to an old client.
590 + #// Set elapsed = CPU
591 + #// (easier to deal with this here than in the manager)
592 + if result.current_cpu_time != 0 and result.elapsed_time == 0:
593 + result.elapsed_time = result.current_cpu_time
594 +
595 + if result.final_cpu_time != 0 and result.final_elapsed_time == 0:
596 + result.final_elapsed_time = result.final_cpu_time
597 +
598 + return result
599 +
600 + def __str__(self):
601 + buf = '%s:\n' % self.__class__.__name__
602 + for attr in self.__dict__:
603 + value = getattr(self, attr)
604 + if attr in ['received_time', 'report_deadline']:
605 + value = time.ctime(value)
606 + buf += '\t%s\t%r\n' % (attr, value)
607 + return buf
608 +
609 +
610 +class BoincClient(object):
611 +
612 + def __init__(self, host="", port=0, passwd=None):
613 + self.hostname = host
614 + self.port = port
615 + self.passwd = passwd
616 + self.rpc = Rpc(text_output=False)
617 + self.version = None
618 + self.authorized = False
619 +
620 + # Informative, not authoritative. Records status of *last* RPC call,
621 + # but does not infer success about the *next* one.
622 + # Thus, it should be read *after* an RPC call, not prior to one
623 + self.connected = False
624 +
625 + def __enter__(self): self.connect(); return self
626 + def __exit__(self, *args): self.disconnect()
627 +
628 + def connect(self):
629 + try:
630 + self.rpc.connect(self.hostname, self.port)
631 + self.connected = True
632 + except socket.error:
633 + self.connected = False
634 + return
635 + self.authorized = self.authorize(self.passwd)
636 + self.version = self.exchange_versions()
637 +
638 + def disconnect(self):
639 + self.rpc.disconnect()
640 +
641 + def authorize(self, password):
642 + ''' Request authorization. If password is None and we are connecting
643 + to localhost, try to read password from the local config file
644 + GUI_RPC_PASSWD_FILE. If file can't be read (not found or no
645 + permission to read), try to authorize with a blank password.
646 + If authorization is requested and fails, all subsequent calls
647 + will be refused with socket.error 'Connection reset by peer' (104).
648 + Since most local calls do no require authorization, do not attempt
649 + it if you're not sure about the password.
650 + '''
651 + if password is None and not self.hostname:
652 + password = read_gui_rpc_password() or ""
653 + nonce = self.rpc.call('<auth1/>').text
654 + hash = hashlib.md5('%s%s' % (nonce, password)).hexdigest().lower()
655 + reply = self.rpc.call('<auth2><nonce_hash>%s</nonce_hash></auth2>' % hash)
656 +
657 + if reply.tag == 'authorized':
658 + return True
659 + else:
660 + return False
661 +
662 + def exchange_versions(self):
663 + ''' Return VersionInfo instance with core client version info '''
664 + return VersionInfo.parse(self.rpc.call('<exchange_versions/>'))
665 +
666 + def get_cc_status(self):
667 + ''' Return CcStatus instance containing basic status, such as
668 + CPU / GPU / Network active/suspended, etc
669 + '''
670 + if not self.connected: self.connect()
671 + try:
672 + return CcStatus.parse(self.rpc.call('<get_cc_status/>'))
673 + except socket.error:
674 + self.connected = False
675 +
676 + def get_host_info(self):
677 + ''' Get information about host hardware and usage. '''
678 + return HostInfo.parse(self.rpc.call('<get_host_info/>'))
679 +
680 + def get_tasks(self):
681 + ''' Same as get_results(active_only=False) '''
682 + return self.get_results(False)
683 +
684 + def get_results(self, active_only=False):
685 + ''' Get a list of results.
686 + Those that are in progress will have information such as CPU time
687 + and fraction done. Each result includes a name;
688 + Use CC_STATE::lookup_result() to find this result in the current static state;
689 + if it's not there, call get_state() again.
690 + '''
691 + reply = self.rpc.call("<get_results><active_only>%d</active_only></get_results>"
692 + % (1 if active_only else 0))
693 + if not reply.tag == 'results':
694 + return []
695 +
696 + results = []
697 + for item in list(reply):
698 + results.append(Result.parse(item))
699 +
700 + return results
701 +
702 + def set_mode(self, component, mode, duration=0):
703 + ''' Do the real work of set_{run,gpu,network}_mode()
704 + This method is not part of the original API.
705 + Valid components are 'run' (or 'cpu'), 'gpu', 'network' (or 'net')
706 + '''
707 + component = component.replace('cpu','run')
708 + component = component.replace('net','network')
709 + try:
710 + reply = self.rpc.call("<set_%s_mode>"
711 + "<%s/><duration>%f</duration>"
712 + "</set_%s_mode>"
713 + % (component,
714 + RunMode.name(mode).lower(), duration,
715 + component))
716 + return (reply.tag == 'success')
717 + except socket.error:
718 + return False
719 +
720 + def set_run_mode(self, mode, duration=0):
721 + ''' Set the run mode (RunMode.NEVER/AUTO/ALWAYS/RESTORE)
722 + NEVER will suspend all activity, including CPU, GPU and Network
723 + AUTO will run according to preferences.
724 + If duration is zero, mode is permanent. Otherwise revert to last
725 + permanent mode after duration seconds elapse.
726 + '''
727 + return self.set_mode('cpu', mode, duration)
728 +
729 + def set_gpu_mode(self, mode, duration=0):
730 + ''' Set the GPU run mode, similar to set_run_mode() but for GPU only
731 + '''
732 + return self.set_mode('gpu', mode, duration)
733 +
734 + def set_network_mode(self, mode, duration=0):
735 + ''' Set the Network run mode, similar to set_run_mode()
736 + but for network activity only
737 + '''
738 + return self.set_mode('net', mode, duration)
739 +
740 + def run_benchmarks(self):
741 + ''' Run benchmarks. Computing will suspend during benchmarks '''
742 + return self.rpc.call('<run_benchmarks/>').tag == "success"
743 +
744 + def quit(self):
745 + ''' Tell the core client to exit '''
746 + if self.rpc.call('<quit/>').tag == "success":
747 + self.connected = False
748 + return True
749 + return False
750 +
751 +
752 +def read_gui_rpc_password():
753 + ''' Read password string from GUI_RPC_PASSWD_FILE file, trim the last CR
754 + (if any), and return it
755 + '''
756 + try:
757 + with open(GUI_RPC_PASSWD_FILE, 'r') as f:
758 + buf = f.read()
759 + if buf.endswith('\n'): return buf[:-1] # trim last CR
760 + else: return buf
761 + except IOError:
762 + # Permission denied or File not found.
763 + pass