1 from __future__ import division
14 if '--iocp' in sys.argv:
15 from twisted.internet import iocpreactor
17 from twisted.internet import defer, reactor, protocol, tcp
18 from twisted.web import server
19 from twisted.python import log
20 from nattraverso import portmapper, ipdiscover
22 import bitcoin.p2p as bitcoin_p2p, bitcoin.data as bitcoin_data
23 from bitcoin import stratum, worker_interface, helper
24 from util import fixargparse, jsonrpc, variable, deferral, math, logging, switchprotocol
25 from . import networks, web, work
26 import p2pool, p2pool.data as p2pool_data, p2pool.node as p2pool_node
28 @defer.inlineCallbacks
29 def main(args, net, datadir_path, merged_urls, worker_endpoint):
31 print 'p2pool (version %s)' % (p2pool.__version__,)
34 @defer.inlineCallbacks
36 # connect to bitcoind over bitcoin-p2p
37 print '''Testing bitcoind P2P connection to '%s:%s'...''' % (args.bitcoind_address, args.bitcoind_p2p_port)
38 factory = bitcoin_p2p.ClientFactory(net.PARENT)
39 reactor.connectTCP(args.bitcoind_address, args.bitcoind_p2p_port, factory)
41 print ''' ...taking a while. Common reasons for this include all of bitcoind's connection slots being used...'''
42 long_dc = reactor.callLater(5, long)
43 yield factory.getProtocol() # waits until handshake is successful
44 if not long_dc.called: long_dc.cancel()
47 defer.returnValue(factory)
49 if args.testnet: # establish p2p connection first if testnet so bitcoind can work without connections
50 factory = yield connect_p2p()
52 # connect to bitcoind over JSON-RPC and do initial getmemorypool
53 url = '%s://%s:%i/' % ('https' if args.bitcoind_rpc_ssl else 'http', args.bitcoind_address, args.bitcoind_rpc_port)
54 print '''Testing bitcoind RPC connection to '%s' with username '%s'...''' % (url, args.bitcoind_rpc_username)
55 bitcoind = jsonrpc.HTTPProxy(url, dict(Authorization='Basic ' + base64.b64encode(args.bitcoind_rpc_username + ':' + args.bitcoind_rpc_password)), timeout=30)
56 yield helper.check(bitcoind, net)
57 temp_work = yield helper.getwork(bitcoind)
59 bitcoind_warning_var = variable.Variable(None)
60 @defer.inlineCallbacks
62 errors = (yield deferral.retry('Error while calling getmininginfo:')(bitcoind.rpc_getmininginfo)())['errors']
63 bitcoind_warning_var.set(errors if errors != '' else None)
65 deferral.RobustLoopingCall(poll_warnings).start(20*60)
68 print ' Current block hash: %x' % (temp_work['previous_block'],)
69 print ' Current block height: %i' % (temp_work['height'] - 1,)
73 factory = yield connect_p2p()
75 print 'Determining payout address...'
76 if args.pubkey_hash is None:
77 address_path = os.path.join(datadir_path, 'cached_payout_address')
79 if os.path.exists(address_path):
80 with open(address_path, 'rb') as f:
81 address = f.read().strip('\r\n')
82 print ' Loaded cached address: %s...' % (address,)
86 if address is not None:
87 res = yield deferral.retry('Error validating cached address:', 5)(lambda: bitcoind.rpc_validateaddress(address))()
88 if not res['isvalid'] or not res['ismine']:
89 print ' Cached address is either invalid or not controlled by local bitcoind!'
93 print ' Getting payout address from bitcoind...'
94 address = yield deferral.retry('Error getting payout address from bitcoind:', 5)(lambda: bitcoind.rpc_getaccountaddress('p2pool'))()
96 with open(address_path, 'wb') as f:
99 my_pubkey_hash = bitcoin_data.address_to_pubkey_hash(address, net.PARENT)
101 my_pubkey_hash = args.pubkey_hash
102 print ' ...success! Payout address:', bitcoin_data.pubkey_hash_to_address(my_pubkey_hash, net.PARENT)
105 print "Loading shares..."
107 known_verified = set()
109 share.time_seen = 0 # XXX
110 shares[share.hash] = share
111 if len(shares) % 1000 == 0 and shares:
112 print " %i" % (len(shares),)
113 ss = p2pool_data.ShareStore(os.path.join(datadir_path, 'shares.'), net, share_cb, known_verified.add)
114 print " ...done loading %i shares (%i verified)!" % (len(shares), len(known_verified))
118 print 'Initializing work...'
120 node = p2pool_node.Node(factory, bitcoind, shares.values(), known_verified, net)
123 for share_hash in shares:
124 if share_hash not in node.tracker.items:
125 ss.forget_share(share_hash)
126 for share_hash in known_verified:
127 if share_hash not in node.tracker.verified.items:
128 ss.forget_verified_share(share_hash)
129 node.tracker.removed.watch(lambda share: ss.forget_share(share.hash))
130 node.tracker.verified.removed.watch(lambda share: ss.forget_verified_share(share.hash))
133 for share in node.tracker.get_chain(node.best_share_var.value, min(node.tracker.get_height(node.best_share_var.value), 2*net.CHAIN_LENGTH)):
135 if share.hash in node.tracker.verified.items:
136 ss.add_verified_hash(share.hash)
137 deferral.RobustLoopingCall(save_shares).start(60)
143 print 'Joining p2pool network using port %i...' % (args.p2pool_port,)
145 @defer.inlineCallbacks
149 host, port_str = host.split(':')
151 defer.returnValue(((yield reactor.resolve(host)), port))
154 if os.path.exists(os.path.join(datadir_path, 'addrs')):
156 with open(os.path.join(datadir_path, 'addrs'), 'rb') as f:
157 addrs.update(dict((tuple(k), v) for k, v in json.loads(f.read())))
159 print >>sys.stderr, 'error parsing addrs'
160 for addr_df in map(parse, net.BOOTSTRAP_ADDRS):
163 if addr not in addrs:
164 addrs[addr] = (0, time.time(), time.time())
168 connect_addrs = set()
169 for addr_df in map(parse, args.p2pool_nodes):
171 connect_addrs.add((yield addr_df))
175 node.p2p_node = p2pool_node.P2PNode(node,
176 port=args.p2pool_port,
177 max_incoming_conns=args.p2pool_conns,
179 connect_addrs=connect_addrs,
180 desired_outgoing_conns=args.p2pool_outgoing_conns,
181 advertise_ip=args.advertise_ip,
183 node.p2p_node.start()
186 with open(os.path.join(datadir_path, 'addrs'), 'wb') as f:
187 f.write(json.dumps(node.p2p_node.addr_store.items()))
188 deferral.RobustLoopingCall(save_addrs).start(60)
194 @defer.inlineCallbacks
198 is_lan, lan_ip = yield ipdiscover.get_local_ip()
200 pm = yield portmapper.get_port_mapper()
201 yield pm._upnp.add_port_mapping(lan_ip, args.p2pool_port, args.p2pool_port, 'p2pool', 'TCP')
202 except defer.TimeoutError:
206 log.err(None, 'UPnP error:')
207 yield deferral.sleep(random.expovariate(1/120))
210 # start listening for workers with a JSON-RPC server
212 print 'Listening for workers on %r port %i...' % (worker_endpoint[0], worker_endpoint[1])
214 wb = work.WorkerBridge(node, my_pubkey_hash, args.donation_percentage, merged_urls, args.worker_fee)
215 web_root = web.get_web_root(wb, datadir_path, bitcoind_warning_var)
216 caching_wb = worker_interface.CachingWorkerBridge(wb)
217 worker_interface.WorkerInterface(caching_wb).attach_to(web_root, get_handler=lambda request: request.redirect('static/'))
218 web_serverfactory = server.Site(web_root)
221 serverfactory = switchprotocol.FirstByteSwitchFactory({'{': stratum.StratumServerFactory(caching_wb)}, web_serverfactory)
222 deferral.retry('Error binding to worker port:', traceback=False)(reactor.listenTCP)(worker_endpoint[1], serverfactory, interface=worker_endpoint[0])
224 with open(os.path.join(os.path.join(datadir_path, 'ready_flag')), 'wb') as f:
232 print 'Started successfully!'
233 print 'Go to http://127.0.0.1:%i/ to view graphs and statistics!' % (worker_endpoint[1],)
234 if args.donation_percentage > 1.1:
235 print '''Donating %.1f%% of work towards P2Pool's development. Thanks for the tip!''' % (args.donation_percentage,)
236 elif args.donation_percentage < .9:
237 print '''Donating %.1f%% of work towards P2Pool's development. Please donate to encourage further development of P2Pool!''' % (args.donation_percentage,)
239 print '''Donating %.1f%% of work towards P2Pool's development. Thank you!''' % (args.donation_percentage,)
240 print 'You can increase this amount with --give-author argument! (or decrease it, if you must)'
244 if hasattr(signal, 'SIGALRM'):
245 signal.signal(signal.SIGALRM, lambda signum, frame: reactor.callFromThread(
246 sys.stderr.write, 'Watchdog timer went off at:\n' + ''.join(traceback.format_stack())
248 signal.siginterrupt(signal.SIGALRM, False)
249 deferral.RobustLoopingCall(signal.alarm, 30).start(1)
251 if args.irc_announce:
252 from twisted.words.protocols import irc
253 class IRCClient(irc.IRCClient):
254 nickname = 'p2pool%02i' % (random.randrange(100),)
255 channel = net.ANNOUNCE_CHANNEL
256 def lineReceived(self, line):
259 irc.IRCClient.lineReceived(self, line)
261 self.in_channel = False
262 irc.IRCClient.signedOn(self)
263 self.factory.resetDelay()
264 self.join(self.channel)
265 @defer.inlineCallbacks
266 def new_share(share):
267 if not self.in_channel:
269 if share.pow_hash <= share.header['bits'].target and abs(share.timestamp - time.time()) < 10*60:
270 yield deferral.sleep(random.expovariate(1/60))
271 message = '\x02%s BLOCK FOUND by %s! %s%064x' % (net.NAME.upper(), bitcoin_data.script2_to_address(share.new_script, net.PARENT), net.PARENT.BLOCK_EXPLORER_URL_PREFIX, share.header_hash)
272 if all('%x' % (share.header_hash,) not in old_message for old_message in self.recent_messages):
273 self.say(self.channel, message)
274 self._remember_message(message)
275 self.watch_id = node.tracker.verified.added.watch(new_share)
276 self.recent_messages = []
277 def joined(self, channel):
278 self.in_channel = True
279 def left(self, channel):
280 self.in_channel = False
281 def _remember_message(self, message):
282 self.recent_messages.append(message)
283 while len(self.recent_messages) > 100:
284 self.recent_messages.pop(0)
285 def privmsg(self, user, channel, message):
286 if channel == self.channel:
287 self._remember_message(message)
288 def connectionLost(self, reason):
289 node.tracker.verified.added.unwatch(self.watch_id)
290 print 'IRC connection lost:', reason.getErrorMessage()
291 class IRCClientFactory(protocol.ReconnectingClientFactory):
293 reactor.connectTCP("irc.freenode.net", 6667, IRCClientFactory())
295 @defer.inlineCallbacks
300 yield deferral.sleep(3)
302 height = node.tracker.get_height(node.best_share_var.value)
303 this_str = 'P2Pool: %i shares in chain (%i verified/%i total) Peers: %i (%i incoming)' % (
305 len(node.tracker.verified.items),
306 len(node.tracker.items),
307 len(node.p2p_node.peers),
308 sum(1 for peer in node.p2p_node.peers.itervalues() if peer.incoming),
309 ) + (' FDs: %i R/%i W' % (len(reactor.getReaders()), len(reactor.getWriters())) if p2pool.DEBUG else '')
311 datums, dt = wb.local_rate_monitor.get_datums_in_last()
312 my_att_s = sum(datum['work']/dt for datum in datums)
313 my_shares_per_s = sum(datum['work']/dt/bitcoin_data.target_to_average_attempts(datum['share_target']) for datum in datums)
314 this_str += '\n Local: %sH/s in last %s Local dead on arrival: %s Expected time to share: %s' % (
315 math.format(int(my_att_s)),
317 math.format_binomial_conf(sum(1 for datum in datums if datum['dead']), len(datums), 0.95),
318 math.format_dt(1/my_shares_per_s) if my_shares_per_s else '???',
322 (stale_orphan_shares, stale_doa_shares), shares, _ = wb.get_stale_counts()
323 stale_prop = p2pool_data.get_average_stale_prop(node.tracker, node.best_share_var.value, min(60*60//net.SHARE_PERIOD, height))
324 real_att_s = p2pool_data.get_pool_attempts_per_second(node.tracker, node.best_share_var.value, min(height - 1, 60*60//net.SHARE_PERIOD)) / (1 - stale_prop)
326 this_str += '\n Shares: %i (%i orphan, %i dead) Stale rate: %s Efficiency: %s Current payout: %.4f %s' % (
327 shares, stale_orphan_shares, stale_doa_shares,
328 math.format_binomial_conf(stale_orphan_shares + stale_doa_shares, shares, 0.95),
329 math.format_binomial_conf(stale_orphan_shares + stale_doa_shares, shares, 0.95, lambda x: (1 - x)/(1 - stale_prop)),
330 node.get_current_txouts().get(bitcoin_data.pubkey_hash_to_script2(my_pubkey_hash), 0)*1e-8, net.PARENT.SYMBOL,
332 this_str += '\n Pool: %sH/s Stale rate: %.1f%% Expected time to block: %s' % (
333 math.format(int(real_att_s)),
335 math.format_dt(2**256 / node.bitcoind_work.value['bits'].target / real_att_s),
338 for warning in p2pool_data.get_warnings(node.tracker, node.best_share_var.value, net, bitcoind_warning_var.value, node.bitcoind_work.value):
339 print >>sys.stderr, '#'*40
340 print >>sys.stderr, '>>> Warning: ' + warning
341 print >>sys.stderr, '#'*40
344 print '%i pieces of uncollectable cyclic garbage! Types: %r' % (len(gc.garbage), map(type, gc.garbage))
346 if this_str != last_str or time.time() > last_time + 15:
349 last_time = time.time()
355 log.err(None, 'Fatal error:')
358 if not hasattr(tcp.Client, 'abortConnection'):
359 print "Twisted doesn't have abortConnection! Upgrade to a newer version of Twisted to avoid memory leaks!"
360 print 'Pausing for 3 seconds...'
363 realnets = dict((name, net) for name, net in networks.nets.iteritems() if '_testnet' not in name)
365 parser = fixargparse.FixedArgumentParser(description='p2pool (version %s)' % (p2pool.__version__,), fromfile_prefix_chars='@')
366 parser.add_argument('--version', action='version', version=p2pool.__version__)
367 parser.add_argument('--net',
368 help='use specified network (default: bitcoin)',
369 action='store', choices=sorted(realnets), default='bitcoin', dest='net_name')
370 parser.add_argument('--testnet',
371 help='''use the network's testnet''',
372 action='store_const', const=True, default=False, dest='testnet')
373 parser.add_argument('--debug',
374 help='enable debugging mode',
375 action='store_const', const=True, default=False, dest='debug')
376 parser.add_argument('-a', '--address',
377 help='generate payouts to this address (default: <address requested from bitcoind>)',
378 type=str, action='store', default=None, dest='address')
379 parser.add_argument('--datadir',
380 help='store data in this directory (default: <directory run_p2pool.py is in>/data)',
381 type=str, action='store', default=None, dest='datadir')
382 parser.add_argument('--logfile',
383 help='''log to this file (default: data/<NET>/log)''',
384 type=str, action='store', default=None, dest='logfile')
385 parser.add_argument('--merged',
386 help='call getauxblock on this url to get work for merged mining (example: http://ncuser:ncpass@127.0.0.1:10332/)',
387 type=str, action='append', default=[], dest='merged_urls')
388 parser.add_argument('--give-author', metavar='DONATION_PERCENTAGE',
389 help='donate this percentage of work towards the development of p2pool (default: 1.0)',
390 type=float, action='store', default=1.0, dest='donation_percentage')
391 parser.add_argument('--iocp',
392 help='use Windows IOCP API in order to avoid errors due to large number of sockets being open',
393 action='store_true', default=False, dest='iocp')
394 parser.add_argument('--irc-announce',
395 help='announce any blocks found on irc://irc.freenode.net/#p2pool',
396 action='store_true', default=False, dest='irc_announce')
397 parser.add_argument('--no-bugreport',
398 help='disable submitting caught exceptions to the author',
399 action='store_true', default=False, dest='no_bugreport')
401 p2pool_group = parser.add_argument_group('p2pool interface')
402 p2pool_group.add_argument('--p2pool-port', metavar='PORT',
403 help='use port PORT to listen for connections (forward this port from your router!) (default: %s)' % ', '.join('%s:%i' % (name, net.P2P_PORT) for name, net in sorted(realnets.items())),
404 type=int, action='store', default=None, dest='p2pool_port')
405 p2pool_group.add_argument('-n', '--p2pool-node', metavar='ADDR[:PORT]',
406 help='connect to existing p2pool node at ADDR listening on port PORT (defaults to default p2pool P2P port) in addition to builtin addresses',
407 type=str, action='append', default=[], dest='p2pool_nodes')
408 parser.add_argument('--disable-upnp',
409 help='''don't attempt to use UPnP to forward p2pool's P2P port from the Internet to this computer''',
410 action='store_false', default=True, dest='upnp')
411 p2pool_group.add_argument('--max-conns', metavar='CONNS',
412 help='maximum incoming connections (default: 40)',
413 type=int, action='store', default=40, dest='p2pool_conns')
414 p2pool_group.add_argument('--outgoing-conns', metavar='CONNS',
415 help='outgoing connections (default: 6)',
416 type=int, action='store', default=6, dest='p2pool_outgoing_conns')
417 parser.add_argument('--disable-advertise',
418 help='''don't advertise local IP address as being available for incoming connections. useful for running a dark node, along with multiple -n ADDR's and --outgoing-conns 0''',
419 action='store_false', default=True, dest='advertise_ip')
421 worker_group = parser.add_argument_group('worker interface')
422 worker_group.add_argument('-w', '--worker-port', metavar='PORT or ADDR:PORT',
423 help='listen on PORT on interface with ADDR for RPC connections from miners (default: all interfaces, %s)' % ', '.join('%s:%i' % (name, net.WORKER_PORT) for name, net in sorted(realnets.items())),
424 type=str, action='store', default=None, dest='worker_endpoint')
425 worker_group.add_argument('-f', '--fee', metavar='FEE_PERCENTAGE',
426 help='''charge workers mining to their own bitcoin address (by setting their miner's username to a bitcoin address) this percentage fee to mine on your p2pool instance. Amount displayed at http://127.0.0.1:WORKER_PORT/fee (default: 0)''',
427 type=float, action='store', default=0, dest='worker_fee')
429 bitcoind_group = parser.add_argument_group('bitcoind interface')
430 bitcoind_group.add_argument('--bitcoind-address', metavar='BITCOIND_ADDRESS',
431 help='connect to this address (default: 127.0.0.1)',
432 type=str, action='store', default='127.0.0.1', dest='bitcoind_address')
433 bitcoind_group.add_argument('--bitcoind-rpc-port', metavar='BITCOIND_RPC_PORT',
434 help='''connect to JSON-RPC interface at this port (default: %s <read from bitcoin.conf if password not provided>)''' % ', '.join('%s:%i' % (name, net.PARENT.RPC_PORT) for name, net in sorted(realnets.items())),
435 type=int, action='store', default=None, dest='bitcoind_rpc_port')
436 bitcoind_group.add_argument('--bitcoind-rpc-ssl',
437 help='connect to JSON-RPC interface using SSL',
438 action='store_true', default=False, dest='bitcoind_rpc_ssl')
439 bitcoind_group.add_argument('--bitcoind-p2p-port', metavar='BITCOIND_P2P_PORT',
440 help='''connect to P2P interface at this port (default: %s <read from bitcoin.conf if password not provided>)''' % ', '.join('%s:%i' % (name, net.PARENT.P2P_PORT) for name, net in sorted(realnets.items())),
441 type=int, action='store', default=None, dest='bitcoind_p2p_port')
443 bitcoind_group.add_argument(metavar='BITCOIND_RPCUSERPASS',
444 help='bitcoind RPC interface username, then password, space-separated (only one being provided will cause the username to default to being empty, and none will cause P2Pool to read them from bitcoin.conf)',
445 type=str, action='store', default=[], nargs='*', dest='bitcoind_rpc_userpass')
447 args = parser.parse_args()
451 defer.setDebugging(True)
455 net_name = args.net_name + ('_testnet' if args.testnet else '')
456 net = networks.nets[net_name]
458 datadir_path = os.path.join((os.path.join(os.path.dirname(sys.argv[0]), 'data') if args.datadir is None else args.datadir), net_name)
459 if not os.path.exists(datadir_path):
460 os.makedirs(datadir_path)
462 if len(args.bitcoind_rpc_userpass) > 2:
463 parser.error('a maximum of two arguments are allowed')
464 args.bitcoind_rpc_username, args.bitcoind_rpc_password = ([None, None] + args.bitcoind_rpc_userpass)[-2:]
466 if args.bitcoind_rpc_password is None:
467 conf_path = net.PARENT.CONF_FILE_FUNC()
468 if not os.path.exists(conf_path):
469 parser.error('''Bitcoin configuration file not found. Manually enter your RPC password.\r\n'''
470 '''If you actually haven't created a configuration file, you should create one at %s with the text:\r\n'''
473 '''rpcpassword=%x\r\n'''
475 '''Keep that password secret! After creating the file, restart Bitcoin.''' % (conf_path, random.randrange(2**128)))
476 conf = open(conf_path, 'rb').read()
478 for line in conf.splitlines(True):
480 line = line[:line.index('#')]
483 k, v = line.split('=', 1)
484 contents[k.strip()] = v.strip()
485 for conf_name, var_name, var_type in [
486 ('rpcuser', 'bitcoind_rpc_username', str),
487 ('rpcpassword', 'bitcoind_rpc_password', str),
488 ('rpcport', 'bitcoind_rpc_port', int),
489 ('port', 'bitcoind_p2p_port', int),
491 if getattr(args, var_name) is None and conf_name in contents:
492 setattr(args, var_name, var_type(contents[conf_name]))
493 if args.bitcoind_rpc_password is None:
494 parser.error('''Bitcoin configuration file didn't contain an rpcpassword= line! Add one!''')
496 if args.bitcoind_rpc_username is None:
497 args.bitcoind_rpc_username = ''
499 if args.bitcoind_rpc_port is None:
500 args.bitcoind_rpc_port = net.PARENT.RPC_PORT
502 if args.bitcoind_p2p_port is None:
503 args.bitcoind_p2p_port = net.PARENT.P2P_PORT
505 if args.p2pool_port is None:
506 args.p2pool_port = net.P2P_PORT
508 if args.p2pool_outgoing_conns > 10:
509 parser.error('''--outgoing-conns can't be more than 10''')
511 if args.worker_endpoint is None:
512 worker_endpoint = '', net.WORKER_PORT
513 elif ':' not in args.worker_endpoint:
514 worker_endpoint = '', int(args.worker_endpoint)
516 addr, port = args.worker_endpoint.rsplit(':', 1)
517 worker_endpoint = addr, int(port)
519 if args.address is not None:
521 args.pubkey_hash = bitcoin_data.address_to_pubkey_hash(args.address, net.PARENT)
523 parser.error('error parsing address: ' + repr(e))
525 args.pubkey_hash = None
527 def separate_url(url):
528 s = urlparse.urlsplit(url)
529 if '@' not in s.netloc:
530 parser.error('merged url netloc must contain an "@"')
531 userpass, new_netloc = s.netloc.rsplit('@', 1)
532 return urlparse.urlunsplit(s._replace(netloc=new_netloc)), userpass
533 merged_urls = map(separate_url, args.merged_urls)
535 if args.logfile is None:
536 args.logfile = os.path.join(datadir_path, 'log')
538 logfile = logging.LogFile(args.logfile)
539 pipe = logging.TimestampingPipe(logging.TeePipe([logging.EncodeReplacerPipe(sys.stderr), logfile]))
540 sys.stdout = logging.AbortPipe(pipe)
541 sys.stderr = log.DefaultObserver.stderr = logging.AbortPipe(logging.PrefixPipe(pipe, '> '))
542 if hasattr(signal, "SIGUSR1"):
543 def sigusr1(signum, frame):
544 print 'Caught SIGUSR1, closing %r...' % (args.logfile,)
546 print '...and reopened %r after catching SIGUSR1.' % (args.logfile,)
547 signal.signal(signal.SIGUSR1, sigusr1)
548 deferral.RobustLoopingCall(logfile.reopen).start(5)
550 class ErrorReporter(object):
552 self.last_sent = None
554 def emit(self, eventDict):
555 if not eventDict["isError"]:
558 if self.last_sent is not None and time.time() < self.last_sent + 5:
560 self.last_sent = time.time()
562 if 'failure' in eventDict:
563 text = ((eventDict.get('why') or 'Unhandled Error')
564 + '\n' + eventDict['failure'].getTraceback())
566 text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
568 from twisted.web import client
570 url='http://u.forre.st/p2pool_error.cgi',
572 postdata=p2pool.__version__ + ' ' + net.NAME + '\n' + text,
574 ).addBoth(lambda x: None)
575 if not args.no_bugreport:
576 log.addObserver(ErrorReporter().emit)
578 reactor.callWhenRunning(main, args, net, datadir_path, merged_urls, worker_endpoint)