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_getinfo_var = variable.Variable(None)
60 @defer.inlineCallbacks
62 bitcoind_getinfo_var.set((yield deferral.retry('Error while calling getinfo:')(bitcoind.rpc_getinfo)()))
64 deferral.RobustLoopingCall(poll_warnings).start(20*60)
67 print ' Current block hash: %x' % (temp_work['previous_block'],)
68 print ' Current block height: %i' % (temp_work['height'] - 1,)
72 factory = yield connect_p2p()
74 print 'Determining payout address...'
75 if args.pubkey_hash is None:
76 address_path = os.path.join(datadir_path, 'cached_payout_address')
78 if os.path.exists(address_path):
79 with open(address_path, 'rb') as f:
80 address = f.read().strip('\r\n')
81 print ' Loaded cached address: %s...' % (address,)
85 if address is not None:
86 res = yield deferral.retry('Error validating cached address:', 5)(lambda: bitcoind.rpc_validateaddress(address))()
87 if not res['isvalid'] or not res['ismine']:
88 print ' Cached address is either invalid or not controlled by local bitcoind!'
92 print ' Getting payout address from bitcoind...'
93 address = yield deferral.retry('Error getting payout address from bitcoind:', 5)(lambda: bitcoind.rpc_getaccountaddress('p2pool'))()
95 with open(address_path, 'wb') as f:
98 my_pubkey_hash = bitcoin_data.address_to_pubkey_hash(address, net.PARENT)
100 my_pubkey_hash = args.pubkey_hash
101 print ' ...success! Payout address:', bitcoin_data.pubkey_hash_to_address(my_pubkey_hash, net.PARENT)
104 print "Loading shares..."
106 known_verified = set()
108 share.time_seen = 0 # XXX
109 shares[share.hash] = share
110 if len(shares) % 1000 == 0 and shares:
111 print " %i" % (len(shares),)
112 ss = p2pool_data.ShareStore(os.path.join(datadir_path, 'shares.'), net, share_cb, known_verified.add)
113 print " ...done loading %i shares (%i verified)!" % (len(shares), len(known_verified))
117 print 'Initializing work...'
119 node = p2pool_node.Node(factory, bitcoind, shares.values(), known_verified, net)
122 for share_hash in shares:
123 if share_hash not in node.tracker.items:
124 ss.forget_share(share_hash)
125 for share_hash in known_verified:
126 if share_hash not in node.tracker.verified.items:
127 ss.forget_verified_share(share_hash)
128 node.tracker.removed.watch(lambda share: ss.forget_share(share.hash))
129 node.tracker.verified.removed.watch(lambda share: ss.forget_verified_share(share.hash))
132 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)):
134 if share.hash in node.tracker.verified.items:
135 ss.add_verified_hash(share.hash)
136 deferral.RobustLoopingCall(save_shares).start(60)
142 print 'Joining p2pool network using port %i...' % (args.p2pool_port,)
144 @defer.inlineCallbacks
148 host, port_str = host.split(':')
150 defer.returnValue(((yield reactor.resolve(host)), port))
153 if os.path.exists(os.path.join(datadir_path, 'addrs')):
155 with open(os.path.join(datadir_path, 'addrs'), 'rb') as f:
156 addrs.update(dict((tuple(k), v) for k, v in json.loads(f.read())))
158 print >>sys.stderr, 'error parsing addrs'
159 for addr_df in map(parse, net.BOOTSTRAP_ADDRS):
162 if addr not in addrs:
163 addrs[addr] = (0, time.time(), time.time())
167 connect_addrs = set()
168 for addr_df in map(parse, args.p2pool_nodes):
170 connect_addrs.add((yield addr_df))
174 node.p2p_node = p2pool_node.P2PNode(node,
175 port=args.p2pool_port,
176 max_incoming_conns=args.p2pool_conns,
178 connect_addrs=connect_addrs,
179 desired_outgoing_conns=args.p2pool_outgoing_conns,
180 advertise_ip=args.advertise_ip,
182 node.p2p_node.start()
185 with open(os.path.join(datadir_path, 'addrs'), 'wb') as f:
186 f.write(json.dumps(node.p2p_node.addr_store.items()))
187 deferral.RobustLoopingCall(save_addrs).start(60)
193 @defer.inlineCallbacks
197 is_lan, lan_ip = yield ipdiscover.get_local_ip()
199 pm = yield portmapper.get_port_mapper()
200 yield pm._upnp.add_port_mapping(lan_ip, args.p2pool_port, args.p2pool_port, 'p2pool', 'TCP')
201 except defer.TimeoutError:
205 log.err(None, 'UPnP error:')
206 yield deferral.sleep(random.expovariate(1/120))
209 # start listening for workers with a JSON-RPC server
211 print 'Listening for workers on %r port %i...' % (worker_endpoint[0], worker_endpoint[1])
213 wb = work.WorkerBridge(node, my_pubkey_hash, args.donation_percentage, merged_urls, args.worker_fee)
214 web_root = web.get_web_root(wb, datadir_path, bitcoind_getinfo_var)
215 caching_wb = worker_interface.CachingWorkerBridge(wb)
216 worker_interface.WorkerInterface(caching_wb).attach_to(web_root, get_handler=lambda request: request.redirect('static/'))
217 web_serverfactory = server.Site(web_root)
220 serverfactory = switchprotocol.FirstByteSwitchFactory({'{': stratum.StratumServerFactory(caching_wb)}, web_serverfactory)
221 deferral.retry('Error binding to worker port:', traceback=False)(reactor.listenTCP)(worker_endpoint[1], serverfactory, interface=worker_endpoint[0])
223 with open(os.path.join(os.path.join(datadir_path, 'ready_flag')), 'wb') as f:
231 print 'Started successfully!'
232 print 'Go to http://127.0.0.1:%i/ to view graphs and statistics!' % (worker_endpoint[1],)
233 if args.donation_percentage > 1.1:
234 print '''Donating %.1f%% of work towards P2Pool's development. Thanks for the tip!''' % (args.donation_percentage,)
235 elif args.donation_percentage < .9:
236 print '''Donating %.1f%% of work towards P2Pool's development. Please donate to encourage further development of P2Pool!''' % (args.donation_percentage,)
238 print '''Donating %.1f%% of work towards P2Pool's development. Thank you!''' % (args.donation_percentage,)
239 print 'You can increase this amount with --give-author argument! (or decrease it, if you must)'
243 if hasattr(signal, 'SIGALRM'):
244 signal.signal(signal.SIGALRM, lambda signum, frame: reactor.callFromThread(
245 sys.stderr.write, 'Watchdog timer went off at:\n' + ''.join(traceback.format_stack())
247 signal.siginterrupt(signal.SIGALRM, False)
248 deferral.RobustLoopingCall(signal.alarm, 30).start(1)
250 if args.irc_announce:
251 from twisted.words.protocols import irc
252 class IRCClient(irc.IRCClient):
253 nickname = 'p2pool%02i' % (random.randrange(100),)
254 channel = net.ANNOUNCE_CHANNEL
255 def lineReceived(self, line):
258 irc.IRCClient.lineReceived(self, line)
260 self.in_channel = False
261 irc.IRCClient.signedOn(self)
262 self.factory.resetDelay()
263 self.join(self.channel)
264 @defer.inlineCallbacks
265 def new_share(share):
266 if not self.in_channel:
268 if share.pow_hash <= share.header['bits'].target and abs(share.timestamp - time.time()) < 10*60:
269 yield deferral.sleep(random.expovariate(1/60))
270 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)
271 if all('%x' % (share.header_hash,) not in old_message for old_message in self.recent_messages):
272 self.say(self.channel, message)
273 self._remember_message(message)
274 self.watch_id = node.tracker.verified.added.watch(new_share)
275 self.recent_messages = []
276 def joined(self, channel):
277 self.in_channel = True
278 def left(self, channel):
279 self.in_channel = False
280 def _remember_message(self, message):
281 self.recent_messages.append(message)
282 while len(self.recent_messages) > 100:
283 self.recent_messages.pop(0)
284 def privmsg(self, user, channel, message):
285 if channel == self.channel:
286 self._remember_message(message)
287 def connectionLost(self, reason):
288 node.tracker.verified.added.unwatch(self.watch_id)
289 print 'IRC connection lost:', reason.getErrorMessage()
290 class IRCClientFactory(protocol.ReconnectingClientFactory):
292 reactor.connectTCP("irc.freenode.net", 6667, IRCClientFactory(), bindAddress=(worker_endpoint[0], 0))
294 @defer.inlineCallbacks
299 yield deferral.sleep(3)
301 height = node.tracker.get_height(node.best_share_var.value)
302 this_str = 'P2Pool: %i shares in chain (%i verified/%i total) Peers: %i (%i incoming)' % (
304 len(node.tracker.verified.items),
305 len(node.tracker.items),
306 len(node.p2p_node.peers),
307 sum(1 for peer in node.p2p_node.peers.itervalues() if peer.incoming),
308 ) + (' FDs: %i R/%i W' % (len(reactor.getReaders()), len(reactor.getWriters())) if p2pool.DEBUG else '')
310 datums, dt = wb.local_rate_monitor.get_datums_in_last()
311 my_att_s = sum(datum['work']/dt for datum in datums)
312 my_shares_per_s = sum(datum['work']/dt/bitcoin_data.target_to_average_attempts(datum['share_target']) for datum in datums)
313 this_str += '\n Local: %sH/s in last %s Local dead on arrival: %s Expected time to share: %s' % (
314 math.format(int(my_att_s)),
316 math.format_binomial_conf(sum(1 for datum in datums if datum['dead']), len(datums), 0.95),
317 math.format_dt(1/my_shares_per_s) if my_shares_per_s else '???',
321 (stale_orphan_shares, stale_doa_shares), shares, _ = wb.get_stale_counts()
322 stale_prop = p2pool_data.get_average_stale_prop(node.tracker, node.best_share_var.value, min(60*60//net.SHARE_PERIOD, height))
323 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)
325 this_str += '\n Shares: %i (%i orphan, %i dead) Stale rate: %s Efficiency: %s Current payout: %.4f %s' % (
326 shares, stale_orphan_shares, stale_doa_shares,
327 math.format_binomial_conf(stale_orphan_shares + stale_doa_shares, shares, 0.95),
328 math.format_binomial_conf(stale_orphan_shares + stale_doa_shares, shares, 0.95, lambda x: (1 - x)/(1 - stale_prop)),
329 node.get_current_txouts().get(bitcoin_data.pubkey_hash_to_script2(my_pubkey_hash), 0)*1e-8, net.PARENT.SYMBOL,
331 this_str += '\n Pool: %sH/s Stale rate: %.1f%% Expected time to block: %s' % (
332 math.format(int(real_att_s)),
334 math.format_dt(2**256 / node.bitcoind_work.value['bits'].target / real_att_s),
337 for warning in p2pool_data.get_warnings(node.tracker, node.best_share_var.value, net, bitcoind_getinfo_var.value, node.bitcoind_work.value):
338 print >>sys.stderr, '#'*40
339 print >>sys.stderr, '>>> Warning: ' + warning
340 print >>sys.stderr, '#'*40
343 print '%i pieces of uncollectable cyclic garbage! Types: %r' % (len(gc.garbage), map(type, gc.garbage))
345 if this_str != last_str or time.time() > last_time + 15:
348 last_time = time.time()
354 log.err(None, 'Fatal error:')
357 if not hasattr(tcp.Client, 'abortConnection'):
358 print "Twisted doesn't have abortConnection! Upgrade to a newer version of Twisted to avoid memory leaks!"
359 print 'Pausing for 3 seconds...'
362 realnets = dict((name, net) for name, net in networks.nets.iteritems() if '_testnet' not in name)
364 parser = fixargparse.FixedArgumentParser(description='p2pool (version %s)' % (p2pool.__version__,), fromfile_prefix_chars='@')
365 parser.add_argument('--version', action='version', version=p2pool.__version__)
366 parser.add_argument('--net',
367 help='use specified network (default: bitcoin)',
368 action='store', choices=sorted(realnets), default='bitcoin', dest='net_name')
369 parser.add_argument('--testnet',
370 help='''use the network's testnet''',
371 action='store_const', const=True, default=False, dest='testnet')
372 parser.add_argument('--debug',
373 help='enable debugging mode',
374 action='store_const', const=True, default=False, dest='debug')
375 parser.add_argument('-a', '--address',
376 help='generate payouts to this address (default: <address requested from bitcoind>)',
377 type=str, action='store', default=None, dest='address')
378 parser.add_argument('--datadir',
379 help='store data in this directory (default: <directory run_p2pool.py is in>/data)',
380 type=str, action='store', default=None, dest='datadir')
381 parser.add_argument('--logfile',
382 help='''log to this file (default: data/<NET>/log)''',
383 type=str, action='store', default=None, dest='logfile')
384 parser.add_argument('--merged',
385 help='call getauxblock on this url to get work for merged mining (example: http://ncuser:ncpass@127.0.0.1:10332/)',
386 type=str, action='append', default=[], dest='merged_urls')
387 parser.add_argument('--give-author', metavar='DONATION_PERCENTAGE',
388 help='donate this percentage of work towards the development of p2pool (default: 1.0)',
389 type=float, action='store', default=1.0, dest='donation_percentage')
390 parser.add_argument('--iocp',
391 help='use Windows IOCP API in order to avoid errors due to large number of sockets being open',
392 action='store_true', default=False, dest='iocp')
393 parser.add_argument('--irc-announce',
394 help='announce any blocks found on irc://irc.freenode.net/#p2pool',
395 action='store_true', default=False, dest='irc_announce')
396 parser.add_argument('--no-bugreport',
397 help='disable submitting caught exceptions to the author',
398 action='store_true', default=False, dest='no_bugreport')
400 p2pool_group = parser.add_argument_group('p2pool interface')
401 p2pool_group.add_argument('--p2pool-port', metavar='PORT',
402 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())),
403 type=int, action='store', default=None, dest='p2pool_port')
404 p2pool_group.add_argument('-n', '--p2pool-node', metavar='ADDR[:PORT]',
405 help='connect to existing p2pool node at ADDR listening on port PORT (defaults to default p2pool P2P port) in addition to builtin addresses',
406 type=str, action='append', default=[], dest='p2pool_nodes')
407 parser.add_argument('--disable-upnp',
408 help='''don't attempt to use UPnP to forward p2pool's P2P port from the Internet to this computer''',
409 action='store_false', default=True, dest='upnp')
410 p2pool_group.add_argument('--max-conns', metavar='CONNS',
411 help='maximum incoming connections (default: 40)',
412 type=int, action='store', default=40, dest='p2pool_conns')
413 p2pool_group.add_argument('--outgoing-conns', metavar='CONNS',
414 help='outgoing connections (default: 6)',
415 type=int, action='store', default=6, dest='p2pool_outgoing_conns')
416 parser.add_argument('--disable-advertise',
417 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''',
418 action='store_false', default=True, dest='advertise_ip')
420 worker_group = parser.add_argument_group('worker interface')
421 worker_group.add_argument('-w', '--worker-port', metavar='PORT or ADDR:PORT',
422 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())),
423 type=str, action='store', default=None, dest='worker_endpoint')
424 worker_group.add_argument('-f', '--fee', metavar='FEE_PERCENTAGE',
425 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)''',
426 type=float, action='store', default=0, dest='worker_fee')
428 bitcoind_group = parser.add_argument_group('bitcoind interface')
429 bitcoind_group.add_argument('--bitcoind-address', metavar='BITCOIND_ADDRESS',
430 help='connect to this address (default: 127.0.0.1)',
431 type=str, action='store', default='127.0.0.1', dest='bitcoind_address')
432 bitcoind_group.add_argument('--bitcoind-rpc-port', metavar='BITCOIND_RPC_PORT',
433 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())),
434 type=int, action='store', default=None, dest='bitcoind_rpc_port')
435 bitcoind_group.add_argument('--bitcoind-rpc-ssl',
436 help='connect to JSON-RPC interface using SSL',
437 action='store_true', default=False, dest='bitcoind_rpc_ssl')
438 bitcoind_group.add_argument('--bitcoind-p2p-port', metavar='BITCOIND_P2P_PORT',
439 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())),
440 type=int, action='store', default=None, dest='bitcoind_p2p_port')
442 bitcoind_group.add_argument(metavar='BITCOIND_RPCUSERPASS',
443 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)',
444 type=str, action='store', default=[], nargs='*', dest='bitcoind_rpc_userpass')
446 args = parser.parse_args()
450 defer.setDebugging(True)
454 net_name = args.net_name + ('_testnet' if args.testnet else '')
455 net = networks.nets[net_name]
457 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)
458 if not os.path.exists(datadir_path):
459 os.makedirs(datadir_path)
461 if len(args.bitcoind_rpc_userpass) > 2:
462 parser.error('a maximum of two arguments are allowed')
463 args.bitcoind_rpc_username, args.bitcoind_rpc_password = ([None, None] + args.bitcoind_rpc_userpass)[-2:]
465 if args.bitcoind_rpc_password is None:
466 conf_path = net.PARENT.CONF_FILE_FUNC()
467 if not os.path.exists(conf_path):
468 parser.error('''Bitcoin configuration file not found. Manually enter your RPC password.\r\n'''
469 '''If you actually haven't created a configuration file, you should create one at %s with the text:\r\n'''
472 '''rpcpassword=%x\r\n'''
474 '''Keep that password secret! After creating the file, restart Bitcoin.''' % (conf_path, random.randrange(2**128)))
475 conf = open(conf_path, 'rb').read()
477 for line in conf.splitlines(True):
479 line = line[:line.index('#')]
482 k, v = line.split('=', 1)
483 contents[k.strip()] = v.strip()
484 for conf_name, var_name, var_type in [
485 ('rpcuser', 'bitcoind_rpc_username', str),
486 ('rpcpassword', 'bitcoind_rpc_password', str),
487 ('rpcport', 'bitcoind_rpc_port', int),
488 ('port', 'bitcoind_p2p_port', int),
490 if getattr(args, var_name) is None and conf_name in contents:
491 setattr(args, var_name, var_type(contents[conf_name]))
492 if args.bitcoind_rpc_password is None:
493 parser.error('''Bitcoin configuration file didn't contain an rpcpassword= line! Add one!''')
495 if args.bitcoind_rpc_username is None:
496 args.bitcoind_rpc_username = ''
498 if args.bitcoind_rpc_port is None:
499 args.bitcoind_rpc_port = net.PARENT.RPC_PORT
501 if args.bitcoind_p2p_port is None:
502 args.bitcoind_p2p_port = net.PARENT.P2P_PORT
504 if args.p2pool_port is None:
505 args.p2pool_port = net.P2P_PORT
507 if args.p2pool_outgoing_conns > 10:
508 parser.error('''--outgoing-conns can't be more than 10''')
510 if args.worker_endpoint is None:
511 worker_endpoint = '', net.WORKER_PORT
512 elif ':' not in args.worker_endpoint:
513 worker_endpoint = '', int(args.worker_endpoint)
515 addr, port = args.worker_endpoint.rsplit(':', 1)
516 worker_endpoint = addr, int(port)
518 if args.address is not None:
520 args.pubkey_hash = bitcoin_data.address_to_pubkey_hash(args.address, net.PARENT)
522 parser.error('error parsing address: ' + repr(e))
524 args.pubkey_hash = None
526 def separate_url(url):
527 s = urlparse.urlsplit(url)
528 if '@' not in s.netloc:
529 parser.error('merged url netloc must contain an "@"')
530 userpass, new_netloc = s.netloc.rsplit('@', 1)
531 return urlparse.urlunsplit(s._replace(netloc=new_netloc)), userpass
532 merged_urls = map(separate_url, args.merged_urls)
534 if args.logfile is None:
535 args.logfile = os.path.join(datadir_path, 'log')
537 logfile = logging.LogFile(args.logfile)
538 pipe = logging.TimestampingPipe(logging.TeePipe([logging.EncodeReplacerPipe(sys.stderr), logfile]))
539 sys.stdout = logging.AbortPipe(pipe)
540 sys.stderr = log.DefaultObserver.stderr = logging.AbortPipe(logging.PrefixPipe(pipe, '> '))
541 if hasattr(signal, "SIGUSR1"):
542 def sigusr1(signum, frame):
543 print 'Caught SIGUSR1, closing %r...' % (args.logfile,)
545 print '...and reopened %r after catching SIGUSR1.' % (args.logfile,)
546 signal.signal(signal.SIGUSR1, sigusr1)
547 deferral.RobustLoopingCall(logfile.reopen).start(5)
549 class ErrorReporter(object):
551 self.last_sent = None
553 def emit(self, eventDict):
554 if not eventDict["isError"]:
557 if self.last_sent is not None and time.time() < self.last_sent + 5:
559 self.last_sent = time.time()
561 if 'failure' in eventDict:
562 text = ((eventDict.get('why') or 'Unhandled Error')
563 + '\n' + eventDict['failure'].getTraceback())
565 text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
567 from twisted.web import client
569 url='http://u.forre.st/p2pool_error.cgi',
571 postdata=p2pool.__version__ + ' ' + net.NAME + '\n' + text,
573 ).addBoth(lambda x: None)
574 if not args.no_bugreport:
575 log.addObserver(ErrorReporter().emit)
577 reactor.callWhenRunning(main, args, net, datadir_path, merged_urls, worker_endpoint)