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