forked from makeasnek/FindTheMag2
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
5011 lines (4625 loc) · 198 KB
/
main.py
File metadata and controls
5011 lines (4625 loc) · 198 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# DO NOT EDIT THIS FILE, EDIT USER_CONFIG.PY INSTEAD
# SPDX-License-Identifier: AGPL-3.0-only
try:
from math import floor, ceil
import copy
import shlex
import shutil
import subprocess
from time import sleep
import asyncio
import config
import logging.handlers
import os
import libs.pyboinc
from libs.pyboinc._parse import parse_generic
from libs.pyboinc import init_rpc_client
import xml.etree.ElementTree as ET
import json
import pprint
import re
import platform
from pathlib import Path
import datetime
import xmltodict
import requests
from requests.auth import HTTPBasicAuth
from typing import List, Union, Dict, Tuple, Set, Any
import sys, signal
from grc_price_utils import get_grc_price_from_sites
# This is needed for some async stuff
import nest_asyncio
nest_asyncio.apply()
# Ignore deprecation warnings in Windows
import warnings
except Exception as e:
print(
"Error loading some required modules. Make sure you have installed the modules in requirements.txt as documented in the README"
)
print(str(e))
quit()
warnings.filterwarnings("ignore", category=DeprecationWarning)
# Set default settings for all vars
preferred_projects_percent: float = 80
preferred_projects: Dict[str, int] = {}
IGNORED_PROJECTS: List[str] = ["https://foldingathome.div72.xyz/"]
BOINC_DATA_DIR: Union[str, None] = None
GRIDCOIN_DATA_DIR: Union[str, None] = None
CONTROL_BOINC: bool = False
BOINC_IP: str = "127.0.0.1"
BOINC_PORT: int = 31416
BOINC_USERNAME: Union[str, None] = None
BOINC_PASSWORD: Union[str, None] = None
# Minimum time in minutes before re-asking a project for work who previously said
# they were out
MIN_RECHECK_TIME: int = 30
ABORT_UNSTARTED_TASKS: bool = False
RECALCULATE_STATS_INTERVAL: int = 60
PRICE_CHECK_INTERVAL: int = 720
LOCAL_KWH: float = 0.1542
GRC_SELL_PRICE: Union[float, None] = None
EXCHANGE_FEE: float = 0.00
ONLY_BOINC_IF_PROFITABLE: bool = False
ONLY_MINE_IF_PROFITABLE: bool = False
HOST_POWER_USAGE: float = 70
MIN_PROFIT_PER_HOUR: float = 0
BENCHMARKING_MINIMUM_WUS: float = 5
BENCHMARKING_MINIMUM_TIME: float = 10
BENCHMARKING_DELAY_IN_DAYS: float = 160
SKIP_BENCHMARKING: bool = False
DEV_FEE: float = 0.05
VERSION = 3.3
DEV_RPC_PORT = 31418
LOG_LEVEL = "WARNING"
START_TEMP: int = 65
STOP_TEMP: int = 75
TEMP_COMMAND = None
ENABLE_TEMP_CONTROL = True # Enable controlling BOINC based on temp. Default: False
TEMP_SLEEP_TIME = 10
TEMP_REGEX = r"\d*"
MAX_LOGFILE_SIZE_IN_MB = 10
ROLLING_WEIGHT_WINDOW = 60
LOOKBACK_PERIOD = 30
DUMP_PROJECT_WEIGHTS: bool = False # Dump weights assigned to projects
DUMP_PROJECT_PRIORITY: bool = (
False # Dump weights adjusted after considering current and past crunching time
)
DUMP_RAC_MAG_RATIOS: bool = False # Dump the RAC:MAG ratios from each Gridcoin project
DUMP_DATABASE: bool = False # Dump the DATABASE
DEV_FEE_MODE: str = "CRUNCH" # valid values: CRUNCH|SIDESTAKE
CRUNCHING_FOR_DEV: bool = False
DEV_EXIT_TEST: bool = False # Only used for testing
# Some globals we need. I try to have all globals be ALL CAPS
FORCE_DEV_MODE = (
False # Used for debugging purposes to force crunching under dev account
)
BOINC_PROJECT_NAMES = {}
DATABASE = {}
DATABASE[
"TABLE_SLEEP_REASON"
] = "" # Sleep reason printed in table, must be reset at script start
DATABASE[
"TABLE_STATUS"
] = "" # Info status printed in table, must be reset at script start
SCRIPTED_RUN: bool = False
SKIP_TABLE_UPDATES: bool = False
HOST_COST_PER_HOUR = (HOST_POWER_USAGE / 1000) * LOCAL_KWH
LAST_KNOWN_CPU_MODE = None
LAST_KNOWN_GPU_MODE = None
LOOKUP_URL_TO_DATABASE = {} # Lookup table for uppered URLS -> canonical URLs.
LOOKUP_URL_TO_BOINC = (
{}
) # Lookup table for uppered URLs -> BOINC urls. Note the key is NOT the canonical url, just an uppered URL for performance reasons.
LOOKUP_URL_TO_BOINC_DEV = (
{}
) # Lookup table for uppered URLs -> BOINC urls for dev client. Note the key is NOT the canonical url, just an uppered URL for performance reasons.
ATTACHED_PROJECT_SET = set()
ATTACHED_PROJECT_SET_DEV = set()
COMBINED_STATS = {}
COMBINED_STATS_DEV = {}
PROJECT_MAG_RATIOS_CACHE = {}
TESTING: bool = False
PRINT_URL_LOOKUP_TABLE: Dict[
str, str
] = {} # Used to convert urls for printing to table
MAG_RATIO_SOURCE: Union[str, None] = None # Valid values: WALLET|WEB
CHECK_SIDESTAKE_RESULTS = False
loop = asyncio.get_event_loop()
# Translates BOINC's CPU and GPU Mode replies into English. Note difference between
# keys integer vs string.
CPU_MODE_DICT = {1: "always", 2: "auto", 3: "never"}
GPU_MODE_DICT = {"1": "always", "2": "auto", "3": "never"}
ROUNDING_DICT = {
"MAGPERCREDIT": 5,
"AVGMAGPERHOUR": 3,
}
DEV_BOINC_PASSWORD = "" # This is only used for printing to table, not used elsewhere
DEV_LOOP_RUNNING = False
SAVE_STATS_DB = (
{}
) # Keeps cache of saved stats databases so we don't write more often than we need too
def resolve_url_database(url: str) -> str:
"""
Given a URL or list of URLs, return the canonical version used in DATABASE and other internal references. Note that some projects operate at multiple
URLs. This will choose one URL and collapse all other URLs into it.
@param url: A url you want canonicalized
"""
uppered = url.upper()
if uppered in LOOKUP_URL_TO_DATABASE:
return LOOKUP_URL_TO_DATABASE[uppered]
uppered = uppered.replace("HTTPS://WWW.", "")
uppered = uppered.replace("HTTP://WWW.", "")
uppered = uppered.replace("HTTPS://", "")
uppered = uppered.replace("HTTP://", "")
if uppered.startswith(
"WWW."
): # This is needed as WWW. may legitimately exist in a url outside of the starting portion
uppered = uppered.replace("WWW.", "")
if uppered.endswith("/"): # Remove trailing slashes
uppered = uppered[:-1]
if "WORLDCOMMUNITYGRID.ORG/BOINC" in uppered:
uppered = "WORLDCOMMUNITYGRID.ORG"
LOOKUP_URL_TO_DATABASE[url.upper()] = uppered
return uppered
# Import user settings from config
try:
from config import *
except Exception as e:
print("Error opening config.py, using defaults! Error is: {}".format(e))
# Import additional user settings from user_config
if os.path.isfile("user_config.py"):
try:
from user_config import * # You can ignore an unresolved reference error here in pycharm since user is expected to create this file
import user_config
except Exception as e:
print("Error opening user_config.py, using defaults! Error is: {}".format(e))
# Verify all imports are upper-cased
for variable in dir(config):
if variable.startswith("__"):
continue
if str(variable) != variable.upper():
error = "Error: variable from config file {} is not uppercased. Make sure all variables you set are uppercased and named the same as the template in config.py".format(
variable
)
print(error)
quit()
if os.path.exists("user_config.py"):
for variable in dir(user_config):
if variable.startswith("__"):
continue
if str(variable) != variable.upper():
error = "Error: variable from config file {} is not uppercased. Make sure all variables you set are uppercased and named the same as the template in config.py".format(
variable
)
print(error)
quit()
# Setup logging
log = logging.getLogger()
if LOG_LEVEL == "NONE":
log.addHandler(logging.NullHandler())
else:
handler = logging.handlers.RotatingFileHandler(
os.environ.get("LOGFILE", "debug.log"),
maxBytes=MAX_LOGFILE_SIZE_IN_MB * 1024 * 1024,
backupCount=1,
)
log.setLevel(os.environ.get("LOGLEVEL", LOG_LEVEL))
formatter = logging.Formatter(
fmt="[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s"
)
handler.setFormatter(formatter)
log.addHandler(handler)
log.error("+++++++++++++++FTM STARTING+++++++++++++++++")
log.error("+++++++++++++++FTM STARTING+++++++++++++++++")
log.error("+++++++++++++++FTM STARTING+++++++++++++++++")
log.error(
"Start FTM log FTM version {} at {}".format(
VERSION, datetime.datetime.now().strftime("%m/%d/%Y, %H:%M:%S")
)
)
# Canonicalize URLs given to us by user
old_preferred_projects = copy.deepcopy(PREFERRED_PROJECTS)
PREFERRED_PROJECTS = {}
for url, amount in old_preferred_projects.items():
canonicalized = resolve_url_database(url)
PREFERRED_PROJECTS[canonicalized] = amount
for url in list(IGNORED_PROJECTS):
IGNORED_PROJECTS.remove(url)
canonicalized = resolve_url_database(url)
IGNORED_PROJECTS.append(canonicalized)
# If user has no preferred projects, their % of crunching should be 0
if len(PREFERRED_PROJECTS) == 0:
preferred_projects_percent: float = 0
# Detect platform, guess BOINC and Gridcoin directories if needed
FOUND_PLATFORM = platform.system()
if not BOINC_DATA_DIR:
if FOUND_PLATFORM == "Linux":
if os.path.isdir("/var/lib/boinc-client"):
BOINC_DATA_DIR = "/var/lib/boinc-client"
elif os.path.isdir("/var/lib/boinc"):
BOINC_DATA_DIR = "/var/lib/boinc"
else:
BOINC_DATA_DIR = os.path.join(Path.home(), "BOINC/")
elif FOUND_PLATFORM == "Darwin":
BOINC_DATA_DIR = os.path.join("/Library/Application Support/BOINC Data/")
else:
BOINC_DATA_DIR = "C:\\ProgramData\\BOINC\\"
if not GRIDCOIN_DATA_DIR:
if FOUND_PLATFORM == "Linux":
GRIDCOIN_DATA_DIR = os.path.join(Path.home(), ".GridcoinResearch/")
elif FOUND_PLATFORM == "Darwin":
GRIDCOIN_DATA_DIR = os.path.join(
Path.home(), "Library/Application Support/GridcoinResearch/"
)
else:
GRIDCOIN_DATA_DIR = os.path.join(
Path.home(), "AppData\\Roaming\\GridcoinResearch\\"
)
class GridcoinClientConnection:
"""Allows connecting to a Gridcoin wallet and issuing RPC commands.
A class for connecting to a Gridcoin wallet and issuing RPC commands. Currently
quite barebones.
Attributes:
config_file:
ip_address:
rpc_port:
rpc_user:
rpc_password:
retries:
retry_delay:
"""
def __init__(
self,
config_file: str = None,
ip_address: str = "127.0.0.1",
rpc_port: str = "9876",
rpc_user: str = None,
rpc_password: str = None,
retries: int = 3,
retry_delay: int = 1,
):
"""Initializes the instance based on the connection attributes.
Attributes:
config_file:
ip_address:
rpc_port:
rpc_user:
rpc_password:
retries: int = 3,
retry_delay: int = 1,
"""
self.configfile = config_file # Absolute path to the client config file
self.ipaddress = ip_address
self.rpc_port = rpc_port
self.rpcuser = rpc_user
self.rpcpassword = rpc_password
self.retries = retries
self.retry_delay = retry_delay
def run_command(
self, command: str, arguments: List[Union[str, bool]] = None
) -> Union[dict, None]:
"""Send command to local Gridcoin wallet
Sends specifified Gridcoin command to the Gridcoin wallet instance and
retrieves result of the command execution.
Args:
command:
arguments:
Returns:
Response from command exectution as a dictionary of json, or None if
an error was encounted while connecting to the Gridcoin wallet instance.
"""
if not arguments:
arguments = []
current_retries = 0
while current_retries < self.retries:
sleep(self.retry_delay)
current_retries += 1
credentials = None
url = "http://" + self.ipaddress + ":" + self.rpc_port + "/"
headers = {"content-type": "application/json"}
payload = {
"method": command,
"params": arguments,
"jsonrpc": "2.0",
"id": 0,
}
jsonpayload = json.dumps(payload, default=json_default)
if self.rpcuser or self.rpcpassword:
credentials = HTTPBasicAuth(self.rpcuser, self.rpcpassword)
try:
response = requests.post(
url, data=jsonpayload, headers=headers, auth=credentials
)
return_response = response.json()
except Exception:
pass
else:
return return_response
return None
def get_approved_project_urls(self) -> List[str]:
"""Retrieves list of projects appoved for Gridcoin.
Retrieves the list of projects from the local Gridcoin wallet that are
approved for earning Gridcoin.
Returns:
A list of UPPERCASED project URLs using gridcoin command listprojects
"""
return_list = []
all_projects = self.run_command("listprojects")
for projectname, project in all_projects["result"].items():
return_list.append(project["base_url"].upper())
return return_list
class BoincClientConnection:
"""Access to BOINC client configuration files.
A simple class for grepping BOINC config files etc. Doesn't do any RPC communication
Note: Usage of it should be wrapped in try/except clauses as it does not
do any error handling internally.
Attributes:
config_dir:
"""
def __init__(self, config_dir: str = None):
"""Initializes the instance using the Gridcoin wallet configuration location.
Args:
config_dir:
"""
if config_dir is None:
self.config_dir = "/var/lib/boinc-client"
else:
self.config_dir = config_dir # Absolute path to the client config dir
def get_project_list(self) -> List[str]:
"""Retrieve the list of projects supported by the BOINC client
Constructs a list of all projects known by the BOINC client. This may include
more projects than those currently attached to the BOINC client. This may also
not include some projects currently attached, if they are projects not included
with BOINC by default.
Returns: List of project URLs.
"""
project_list_file = os.path.join(self.config_dir, "all_projects_list.xml")
return_list = []
with open(project_list_file, mode="r", encoding="ASCII", errors="ignore") as f:
parsed = xmltodict.parse(f.read())
for project in parsed["projects"]["project"]:
return_list.append(project["url"])
return return_list
def grc_project_name_to_url(
searchname: str, all_projects: Union[Dict[str, Dict[str, Any]], Dict[str, str]]
) -> Union[str, None]:
"""
Convert a project name into its canonical project URL
: param : all_projects putput from listprojects rpc command
"""
for found_project_name, found_project_dict in all_projects.items():
if found_project_name.upper() == searchname.upper():
if isinstance(found_project_dict, str):
return found_project_dict
elif isinstance(found_project_dict, dict):
return found_project_dict["base_url"]
return None
def wait_till_synced(grc_client: GridcoinClientConnection):
"""
A function to WAIT until client is fully synced
:param grc_client:
:return:
"""
from time import sleep
printed = False
while True:
response = grc_client.run_command("getinfo")
if isinstance(response, dict):
sync_status = response.get("result", {}).get("in_sync")
if sync_status == True:
return
sleep(1)
if printed == False:
print("Gridcoin wallet is not fully synced yet. Waiting for full sync...")
printed = True
def combine_dicts(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> None:
"""
Given dict1, dict2, add dict2 to dict1, over-writing anything in dict1.
@param dict1:
@param dict2:
@return: NONE
"""
for k, v in dict2.items():
dict1[k] = v
def resolve_url_boinc_rpc(
url: str,
known_attached_projects: Set[str] = None,
known_attached_projects_dev: Set[str] = None,
known_boinc_projects: List[str] = None,
dev_mode: bool = False,
) -> str:
"""
Given a URL, return the version BOINC is attached to for RPC purposes. Variables aside from dev_mode default to globals if
not passed in.
@param url: A url you want canonicalized
@param known_attached_projects: Projects BOINC is attached to
@param known_boinc_projects: Projects BOINC knows about via default install xml file (or rpc get_all_projects which returns the same)
"""
original_uppered = url.upper()
if "FOLDINGATHOME" in original_uppered:
return url
if not known_attached_projects:
known_attached_projects = ATTACHED_PROJECT_SET
if not known_attached_projects_dev:
known_attached_projects_dev = ATTACHED_PROJECT_SET_DEV
if not known_boinc_projects:
known_boinc_projects = ALL_PROJECT_URLS
# Check quick lookup tables first
if dev_mode:
if original_uppered in LOOKUP_URL_TO_BOINC_DEV:
return LOOKUP_URL_TO_BOINC_DEV[original_uppered]
else:
if original_uppered in LOOKUP_URL_TO_BOINC:
return LOOKUP_URL_TO_BOINC[original_uppered]
# Do full lookup if that doesn't work
uppered = original_uppered.replace("HTTPS://WWW.", "")
uppered = uppered.replace("HTTP://WWW.", "")
uppered = uppered.replace("HTTPS://", "")
uppered = uppered.replace("HTTP://", "")
if uppered.startswith("WWW."):
uppered = uppered.replace("WWW.", "")
if dev_mode:
for known_attached_project in known_attached_projects_dev:
if uppered in known_attached_project.upper():
LOOKUP_URL_TO_BOINC_DEV[original_uppered] = known_attached_project
return known_attached_project
else:
for known_attached_project in known_attached_projects:
if uppered in known_attached_project.upper():
LOOKUP_URL_TO_BOINC[original_uppered] = known_attached_project
return known_attached_project
log.debug(
"{} not in in known attached projects in resolve_url_boinc_rpc".format(
uppered
)
)
for known_boinc_project in known_boinc_projects:
if uppered in known_boinc_project.upper():
return known_boinc_project
log.warning("Unable to resolve URL to BOINC url: {}".format(url))
return url
def resolve_url_list_to_database(url_list: List[str]) -> List[str]:
"""
@param url_list: A list of URLs
@return: The URLs in canonical database format
"""
return_list = []
for url in url_list:
return_list.append(resolve_url_database(url))
return return_list
def shutdown_dev_client(quiet: bool = False) -> None:
"""Shutdown developer BOINC client.
Sends RPC quit command to running dev BOINC client.
Args:
quiet:
Raises:
Exception: An error occured shutting down the dev BOINC client.
"""
# This is needed in case this function is called while main loop is still
# waiting for an RPC command etc
new_loop = asyncio.get_event_loop()
log.info("Attempting to shut down dev client at safe_exit...")
try:
dev_rpc_client = new_loop.run_until_complete(
setup_connection(BOINC_IP, DEV_BOINC_PASSWORD, port=DEV_RPC_PORT)
) # Setup dev BOINC RPC connection
authorize_response = new_loop.run_until_complete(
dev_rpc_client.authorize(DEV_BOINC_PASSWORD)
) # Authorize dev RPC connection
shutdown_response = new_loop.run_until_complete(
run_rpc_command(dev_rpc_client, "quit")
)
except Exception as e:
log.error("Error shutting down dev client {}".format(e))
def safe_exit(arg1, arg2) -> None:
"""Safely exit Find The Mag.
Safely exit tool by saving database, restoring original user preferences,
and quitting dev BOINC client.
Args: arg1 and arg2:
Required by the signal handler library,
but aren't used for anything inside this function
"""
print_and_log(
"Program exiting gracefully. Please be patient this may take a few minutes",
"INFO",
)
# Backup most recent database save then save database to json file
log.debug("Saving database")
shutil.copy("stats.json", "stats.json.backup")
save_stats(DATABASE)
# If BOINC control is not enabled, we can skip the rest of these steps
if not CONTROL_BOINC:
quit()
new_loop = (
asyncio.get_event_loop()
) # This is needed in case this function is called while main loop is still waiting for an RPC command etc
# Shutdown developer BOINC client, if running
if (
not should_crunch_for_dev(False) and CRUNCHING_FOR_DEV or DEV_EXIT_TEST
): # If we are crunching for dev and won't start crunching again on next run
new_loop.run_until_complete(dev_cleanup(rpc_client=None))
shutdown_dev_client()
# Restore crunching settings pre-dev-mode
if CRUNCHING_FOR_DEV:
try:
rpc_client = new_loop.run_until_complete(
setup_connection(BOINC_IP, BOINC_PASSWORD, port=BOINC_PORT)
) # Setup dev BOINC RPC connection
authorize_response = new_loop.run_until_complete(
rpc_client.authorize(BOINC_PASSWORD)
) # Authorize dev RPC connection
new_loop.run_until_complete(
run_rpc_command(rpc_client, "set_gpu_mode", LAST_KNOWN_GPU_MODE)
)
new_loop.run_until_complete(
run_rpc_command(rpc_client, "set_run_mode", LAST_KNOWN_CPU_MODE)
)
except Exception as e:
log.error("Error restoring crunching status in main client {}".format(e))
# Restore original BOINC preferences
if os.path.exists(override_dest_path):
print("Restoring original preferences...")
log.debug("Restoring original preferences...")
try:
shutil.copy(override_dest_path, override_path)
except PermissionError as e:
print("Permission error restoring original BOINC preferences {}".format(e))
log.error(
"Permission error restoring original BOINC preferences {}".format(e)
)
print("Be sure you have permission to edit this file")
print(
"Linux users try 'sudo usermod -aG boinc your_username_here' to fix this error".format(
override_path
)
)
print(
"Note that you will need to restart your machine for these changes to take effect"
)
print(
"MacOS users: This is a known issue, if you find a good fix for it please let us know on Github!"
)
except Exception as e:
print("Error restoring original BOINC preferences {}".format(e))
log.error("Error restoring original BOINC preferences {}".format(e))
print("Be sure you have permission to edit this file")
print(
"Linux users try 'sudo usermod -aG boinc your_username_here' to fix this error".format(
override_path
)
)
print(
"Note that you will need to restart your machine for these changes to take effect"
)
else:
os.remove(override_dest_path)
try:
loop.close()
except Exception as e:
log.error("Error closing an event loop: {}".format(e))
quit()
async def get_stats_helper(rpc_client: libs.pyboinc.rpc_client) -> list:
"""
Return stats from BOINC client. Development on this is stalled due to BOINC not returning all stats + projects in testing.
"""
return_value = []
reply = await run_rpc_command(rpc_client, "get_statistics")
if not reply:
log.error("Error getting boinc stats")
return return_value
if isinstance(reply, str):
log.info("BOINC appears to have no stats... :{}".format(reply))
return return_value
job_logs = await run_rpc_command(rpc_client, "get_old_results")
return reply
async def get_task_list(rpc_client: libs.pyboinc.rpc_client) -> list:
"""List of active, waiting, or paused BOINC tasks.
Return list of tasks from BOINC client which are not completed/failed. These
can be active tasks, tasks waiting to be started, or paused tasks.
Args:
rpc_client:
Returns:
List of BOINC tasks.
"""
# Known task states
# 2: Active
return_value = []
reply = await run_rpc_command(rpc_client, "get_results")
if not reply:
log.error("Error getting boinc task list")
return return_value
if isinstance(reply, str):
log.info("BOINC appears to have no tasks...")
return return_value
for task in reply:
if task["state"] in [2]:
return_value.append(task)
else:
log.warning(
"Warning: Found unknown task state {}: {}".format(task["state"], task)
)
return return_value
async def is_boinc_crunching(rpc_client: libs.pyboinc.rpc_client) -> bool:
"""Check if BOINC is actively crunching tasks.
Queries BOINC client as to crunching status. Returns True is BOINC client
is crunching, false otherwise.
Args:
rpc_client:
Returns:
True if crunching, or False if not crunching or unsure.
Raises:
Exception: An error occured attempting to check the BOINC client crunching status.
"""
try:
reply = await run_rpc_command(rpc_client, "get_cc_status")
task_suspend_reason = int(reply["task_suspend_reason"])
if task_suspend_reason != 0:
# These are documented at
# https://github.com/BOINC/boinc/blob/73a7754e7fd1ae3b7bf337e8dd42a7a0b42cf3d2/android/BOINC/app/src/main/java/edu/berkeley/boinc/utils/BOINCDefs.kt
log.debug(
"Determined BOINC client is not crunching task_suspend_reason: {}".format(
task_suspend_reason
)
)
return False
if task_suspend_reason == 0:
log.debug(
"Determined BOINC client is crunching task_suspend_reason: {}".format(
task_suspend_reason
)
)
return True
log.warning("Unable to determine if BOINC is crunching or not, assuming not.")
return False
except Exception as e:
print(
"Error checking if BOINC is crunching. If you continue to see this error, make sure BOINC is running"
)
log.error(
"Error checking if BOINC is crunching (in is_boinc_crunching: ".format(e)
)
return False
async def setup_connection(
boinc_ip: str = BOINC_IP, boinc_password: str = BOINC_PASSWORD, port: int = 31416
) -> Union[libs.pyboinc.rpc_client.RPCClient, None]:
"""Create BOINC RPC client connection.
Sets up a BOINC RPC client connection
Args:
boinc_ip:
boinc_password:
port:
Returns:
"""
rpc_client = None
if not boinc_ip:
boinc_ip = "127.0.0.1"
rpc_client = await init_rpc_client(boinc_ip, boinc_password, port=port)
return rpc_client
def temp_check() -> bool:
"""Checks if temperature is within acceptable limit.
Confirms if we should keep crunching based on temperature, or not.
Returns:
True if we should keep crunching, False otherwise.
Raises:
Exception: An error occured attempting to read the temperature.
"""
if not ENABLE_TEMP_CONTROL:
return True
text = ""
if TEMP_URL:
import requests as req
try:
text = req.get(TEMP_URL).text
except Exception as e:
print_and_log("Error checking temp: {}".format(e), "ERROR")
return True
elif TEMP_COMMAND:
command = shlex.split(TEMP_COMMAND)
try:
text = subprocess.check_output(command).decode()
except Exception as e:
print_and_log("Error checking temp: {}".format(e), "ERROR")
return True
command_output = TEMP_FUNCTION()
match = None
if command_output:
text = str(command_output)
match = re.search(TEMP_REGEX, text)
if text:
match = re.search(TEMP_REGEX, text)
if match:
try:
found_temp = int(match.group(0))
log.debug("Found temp {}".format(found_temp))
if found_temp > STOP_TEMP or found_temp < START_TEMP:
return False
except Exception as e:
print("Error parsing temp {} {}".format(match, e))
return True
else:
print("No temps found!")
log.error("No temps found!")
return True
return True
def update_fetch(
update_text: str = None, current_ver: float = None
) -> Tuple[bool, bool, Union[str, None]]:
"""Check if FindTheMag updates are avialable.
Check with FindTheMag repository on GitHub whether or not an update is
available. If avaialble, inform the user and provide some information.
Update checks are performed no often then once per week. Check times are
stored in the database for future reference.
Args:
update_text: Used for testing purposes. Default: None
current_ver: Added for testing purposes. Default: None
Returns:
A tuple consisting of:
A bool, set to True if and update is available.
A bool, set to True if the update is a security update.
A string containing update related information.
Raises:
Exception: An error occured when attempting to parse the retrieved update file.
"""
update_return = False
return_string = ""
security_update_return = False
# Added for testing purposes
if update_text:
resp = update_text
else:
resp = None
if not current_ver:
current_ver = VERSION
# If we've checked for updates in the last week, ignore
delta = datetime.datetime.now() - DATABASE.get(
"LASTUPDATECHECK", datetime.datetime(1997, 3, 3)
)
if abs(delta.days) < 7:
return False, False, None
# Get update status from Github
if not resp:
import requests as req
headers = req.utils.default_headers()
headers.update(
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36",
}
)
url = "https://raw.githubusercontent.com/makeasnek/FindTheMag2/main/updates.txt"
try:
resp = req.get(url, headers=headers).text
except Exception as e:
DATABASE["TABLE_STATUS"] = "Error checking for updates {}".format(e)
log.error("Error checking for updates {}".format(e))
return False, False, None
if "UPDATE FILE FOR FINDTHEMAG DO NOT DELETE THIS LINE" not in resp:
DATABASE["TABLE_STATUS"] = "Error checking for updates invalid update file"
log.error("Error checking for updates invalid update file")
return False, False, None
try:
for line in resp.splitlines():
if line.startswith("#"):
continue
if line == "":
continue
if "," not in line:
continue
split = line.split(",")
version = float(split[0])
if split[1] == "1":
security = True
else:
security = False
notes = split[2]
if version > current_ver:
if security:
security_update_return = True
update_return = True
return_string = (
return_string
+ "Version {} available. This is an important security update. Changes include {}\n".format(
version, notes
)
)
else:
update_return = True
return_string = (
return_string
+ "Version {} available. Changes include {}\n".format(
version, notes
)
)
except Exception as e:
log.error("Error parsing update file")
DATABASE["LASTUPDATECHECK"] = datetime.datetime.now()
if return_string == "":
return_string = None
return update_return, security_update_return, return_string
def update_check() -> None:
"""Check if FindTheMag updates are avialable.
Check for updates to the FindTheMag tool and logs information on any updates found.
"""
available, security, print_me = update_fetch()
if available:
print_and_log(print_me, "INFO")
def get_grc_price(sample_text: str = None) -> Union[float, None]:
"""
Gets average GRC price from three online sources. Returns None if unable to determine
@sample_text: Used for testing. Just a "view source" of all pages added together
"""
"""Retrieve current average Gridcoin price.
Calculates the average GRC price based on values from three online sources.
Note: Retrieving the prices is dependent on the target website formatting. If the
source website changes significantly, retrieval may fail until the relevant
search pattern in updated.
Args:
sample_text: Used for testing.
Typicaly a "view source" of all pages added together.
Returns:
Average GCR price in decimal, or None if unable to determine price.
Raises:
Exception: An error occurred accessing an online GRC price source.
"""
price, table_message, url_messages, info_log_messages, error_log_messages = get_grc_price_from_sites()
for log_message in info_log_messages:
log.info(log_message)
for log_message in error_log_messages:
log.error(log_message)
if price:
DATABASE["TABLE_STATUS"] = table_message
for url_message in url_messages:
print_and_log(url_message, "ERROR")
return price
DATABASE["TABLE_STATUS"] = table_message
for url_message in url_messages:
print_and_log(url_message, "ERROR")
return DATABASE.get("GRCPRICE", 0)
def get_approved_project_urls_web(query_result: str = None) -> Dict[str, str]:
"""List of projects currently witelised by Gridcoin.
Gets current whitelist from the Gridcoinstats website. Limits fetching
from website to once every 24 hours through caching list in database.