-
Notifications
You must be signed in to change notification settings - Fork 5
/
ckPoolNotify.py
executable file
·1082 lines (912 loc) · 43.7 KB
/
ckPoolNotify.py
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
#!/usr/bin/env python
"""Script used to monitor the CK Solo bitcoin mining pool."""
################################################################################
#
# File: ckPoolNotify.py
#
# Contains: This script monitors the CK Solo pool, emailing the caller
# with status changes.
#
# Currently this script only monitors changes to the best shares
# submitted by the specified workers or users. An email will
# be sent if any monitored best share improves.
#
# See the help documentation for details on using this script:
#
# ckPoolNotify.py --help
#
# Written by: edonkey, September 15, 2015
#
# Donations: 37HQFRi9qCPaWKmaD4R5F5kbLqHA1yuQyV
#
################################################################################
import os
import sys
import signal
import time
import datetime
import urlparse
import json
import requests
import keyring
import smtplib
import email
import pickle
import getpass
from email.MIMEMultipart import MIMEMultipart
from email.mime.text import MIMEText
from os.path import expanduser
from optparse import OptionParser
# Globals
gDebug = False
gVerbose = False
gQuiet = False
gSeparator = "----------------------------------------------------------------------------------------------------"
# Globals used to test the block finding code. Should be left off unless you're developing or
# testing this script
gDebugPretendWeFoundABlock = False
gDebugFakeFoundAddress = None
# This system name will appear in the platform keyring.
gKeyringSystem = "ckPoolNotify"
# Defaults
gDefaultPoolUrl = "http://solo.ckpool.org"
gDefaultCkSoloPoolFeeAddress = "1PKN98VN2z5gwSGZvGKS2bj8aADZBkyhkZ"
# Number of minutes between checks to see if a block was found
gDefaultBlockCheckMinutes = 5
gDefaultDifficultyUrl = "https://blockexplorer.com/q/getdifficulty"
gDefaultDifficultyJsonKey = "difficulty"
gDefaultSmptServer = "smtp.gmail.com:587"
gDefaultMonitorSleepSeconds = 90
gDefaultDateTimeStrFormat = "%Y-%m-%d %H:%M:%S"
# Boolean expression dictionary
gBooleanExpressionDict = {
"on": True,
"off": False,
"true": True,
"false": False,
"yes": True,
"no": False,
"1": True,
"0": False,
}
# Get the name and path to our script
gScriptPathArg = sys.argv[0]
gScriptName = os.path.basename(gScriptPathArg)
# Get the user's home directory
gHomeDir = expanduser("~")
# Define the file where we'll store the current stats dictionary. We do this conditionally
# based on the platform. For all platforms other than Windows, we use the dot char prefix to make
# the file invisible.
# TODO: For windows, consider setting the file to be invisible.
if sys.platform == "win32":
gSavedStatsFilePath = os.path.join(gHomeDir, "ckPoolNotify_SavedStats")
else:
gSavedStatsFilePath = os.path.join(gHomeDir, ".ckPoolNotify_SavedStats")
#---------------------------------------------------------------------------------------------------
def stringArgCheck(arg):
return (arg != None) and \
(len(arg) > 0 ) and \
(arg != "") and \
(arg != '') and \
(arg != "\"\"")
#---------------------------------------------------------------------------------------------------
def exitFail(message="", exitCode=1):
if stringArgCheck(message):
sys.stderr.write(message + "\n")
sys.exit(exitCode)
#---------------------------------------------------------------------------------------------------
def signalHandler(signal, frame):
print('')
print('Exiting...')
sys.exit(0)
#---------------------------------------------------------------------------------------------------
# Get the current date/time in the specified format
def getNowStr(format=gDefaultDateTimeStrFormat):
return datetime.datetime.now().strftime(format)
#---------------------------------------------------------------------------------------------------
# Print the specified strings, prepending with the current date/time
def p(*args):
line = getNowStr() + ": ";
for arg in args:
line = " ".join([line, str(arg)])
print line
return
#---------------------------------------------------------------------------------------------------
# Evaluate the specified boolean expression string into a boolean value. Also returns whether or
# not a valid, known boolean expression string was provided
def evaluateBoolExpression(boolExpression):
expressionValue = False
validExpression = False
boolExpressionLower = boolExpression.lower()
if boolExpressionLower in gBooleanExpressionDict:
validExpression = True
expressionValue = gBooleanExpressionDict[boolExpressionLower]
return (expressionValue, validExpression)
#---------------------------------------------------------------------------------------------------
# Build up a list of valid expressions dynamically from the dictionary
def getValidBoolExpresionsStr():
validExpressions = ""
for key in gBooleanExpressionDict:
if len(validExpressions) > 0:
validExpressions += ", "
validExpressions = validExpressions + key
return validExpressions
#---------------------------------------------------------------------------------------------------
def exitFailBadBooleanExpression(message, badExpression):
errorMessage = message + ": \"" + badExpression + "\". Valid boolean expressions include: " + getValidBoolExpresionsStr()
exitFail(errorMessage)
#---------------------------------------------------------------------------------------------------
def setPassword(user, password):
if not stringArgCheck(password):
exitFail("You have to specify an actual password.")
# Save the password in the keychain
if gDebug: print("Saving the password to the keychain under this sender: \"" + user + "\"")
keyring.set_password(gKeyringSystem, user, password)
#---------------------------------------------------------------------------------------------------
def setOrGetPassword(user, passwordSpecified):
# If a password was specified, then use it. Otherwise get the password out of the keychain.
password = ""
if passwordSpecified:
password = passwordSpecified
setPassword(user, password)
else:
password = keyring.get_password(gKeyringSystem, user)
if not stringArgCheck(password):
print("No password found in the keychain for this sender: \"" + user + "\"")
exitFail("You must specify a password at least once in order to store it in the keychain for this user.")
return password
#---------------------------------------------------------------------------------------------------
def getCurrentDifficulty(getDifficultyUrl=gDefaultDifficultyUrl, difficultyKey=gDefaultDifficultyJsonKey):
# Default the difficulty to zero (yeah, you wish!) in case we fail to get it from the web
curDifficulty = 0.0
try:
if gDebug: print("Attempting to get the current difficulty from this URL: \"" + getDifficultyUrl + "\", and this key: " + difficultyKey)
# Get the JSON result from the difficulty provider URL
r = requests.get(getDifficultyUrl)
status = r.status_code
r.raise_for_status()
data = r.json()
if gDebug: print(" JSON returned: " + str(data))
# Get the difficulty value from the JSON data retrned
curDifficulty = data[difficultyKey]
if gDebug: print(" curDifficulty: " + str(curDifficulty))
except requests.exceptions.ConnectionError, e:
print('Could not get difficulty due to a connection Error.')
except Exception, e:
print('Fetching data failed: %s' % str(e))
return curDifficulty
#---------------------------------------------------------------------------------------------------
def wasABlockFound(lastBlock, poolFeeAddress=gDefaultCkSoloPoolFeeAddress):
# Initialize the return values
newBlock = 0
blockFinderAddress = ""
# Assemble the pool fee address URL. If there's a new input to this address, it means
# the pool found a block. Also, the other input will be the block finder's address.
poolFeeAddressUrl = "https://blockchain.info/address/" + poolFeeAddress + "?format=json"
try:
if gDebug: print("Looking for a payout to the pool fee address: \"" + poolFeeAddress + "\"")
response = requests.get(poolFeeAddressUrl)
data = response.json()
blockNumberFound = data['txs'][0][u'block_height']
if gDebug:
print(" Found this block number: " + str(blockNumberFound))
# HACK TEST to fake out a found block.
if gDebugPretendWeFoundABlock:
print (" Pretend we found a block by hacking the last block number.")
lastBlock = blockNumberFound - 1
# Check to see if this is a new block
if blockNumberFound > lastBlock:
newBlock = blockNumberFound
blockFinderAddress = data['txs'][0][u'out'][0][u'addr']
if gDebug:
print(" And this block finder: " + blockFinderAddress)
elif gDebug:
print(" This is not a new block. Bummer...")
except requests.exceptions.ConnectionError, e:
p("Connection Error. Will retry later.." )
status = -2
except Exception, e:
p("Fetching data failed: %s" % str(e))
status = -2
return (newBlock, blockFinderAddress)
#---------------------------------------------------------------------------------------------------
class EmailServer:
#---------------------------------------------------------------------------
# Default constructor
def __init__(self, serverUrl, user, password):
# Initialize the member variables with defaults
self.serverUrl = serverUrl
self.user = user
self.password = password
#---------------------------------------------------------------------------
def send(self, sender, recipients, subject, body, printEmail=False):
didSend = False
recipientList = recipients if type(recipients) is list else [recipients]
# If running in verbose mode or if the caller wants us to print the email,
# then print it out now
if gVerbose or printEmail:
print(gSeparator)
p("Sending an email:")
print(" Sender: " + sender)
print(" Recipients: " + str(recipients))
print(" Subject: " + subject)
print(" Body:\n\n" + body)
print(gSeparator)
print("")
# Prepare actual message
message = email.MIMEMultipart.MIMEMultipart()
message['From'] = sender
message['To'] = email.Utils.COMMASPACE.join(recipientList)
message['Subject'] = subject
message.attach(MIMEText(body, 'plain'))
try:
smtp = smtplib.SMTP(self.serverUrl)
smtp.ehlo()
# If a user and password were specified, then perform authentication
if stringArgCheck(self.user) and stringArgCheck(self.password):
smtp.starttls()
smtp.login(self.user, self.password)
# Send the email
smtp.sendmail(sender, recipientList, message.as_string())
# Shut down the server
smtp.quit()
# Remember that we succeeded
didSend = True
except Exception, err:
print "Failed to send mail:", err
return didSend
#---------------------------------------------------------------------------------------------------
# This class saves status information for user and worker URLs to a file. The file is actually
# a pickled dictionary where the key is the status URL and the value is the JSON dictionary
# returned from the pool API.
#
# Rather than just storing the bestshare (as a previous iteration of this script did), storing the
# entire JSON dictionary for monitored URLs should allow us to add new monitoring features without
# changing the file format of the stats data.
class SavedStats:
#---------------------------------------------------------------------------
# Default constructor
def __init__(self, path):
# Initialize the member variables with defaults
self.path = path
self.statsDict = None
self.lastBlock = 0
self.restore()
# If we didn't restore a stats dictionary, then instance a new one
if not self.statsDict:
if gDebug: print("Couldn't find saved stats data. Initializing a new dictionary...")
self.statsDict = {}
#---------------------------------------------------------------------------
def restore(self):
if os.path.exists(self.path) and (0 != os.path.getsize(self.path)):
if gDebug: print("Reading the saved saved stats dictionary from here: " + self.path)
try:
file = open(self.path, "rb")
file.seek(0, 0)
unpickled = pickle.load(file)
self.statsDict = unpickled["userStats"]
if "lastBlock" in unpickled:
self.lastBlock = unpickled["lastBlock"]
file.close()
if gDebug: print(" Restored these stats key/values:" + str(self.statsDict))
except Exception, err:
print "Exception trying to access the saved saved stats data file:", err
#---------------------------------------------------------------------------
def save(self):
if gDebug: print("Writing the saved saved stats dictionary from here: " + self.path)
try:
file = open(self.path, "a+b")
file.seek(0, 0)
file.truncate()
dictToPickle = {"userStats": self.statsDict, "lastBlock": self.lastBlock}
pickle.dump(dictToPickle, file)
file.close()
except Exception, err:
print "Exception trying to save the saved stats data file:", err
#---------------------------------------------------------------------------------------------------
def getLastUpdateTimeFromStatsJson(statsJson, localTime=False):
# Set default values in case we can't find a given hash rate in the stats
lastUpdateTime = None
try:
lastUpdateSecs = statsJson['lastupdate']
if localTime:
lastUpdateTime = time.localtime(lastUpdateSecs)
else:
lastUpdateTime = time.gmtime(lastUpdateSecs)
except Exception, e:
errorStr = "Fetching data failed: " + str(e)
p(errorStr)
return lastUpdateTime
#---------------------------------------------------------------------------------------------------
def getHashRatesFromStatsJson(statsJson):
# Set default values in case we can't find a given hash rate in the stats
hashRate5m = "?"
hashRate1hr = "?"
hashRate1d = "?"
hashRate7d = "?"
shares = "?"
# Get last hash rates out of the specified JSON
try:
hashRate5m = statsJson['hashrate5m']
except Exception, e:
errorStr = "Fetching data failed: " + str(e)
p(errorStr)
try:
hashRate1hr = statsJson['hashrate1hr']
except Exception, e:
errorStr = "Fetching data failed: " + str(e)
p(errorStr)
try:
hashRate1d = statsJson['hashrate1d']
except Exception, e:
errorStr = "Fetching data failed: " + str(e)
p(errorStr)
try:
hashRate7d = statsJson['hashrate7d']
except Exception, e:
errorStr = "Fetching data failed: " + str(e)
p(errorStr)
try:
shares = statsJson['shares']
except Exception, e:
errorStr = "Fetching data failed: " + str(e)
p(errorStr)
return (hashRate5m, hashRate1hr, hashRate1d, hashRate7d, shares)
#---------------------------------------------------------------------------------------------------
def getUserAndWorkersFromURLs(listUrls):
listedUsers = []
listedWorkers = []
for curListUrl in listUrls:
try:
if gDebug: print("Attempting to get the user/workers list from this URL: \"" + curListUrl + "\"")
# Get the text result from the list URL
r = requests.get(curListUrl)
listText = r.text
if gDebug: print(" Text returned: " + listText)
# Split the text into lines, then evaluate each one. Attempts to deal with
# URLs as well as simple addresses
listLines = listText.splitlines()
for curListLine in listLines:
curLine = curListLine.strip()
if stringArgCheck(curLine):
# Ignore the line if it's a comment
if curLine[0] != "#":
# If the line has illegal characters (like might happen when DropBox
# fails and returns an HTML formatted error), then consider the whole
# file as bad data, clear any found users or workers, and throw an
# exception.
illegalChars = set('<>')
if any((c in illegalChars) for c in curLine):
listedUsers = []
listedWorkers = []
raise ValueError("Ignoring the file at this URL because illegal characters were detected: \"" + curListUrl + "\"")
else:
# See if we're dealing with a URL
curAddress = curLine.split("/")[-1]
if len(curAddress) > 0:
if "." in curAddress:
listedWorkers.append(curAddress)
else:
listedUsers.append(curAddress)
except requests.exceptions.ConnectionError, e:
print("Could not get this user/worker list due to a connection Error:: \"" + curListUrl + "\"")
except ValueError, e:
print("Bad data read: %s" % str(e))
except Exception, e:
print("Unexpected exception: %s" % str(e))
return (listedUsers, listedWorkers)
#---------------------------------------------------------------------------------------------------
def monitorPool(poolUrls, workers, users, listUrls, sleepSeconds, emailServer, sender, recipients, doBestShareNotification=True, doShowHashRate=True, notifyTime=None):
# Build up a list of URLs to monitor
urlsToMonitor = []
# Add in any explicit pool URLs
if poolUrls and len(poolUrls > 0):
urlsToMonitor.extend(poolUrls)
# Initialize an array of monitored addresses. This cache of addresses will be used when
# a block is found to see if the winner was one of the monitored addresses.
monitoredAddresses = []
# Construct any worker URLs
if workers and len(workers) > 0:
for curWorker in workers:
curWorkerUrl = urlparse.urljoin(gDefaultPoolUrl + "/workers/", curWorker)
if curWorkerUrl not in urlsToMonitor:
urlsToMonitor.append(curWorkerUrl)
# Split off the worker name from the address and add the address to the list
# of monitored addresses
curWorkerAddress = curWorker.split(".", 1)[0]
if curWorkerAddress not in monitoredAddresses:
monitoredAddresses.append(curWorkerAddress)
# Construct any user URLs
if users and len(users) > 0:
for curUser in users:
curUserUrl = urlparse.urljoin(gDefaultPoolUrl + "/users/", curUser)
if curUserUrl not in urlsToMonitor:
urlsToMonitor.append(curUserUrl)
if curUser not in monitoredAddresses:
monitoredAddresses.append(curUser)
# We need at least one URL to monitor
callerProvidedListUrls = True
if (listUrls and (len(listUrls) > 0)):
callerProvidedListUrls = True
if (len(urlsToMonitor) == 0) and not callerProvidedListUrls:
exitFail("You need at least one pool URL to monitor.")
if gDebug:
print("monitoredAddresses: " + str(monitoredAddresses))
# Initialize the dictionary that will keep track of the saved stats.
# First we look to see if we have a saved dictionary of best shares in a file.
savedStats = SavedStats(gSavedStatsFilePath)
# If we haven't initialized the last block found by the pool, do so now and
# save the stats to disk. This way we can detect when a new block has been found.
lastFoundBlockCheck = datetime.datetime.now()
if savedStats.lastBlock == 0:
(savedStats.lastBlock, ignoreAddress) = wasABlockFound(lastBlock=0)
if savedStats.lastBlock != 0:
savedStats.save()
# If any URLs that we wan't to monitor are not in the dictionary, add a skeleton
# dictionary for it now with a zero best share.
for curUrl in urlsToMonitor:
if curUrl not in savedStats.statsDict:
savedStats.statsDict[curUrl] = { "bestshare": 0.0 }
# Initialize the explicit notification date to nothing for now
nextNotifyDate = None
# Main monitor loop
if gVerbose:
p("Monitor starting...")
while True:
# If the caller specified a notification time and we have not yet computed the next date
# when we will notify, then compute that now.
if notifyTime and not nextNotifyDate:
now = datetime.datetime.now()
nextNotifyDate = datetime.datetime.combine(now, notifyTime)
if nextNotifyDate < now:
nextNotifyDate += datetime.timedelta(days=1)
# If the caller provided a URLs to lists of users or workers, then try to get the lists now.
if callerProvidedListUrls:
(listedUsers, listedWorkers) = getUserAndWorkersFromURLs(listUrls)
for curUser in listedUsers:
curUserUrl = urlparse.urljoin(gDefaultPoolUrl + "/users/", curUser)
if curUserUrl not in urlsToMonitor:
urlsToMonitor.append(curUserUrl)
if curUser not in monitoredAddresses:
monitoredAddresses.append(curUser)
for curWorker in listedWorkers:
curWorkerUrl = urlparse.urljoin(gDefaultPoolUrl + "/workers/", curWorker)
if curWorkerUrl not in urlsToMonitor:
urlsToMonitor.append(curWorkerUrl)
# Split off the worker name from the address and add the address to the list
# of monitored addresses
curWorkerAddress = curWorker.split(".", 1)[0]
if curWorkerAddress not in monitoredAddresses:
monitoredAddresses.append(curWorkerAddress)
# If any URLs that we wan't to monitor are not in the dictionary, add a skeleton
# dictionary for it now with a zero best share.
for curUrl in urlsToMonitor:
if curUrl not in savedStats.statsDict:
savedStats.statsDict[curUrl] = { "bestshare": 0.0 }
# If after getting the lists we have no URLs to monitor, let the user know.
if len(urlsToMonitor) == 0:
p("What? The worker list URLs provided did not provide any workers or users.")
newBestShares = None
for curUrl in urlsToMonitor:
try:
if gDebug: print("Monitor attempting to contact this pool URL: " + curUrl)
# Get the JSON result from the current URL
r = requests.get(curUrl)
status = r.status_code
r.raise_for_status()
data = r.json()
if gDebug: print(" JSON returned: " + str(data))
# Get the stats for the user from the JSON
curBestShare = data['bestshare']
savedUrlStatsDict = savedStats.statsDict[curUrl]
savedBestShare = savedUrlStatsDict['bestshare']
# If the best share for the URL is greater than what we remember, then add it
# to our dictionary of new best shares, which we will report to the caller.
if curBestShare > savedBestShare:
if doBestShareNotification:
if newBestShares == None:
newBestShares = {}
newBestShares[curUrl] = curBestShare
else:
if gDebug: print(" Caller has disabled best share notification.")
# Remember the new JSON dictionary in the saved stats
savedStats.statsDict[curUrl] = data
except requests.exceptions.ConnectionError, e:
p("Connection Error. Retrying in %i seconds" % sleepSeconds)
status = -2
except Exception, e:
curStatsAddress = curUrl.split("/")[-1]
p("Fetching data for \"" + curStatsAddress + "\" failed: " + str(e))
status = -2
if status == 401:
print (getNowStr() + ": You are not authorized to access the JSON interface for this URL: " + curUrl)
# If it's time to see if the pool found a block, then check now
newBlock = 0
foundAddress = None
foundAddressIsOneOfOurs = False
if datetime.datetime.now() >= (lastFoundBlockCheck + datetime.timedelta(minutes = gDefaultBlockCheckMinutes)):
if gDebug: p("Checking to see if the pool found a block...")
lastFoundBlockCheck = datetime.datetime.now()
(newBlock, foundAddress) = wasABlockFound(lastBlock=savedStats.lastBlock)
# HACK TEST to fake out a found block.
if gDebugPretendWeFoundABlock:
if gDebugFakeFoundAddress:
print (" Pretend we found a block by changing the found address to this test address: " + gDebugFakeFoundAddress)
foundAddress = gDebugFakeFoundAddress
else:
print (" Pretend we found a block by changing the found address to one of our monitored ones.")
foundAddress = monitoredAddresses[0]
# If a new block was found, remember it in our stats (which will be saved below)
if newBlock != 0:
savedStats.lastBlock = newBlock
# Keep track of email sections so that we can separate them
emailSectionCount = 0
# If the caller specified a notification date and we've hit it, then we need to
# force notification.
forceNotify = False
if nextNotifyDate:
if datetime.datetime.now() >= nextNotifyDate:
if gDebug: p("Time to force daily notification: " + str(nextNotifyDate))
# Remember that we want to force notification, and zero out the notify
# date so that it will be recomputed at the top of the loop.
forceNotify = True
nextNotifyDate = None
# If we have new best shares, notify the user and remember the changed stats.
newBestSharesFound = False
if newBestShares and (len(newBestShares) > 0):
newBestSharesFound = True
if forceNotify or (newBlock != 0) or newBestSharesFound:
# Save the updated stats
savedStats.save()
# Build up the body of the email text.
subject = "CK Solo Pool: "
appendStr = ""
body = ""
# If we're forcing notification now, then append to the subject
if forceNotify:
subject = subject + appendStr + "Daily notification"
appendStr = " & "
# See if a new block was found
newBlockWasFound = False
if (newBlock != 0) and stringArgCheck(foundAddress):
newBlockWasFound = True
# If a block was found, then add that info the the email notification
if (newBlock != 0) and stringArgCheck(foundAddress):
p("New block found: " + str(newBlock))
appendStr = " & "
if gDebugPretendWeFoundABlock:
subject = "TEST - " + subject
subject = subject + "New Block found"
# Add a section separator as needed.
if emailSectionCount != 0:
body = body + "\n" + gSeparator + "\n"
emailSectionCount = emailSectionCount + 1
if gDebugPretendWeFoundABlock:
body = body + "IMPORTANT! A block was NOT actually found. This email is just a test.\n"
body = body + "\n"
# Build up the block found section of the email
body = body + "This lucky address found block number " + str(newBlock) + ":\n\n"
body = body + foundAddress + "\n"
body = body + "\n"
# If the address that found the block is one of ours, then this is a big day!
if foundAddress in monitoredAddresses:
foundAddressIsOneOfOurs = True
body = body + "OMG! That's one of your monitored addresses!\n\n"
body = body + "If it was your address, congratulations! You should go celebrate!\n"
else:
body = body + "Unfortunately that was not one of your monitored addresses. Better luck next time...\n"
# If we found new best shares, add that info to the subject and body of the email
if newBestSharesFound:
p("New best share found!")
subject = subject + appendStr + "New best share found"
appendStr = " & "
# Add a section separator as needed.
if emailSectionCount != 0:
body = body + "\n" + gSeparator + "\n"
emailSectionCount = emailSectionCount + 1
body = body + "New best share stats for monitored addresses:\n\n"
# Try to get the current difficulty to include in the email. If we got it, the
# value will be non-zero.
curDifficulty = getCurrentDifficulty()
# If we know the current difficulty, put it at the top for reference
if curDifficulty != 0.0:
body = body + "Current difficulty: " + str(curDifficulty) + "\n"
# Loop through the new best shares indicating their stats URL, value, and percentage
# of the current difficulty. Sort the URLs in the dictionary so that there's a consistent order
# in the email.
sortedBestSharesUrls = sorted(newBestShares, key=lambda s: s.lower())
for curUrl in sortedBestSharesUrls:
curValue = newBestShares[curUrl]
if len(body) > 0:
body = body + "\n"
curStatsAddress = curUrl.split("/")[-1]
body = body + " " + curStatsAddress + ":\n"
body = body + " New best share: " + str(newBestShares[curUrl]) + "\n"
if (curValue != 0.0) and (curDifficulty != 0.0):
percentOfDifficulty = (curValue / curDifficulty) * 100
body = body + " Percent of difficulty: " + str(percentOfDifficulty) + "%\n"
# If the found address is one that we monitor, and if we're supposed to display the
# current hash rate, find all the monitored workers or users (by partial match)
# and include their hashrate in the email
if doShowHashRate and (foundAddressIsOneOfOurs or newBestSharesFound or forceNotify):
# Add a section separator as needed.
if emailSectionCount != 0:
body = body + "\n" + gSeparator + "\n"
emailSectionCount = emailSectionCount + 1
body = body + "Hash rates of monitored addresses:\n\n"
# We want to show the hash rates of the monitored addresses if a block was
# found and it was one of our addresses, or if there was a new best share.
# Build up a sorted list of URLs or addresses that we care about first.
urlsToReport = []
# Loop through the monitored addresses looking for any match for the found address
for curUrl in urlsToMonitor:
# If we're doing a daily notification, then all the monitored URLs are
# interesting to us.
if forceNotify:
for curUrl in urlsToMonitor:
if curUrl not in urlsToReport:
urlsToReport.append(curUrl)
else:
# If we get here, we're not doing daily notification, so we have to decide
# what URLs we're interested in.
#
# Add all monitored URLs that contain the found address to the URL list we
# want to report hashrate for.
if foundAddress:
for curUrl in urlsToMonitor:
if foundAddress in curUrl:
if curUrl not in urlsToReport:
urlsToReport.append(curUrl)
# Add all best share URLs to the list to report
if newBestSharesFound:
for key, value in newBestShares.iteritems():
if key not in urlsToReport:
urlsToReport.append(key)
# Sort the list
urlsToReport = sorted(urlsToReport, key=lambda s: s.lower())
if gDebug: print("urlsToReport : " + str(urlsToReport))
# Get the hash rate for each address in our sorted list and add it to the
# email body
for curUrl in urlsToReport:
curAddress = curUrl.split("/")[-1]
if gDebug:
print("Getting hash rates from saved stats for this URL: " + curUrl)
print(" and this address: " + curAddress)
body = body + " " + curAddress + ":\n"
curStatsDict = savedStats.statsDict[curUrl]
# Get the last update time from the stats
curLastUpdateTimeStr = "Unknown"
curLastUpdateTime = getLastUpdateTimeFromStatsJson(curStatsDict)
if curLastUpdateTime:
curLastUpdateTimeStr = time.strftime('%Y-%m-%d %H:%M:%S', curLastUpdateTime)
body = body + " Updated: " + curLastUpdateTimeStr + "\n"
# Get the hash rates from the saved stats
(hashRate5m, hashRate1hr, hashRate1d, hashRate7d, shares) = getHashRatesFromStatsJson(curStatsDict)
# Add the hashrates to the email body
body = body + " 5 minute: " + hashRate5m + "\n"
body = body + " 1 hour: " + hashRate1hr + "\n"
body = body + " 5 day: " + hashRate1d + "\n"
body = body + " 7 days: " + hashRate7d + "\n"
body = body + " Shares: " + str(shares) + "\n"
body = body + "\n"
body = body + "\n"
# Send the email. If a block was found for our address, then print the email to
# standard out so that we have a record of it in case the email fails to send.
if gDebug or gVerbose:
p("Sending the new notification email...")
if newBlockWasFound:
subject = subject + "!"
success = emailServer.send(sender, recipients, subject, body, printEmail=foundAddressIsOneOfOurs)
if not success:
p(" Could not send the notification email!")
elif gDebug or gVerbose:
p(" Email sent!")
# Clear the new best shares dictionary for the next time through the loop
newBestShares = None
# Sleep waiting for the next time to monitor
time.sleep(sleepSeconds)
#---------------------------------------------------------------------------------------------------
# Script starts here
#---------------------------------------------------------------------------------------------------
# Establish our signal handler
signal.signal(signal.SIGINT, signalHandler)
# Disable annoying InsecurePlatformWarning warnings. Since we only access known URLs, ignoring
# these warnings should be fine.
requests.packages.urllib3.disable_warnings()
usage="""ckPoolNotify.py [OPTIONS]"""
description="""This script monitors the CK Solo pool, emailing the caller with status changes.
Currently this script monitors the best shares submitted by specified workers or users. If the
best shares improve from historic values saved by this script, an email is sent to the specified
recipients. If you want this script to send from an authenticated email server, then the best way
to get started is to set your password and send a test email. For example: \"./ckPoolNotify.py --user
<your email address> --setpassword --test\" Once you've successfully received the test email, you
can run the script in normal monitor mode."""
# Initialize the options parser for this script
parser = OptionParser(usage=usage, description=description)
parser.set_defaults(verbose=False, debug=False, server=gDefaultSmptServer, bestshare=None, showhashrate=None, sleepseconds=gDefaultMonitorSleepSeconds, clear=False, fakefoundaddress=None)
parser.add_option("--verbose",
action="store_true", dest="verbose",
help="Verbose output from this script, and from wraptool.")
parser.add_option("-W", "--setpassword",
action="store_true", dest="setpassword",
help="If specified, then the password used to authenticate the user for sending emails will be requested and saved in the user's keychain. This option prevents the password from being seen in the command line history. Once saved, the password will be securely obtained from the keychain as needed.")
parser.add_option("-u", "--user",
action="store", dest="user",
help="If authentication is used, this is the user to authenticate.")
parser.add_option("-p", "--password",
action="store", dest="password",
help="If authentication is used, this is the user's password. This password will be stored in the keychain, so it only needs to be provided once.")
parser.add_option("-f", "--sender",
action="store", dest="sender",
help="The sender's email address to use. If no sender's address was provided but a user was provided for an authenticated email server, then the user will be used as the sender.")
parser.add_option("-s", "--server",
action="store", dest="server",
help="Email server that will send notifications. Defaults to gmail: \"" + gDefaultSmptServer + "\"")
parser.add_option("-r", "--recipients",
action="store", dest="recipients",
help="Email receipients to receive alerts, in comma delimited form: \"[email protected],[email protected]\". If not specified, then this script will use the sender's address as the recipient.")
parser.add_option("-P", "--poolurls",
action="store", dest="poolurls",
help="If specified, then these pool URLs will be monitored. The URLs must be complete (including any users or workers). The form specified must be comma delimited like this: \"http://pool1.com/worker1,http://pool1.com/worker2\"")
parser.add_option("-w", "--workers",
action="store", dest="workers",
help="If specified, then these workers will be monitored on CK's solo pool. If there's more than one, they must be in comma delimited format like this: \"worker1,worker2\"")
parser.add_option("-U", "--users",
action="store", dest="users",
help="If specified, then these users will be monitored on CK's solo pool. If there's more than one, they must be in comma delimited format like this: \"user1,user2\"")
parser.add_option("-l", "--listurls",
action="store", dest="listurls",
help="If specified, then these URLs will be used to provide a simple text file of user and worker addresses. If there's more than one URL, they must be in comma delimited formate like this: \"http://url1,http://url2\". The text files referred by the URLs should have one user or worker address per line. You can use this option in combination with the --users or --workers options as desired.")
parser.add_option("-S", "--sleepseconds",
action="store", dest="sleepseconds",
help="If specified, then this is the number of seconds to sleep between monitoring events. Defaults to " + str(gDefaultMonitorSleepSeconds) + " seconds.")
parser.add_option("-b", "--bestshare",
action="store", dest="bestshare",
help="By default this script notifies receipients if the best share of any monitored workers or users increases. This option allows you to explicitly enable or disable this notification by providing boolean expression including: " + getValidBoolExpresionsStr() + ". For example, this option will disable best share notification: --bestshare \"off\"")
parser.add_option("-H", "--showhashrate",
action="store", dest="showhashrate",
help="By default this script will include the hash rates of any monitored workers or users. This option allows you to explicitly enable or disable including the hash rates providing boolean expression including: " + getValidBoolExpresionsStr() + ". For example, this option will disable hash rate info in notification emails: --showhashrate \"off\"")
parser.add_option("-n", "--notifytime",
action="store", dest="notifytime",
help="If specified, then a notification email with the stats of the monitored addresses will be sent daily at the specified time on the clock. The time string is specified in local time and takes the form: \"HH:MM\". For example, to receive an notification email every day at 6 AM, you would use this option: --notifytime 6:00")
parser.add_option("-t", "--test",
action="store_true", dest="test",
help="If specified, then send a test message to the recipients using the senders credentials, then quit. If a password is provided, it will be saved in the current user's keychain.")
parser.add_option("-c", "--clear",
action="store_true", dest="clear",
help="If specified, then clear any saved history. This will result in finding new best share data and sending a new notification email.")
parser.add_option("-F", "--fakefoundaddress",
action="store", dest="fakefoundaddress",
help="If you pass an address via this option, then the script will go into test mode where it will pretend that this address found a block. Within " + str(gDefaultBlockCheckMinutes) + " minutes an email will be sent indicate that this address found a block. This option is for development and testing only.")
parser.add_option("--debug",
action="store_true", dest="debug",
help="Turn on debugging output for this script.")
# Parse the incomming arguments.
(options, args) = parser.parse_args()
# See if we're debugging this script
#options.debug=True
if options.debug:
gDebug = True
else:
gDebug = False
if gDebug:
print("After options parsing:")
print(" options:", options)
print(" args...:", args)
# If the verbose option was specified, we'll display verbose output
if options.verbose:
gVerbose = True
else:
gVerbose = False
# If the caller wants us to clear history, then delete the saved data file.
if options.clear:
if os.path.exists(gSavedStatsFilePath):
print("Deleting the saved stats data file located here: \"" + gSavedStatsFilePath + "\"")
os.remove(gSavedStatsFilePath)
# Make sure the caller specifies a user account to send emails. If a user was specified for
# authentication and no sender was specified, then user the user as the sender.
sender = options.sender
if not stringArgCheck(sender):
if stringArgCheck(options.user):
sender = options.user
else:
exitFail("You must specify the sending address for notifications.")
# Make sure the caller specifies some email recipients
recipients=[]
if stringArgCheck(options.recipients):
recipients = options.recipients.split(",")
else:
recipients.append(sender)
if gDebug: print("Using the sender as the recipient: " + str(recipients))
# Make sure we have an smtp server.
if not stringArgCheck(options.server):
exitFail("You must specify an SMTP server that will be used to send emails.")
# If the caller wants to set the password in the keychain, then do that now, preventing the keychain
# from being visible in the command line history or terminal window.
password = None
if options.setpassword:
if not stringArgCheck(options.user):
exitFail("You must specify a user in order to set the password.")
print("Please enter the password used to authenticate the user for sending emails.")
password = getpass.getpass()
setPassword(options.user, password)
# If the caller specified a user for email authentication, then we will also need a password.
# If a password was specified, then save it in the keychain. If a password was not specified,
# then try to retrieve it from the keychain.
if stringArgCheck(options.user):
if not password:
password = setOrGetPassword(options.user, options.password)
# Initialize an email server object. We'll need it whether we're in test mode or monitor mode
emailServer = EmailServer(serverUrl=options.server, user=options.user, password=password)