7eb6d3eecc1c2f8f7df4b58d194ba37f96eec99f
[p2pool.git] / p2pool / main.py
1 from __future__ import division
2
3 import base64
4 import json
5 import os
6 import random
7 import sys
8 import time
9 import signal
10 import traceback
11 import urlparse
12
13 if '--iocp' in sys.argv:
14     from twisted.internet import iocpreactor
15     iocpreactor.install()
16 from twisted.internet import defer, reactor, protocol, task
17 from twisted.web import server
18 from twisted.python import log
19 from nattraverso import portmapper, ipdiscover
20
21 import bitcoin.p2p as bitcoin_p2p, bitcoin.data as bitcoin_data
22 from bitcoin import worker_interface, helper
23 from util import fixargparse, jsonrpc, variable, deferral, math, logging
24 from . import networks, web, work
25 import p2pool, p2pool.data as p2pool_data, p2pool.node as p2pool_node
26
27 @defer.inlineCallbacks
28 def main(args, net, datadir_path, merged_urls, worker_endpoint):
29     try:
30         print 'p2pool (version %s)' % (p2pool.__version__,)
31         print
32         
33         @defer.inlineCallbacks
34         def connect_p2p():
35             # connect to bitcoind over bitcoin-p2p
36             print '''Testing bitcoind P2P connection to '%s:%s'...''' % (args.bitcoind_address, args.bitcoind_p2p_port)
37             factory = bitcoin_p2p.ClientFactory(net.PARENT)
38             reactor.connectTCP(args.bitcoind_address, args.bitcoind_p2p_port, factory)
39             yield factory.getProtocol() # waits until handshake is successful
40             print '    ...success!'
41             print
42             defer.returnValue(factory)
43         
44         if args.testnet: # establish p2p connection first if testnet so bitcoind can work without connections
45             factory = yield connect_p2p()
46         
47         # connect to bitcoind over JSON-RPC and do initial getmemorypool
48         url = '%s://%s:%i/' % ('https' if args.bitcoind_rpc_ssl else 'http', args.bitcoind_address, args.bitcoind_rpc_port)
49         print '''Testing bitcoind RPC connection to '%s' with username '%s'...''' % (url, args.bitcoind_rpc_username)
50         bitcoind = jsonrpc.Proxy(url, dict(Authorization='Basic ' + base64.b64encode(args.bitcoind_rpc_username + ':' + args.bitcoind_rpc_password)), timeout=30)
51         yield helper.check(bitcoind, net)
52         temp_work = yield helper.getwork(bitcoind)
53         
54         bitcoind_warning_var = variable.Variable(None)
55         @defer.inlineCallbacks
56         def poll_warnings():
57             errors = (yield deferral.retry('Error while calling getmininginfo:')(bitcoind.rpc_getmininginfo)())['errors']
58             bitcoind_warning_var.set(errors if errors != '' else None)
59         yield poll_warnings()
60         task.LoopingCall(poll_warnings).start(20*60)
61         
62         print '    ...success!'
63         print '    Current block hash: %x' % (temp_work['previous_block'],)
64         print '    Current block height: %i' % (temp_work['height'] - 1,)
65         print
66         
67         if not args.testnet:
68             factory = yield connect_p2p()
69         
70         print 'Determining payout address...'
71         if args.pubkey_hash is None:
72             address_path = os.path.join(datadir_path, 'cached_payout_address')
73             
74             if os.path.exists(address_path):
75                 with open(address_path, 'rb') as f:
76                     address = f.read().strip('\r\n')
77                 print '    Loaded cached address: %s...' % (address,)
78             else:
79                 address = None
80             
81             if address is not None:
82                 res = yield deferral.retry('Error validating cached address:', 5)(lambda: bitcoind.rpc_validateaddress(address))()
83                 if not res['isvalid'] or not res['ismine']:
84                     print '    Cached address is either invalid or not controlled by local bitcoind!'
85                     address = None
86             
87             if address is None:
88                 print '    Getting payout address from bitcoind...'
89                 address = yield deferral.retry('Error getting payout address from bitcoind:', 5)(lambda: bitcoind.rpc_getaccountaddress('p2pool'))()
90             
91             with open(address_path, 'wb') as f:
92                 f.write(address)
93             
94             my_pubkey_hash = bitcoin_data.address_to_pubkey_hash(address, net.PARENT)
95         else:
96             my_pubkey_hash = args.pubkey_hash
97         print '    ...success! Payout address:', bitcoin_data.pubkey_hash_to_address(my_pubkey_hash, net.PARENT)
98         print
99         
100         ss = p2pool_data.ShareStore(os.path.join(datadir_path, 'shares.'), net)
101         shares = {}
102         known_verified = set()
103         print "Loading shares..."
104         for i, (mode, contents) in enumerate(ss.get_shares()):
105             if mode == 'share':
106                 contents.time_seen = 0
107                 shares[contents.hash] = contents
108                 if len(shares) % 1000 == 0 and shares:
109                     print "    %i" % (len(shares),)
110             elif mode == 'verified_hash':
111                 known_verified.add(contents)
112             else:
113                 raise AssertionError()
114         print "    ...done loading %i shares (%i verified)!" % (len(shares), len(known_verified))
115         print
116         
117         
118         print 'Initializing work...'
119         
120         node = p2pool_node.Node(factory, bitcoind, shares.values(), known_verified, net)
121         yield node.start()
122         
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         del shares, known_verified
130         node.tracker.removed.watch(lambda share: ss.forget_share(share.hash))
131         node.tracker.verified.removed.watch(lambda share: ss.forget_verified_share(share.hash))
132         
133         def save_shares():
134             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                 ss.add_share(share)
136                 if share.hash in node.tracker.verified.items:
137                     ss.add_verified_hash(share.hash)
138         task.LoopingCall(save_shares).start(60)
139         
140         print '    ...success!'
141         print
142         
143         
144         print 'Joining p2pool network using port %i...' % (args.p2pool_port,)
145         
146         @defer.inlineCallbacks
147         def parse(x):
148             if ':' in x:
149                 ip, port = x.split(':')
150                 defer.returnValue(((yield reactor.resolve(ip)), int(port)))
151             else:
152                 defer.returnValue(((yield reactor.resolve(x)), net.P2P_PORT))
153         
154         addrs = {}
155         if os.path.exists(os.path.join(datadir_path, 'addrs')):
156             try:
157                 with open(os.path.join(datadir_path, 'addrs'), 'rb') as f:
158                     addrs.update(dict((tuple(k), v) for k, v in json.loads(f.read())))
159             except:
160                 print >>sys.stderr, 'error parsing addrs'
161         for addr_df in map(parse, net.BOOTSTRAP_ADDRS):
162             try:
163                 addr = yield addr_df
164                 if addr not in addrs:
165                     addrs[addr] = (0, time.time(), time.time())
166             except:
167                 log.err()
168         
169         connect_addrs = set()
170         for addr_df in map(parse, args.p2pool_nodes):
171             try:
172                 connect_addrs.add((yield addr_df))
173             except:
174                 log.err()
175         
176         node.p2p_node = p2pool_node.P2PNode(node, args.p2pool_port, args.p2pool_conns, addrs, connect_addrs)
177         node.p2p_node.start()
178         
179         def save_addrs():
180             with open(os.path.join(datadir_path, 'addrs'), 'wb') as f:
181                 f.write(json.dumps(node.p2p_node.addr_store.items()))
182         task.LoopingCall(save_addrs).start(60)
183         
184         print '    ...success!'
185         print
186         
187         if args.upnp:
188             @defer.inlineCallbacks
189             def upnp_thread():
190                 while True:
191                     try:
192                         is_lan, lan_ip = yield ipdiscover.get_local_ip()
193                         if is_lan:
194                             pm = yield portmapper.get_port_mapper()
195                             yield pm._upnp.add_port_mapping(lan_ip, args.p2pool_port, args.p2pool_port, 'p2pool', 'TCP')
196                     except defer.TimeoutError:
197                         pass
198                     except:
199                         if p2pool.DEBUG:
200                             log.err(None, 'UPnP error:')
201                     yield deferral.sleep(random.expovariate(1/120))
202             upnp_thread()
203         
204         # start listening for workers with a JSON-RPC server
205         
206         print 'Listening for workers on %r port %i...' % (worker_endpoint[0], worker_endpoint[1])
207         
208         wb = work.WorkerBridge(node, my_pubkey_hash, args.donation_percentage, merged_urls, args.worker_fee)
209         web_root = web.get_web_root(wb, datadir_path, bitcoind_warning_var)
210         worker_interface.WorkerInterface(wb).attach_to(web_root, get_handler=lambda request: request.redirect('/static/'))
211         
212         deferral.retry('Error binding to worker port:', traceback=False)(reactor.listenTCP)(worker_endpoint[1], server.Site(web_root), interface=worker_endpoint[0])
213         
214         with open(os.path.join(os.path.join(datadir_path, 'ready_flag')), 'wb') as f:
215             pass
216         
217         print '    ...success!'
218         print
219         
220         
221         # done!
222         print 'Started successfully!'
223         print 'Go to http://127.0.0.1:%i/ to view graphs and statistics!' % (worker_endpoint[1],)
224         if args.donation_percentage > 0.51:
225             print '''Donating %.1f%% of work towards P2Pool's development. Thanks for the tip!''' % (args.donation_percentage,)
226         elif args.donation_percentage < 0.49:
227             print '''Donating %.1f%% of work towards P2Pool's development. Please donate to encourage further development of P2Pool!''' % (args.donation_percentage,)
228         else:
229             print '''Donating %.1f%% of work towards P2Pool's development. Thank you!''' % (args.donation_percentage,)
230             print 'You can increase this amount with --give-author argument! (or decrease it, if you must)'
231         print
232         
233         
234         if hasattr(signal, 'SIGALRM'):
235             signal.signal(signal.SIGALRM, lambda signum, frame: reactor.callFromThread(
236                 sys.stderr.write, 'Watchdog timer went off at:\n' + ''.join(traceback.format_stack())
237             ))
238             signal.siginterrupt(signal.SIGALRM, False)
239             task.LoopingCall(signal.alarm, 30).start(1)
240         
241         if args.irc_announce:
242             from twisted.words.protocols import irc
243             class IRCClient(irc.IRCClient):
244                 nickname = 'p2pool%02i' % (random.randrange(100),)
245                 channel = net.ANNOUNCE_CHANNEL
246                 def lineReceived(self, line):
247                     if p2pool.DEBUG:
248                         print repr(line)
249                     irc.IRCClient.lineReceived(self, line)
250                 def signedOn(self):
251                     self.in_channel = False
252                     irc.IRCClient.signedOn(self)
253                     self.factory.resetDelay()
254                     self.join(self.channel)
255                     @defer.inlineCallbacks
256                     def new_share(share):
257                         if not self.in_channel:
258                             return
259                         if share.pow_hash <= share.header['bits'].target and abs(share.timestamp - time.time()) < 10*60:
260                             yield deferral.sleep(random.expovariate(1/60))
261                             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)
262                             if all('%x' % (share.header_hash,) not in old_message for old_message in self.recent_messages):
263                                 self.say(self.channel, message)
264                                 self._remember_message(message)
265                     self.watch_id = node.tracker.verified.added.watch(new_share)
266                     self.recent_messages = []
267                 def joined(self, channel):
268                     self.in_channel = True
269                 def left(self, channel):
270                     self.in_channel = False
271                 def _remember_message(self, message):
272                     self.recent_messages.append(message)
273                     while len(self.recent_messages) > 100:
274                         self.recent_messages.pop(0)
275                 def privmsg(self, user, channel, message):
276                     if channel == self.channel:
277                         self._remember_message(message)
278                 def connectionLost(self, reason):
279                     node.tracker.verified.added.unwatch(self.watch_id)
280                     print 'IRC connection lost:', reason.getErrorMessage()
281             class IRCClientFactory(protocol.ReconnectingClientFactory):
282                 protocol = IRCClient
283             reactor.connectTCP("irc.freenode.net", 6667, IRCClientFactory())
284         
285         @defer.inlineCallbacks
286         def status_thread():
287             last_str = None
288             last_time = 0
289             while True:
290                 yield deferral.sleep(3)
291                 try:
292                     height = node.tracker.get_height(node.best_share_var.value)
293                     this_str = 'P2Pool: %i shares in chain (%i verified/%i total) Peers: %i (%i incoming)' % (
294                         height,
295                         len(node.tracker.verified.items),
296                         len(node.tracker.items),
297                         len(node.p2p_node.peers),
298                         sum(1 for peer in node.p2p_node.peers.itervalues() if peer.incoming),
299                     ) + (' FDs: %i R/%i W' % (len(reactor.getReaders()), len(reactor.getWriters())) if p2pool.DEBUG else '')
300                     
301                     datums, dt = wb.local_rate_monitor.get_datums_in_last()
302                     my_att_s = sum(datum['work']/dt for datum in datums)
303                     this_str += '\n Local: %sH/s in last %s Local dead on arrival: %s Expected time to share: %s' % (
304                         math.format(int(my_att_s)),
305                         math.format_dt(dt),
306                         math.format_binomial_conf(sum(1 for datum in datums if datum['dead']), len(datums), 0.95),
307                         math.format_dt(2**256 / node.tracker.items[node.best_share_var.value].max_target / my_att_s) if my_att_s and node.best_share_var.value else '???',
308                     )
309                     
310                     if height > 2:
311                         (stale_orphan_shares, stale_doa_shares), shares, _ = wb.get_stale_counts()
312                         stale_prop = p2pool_data.get_average_stale_prop(node.tracker, node.best_share_var.value, min(60*60//net.SHARE_PERIOD, height))
313                         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)
314                         
315                         this_str += '\n Shares: %i (%i orphan, %i dead) Stale rate: %s Efficiency: %s Current payout: %.4f %s' % (
316                             shares, stale_orphan_shares, stale_doa_shares,
317                             math.format_binomial_conf(stale_orphan_shares + stale_doa_shares, shares, 0.95),
318                             math.format_binomial_conf(stale_orphan_shares + stale_doa_shares, shares, 0.95, lambda x: (1 - x)/(1 - stale_prop)),
319                             node.get_current_txouts().get(bitcoin_data.pubkey_hash_to_script2(my_pubkey_hash), 0)*1e-8, net.PARENT.SYMBOL,
320                         )
321                         this_str += '\n Pool: %sH/s Stale rate: %.1f%% Expected time to block: %s' % (
322                             math.format(int(real_att_s)),
323                             100*stale_prop,
324                             math.format_dt(2**256 / node.bitcoind_work.value['bits'].target / real_att_s),
325                         )
326                         
327                         for warning in p2pool_data.get_warnings(node.tracker, node.best_share_var.value, net, bitcoind_warning_var.value, node.bitcoind_work.value):
328                             print >>sys.stderr, '#'*40
329                             print >>sys.stderr, '>>> Warning: ' + warning
330                             print >>sys.stderr, '#'*40
331                     
332                     if this_str != last_str or time.time() > last_time + 15:
333                         print this_str
334                         last_str = this_str
335                         last_time = time.time()
336                 except:
337                     log.err()
338         status_thread()
339     except:
340         reactor.stop()
341         log.err(None, 'Fatal error:')
342
343 def run():
344     realnets = dict((name, net) for name, net in networks.nets.iteritems() if '_testnet' not in name)
345     
346     parser = fixargparse.FixedArgumentParser(description='p2pool (version %s)' % (p2pool.__version__,), fromfile_prefix_chars='@')
347     parser.add_argument('--version', action='version', version=p2pool.__version__)
348     parser.add_argument('--net',
349         help='use specified network (default: bitcoin)',
350         action='store', choices=sorted(realnets), default='bitcoin', dest='net_name')
351     parser.add_argument('--testnet',
352         help='''use the network's testnet''',
353         action='store_const', const=True, default=False, dest='testnet')
354     parser.add_argument('--debug',
355         help='enable debugging mode',
356         action='store_const', const=True, default=False, dest='debug')
357     parser.add_argument('-a', '--address',
358         help='generate payouts to this address (default: <address requested from bitcoind>)',
359         type=str, action='store', default=None, dest='address')
360     parser.add_argument('--datadir',
361         help='store data in this directory (default: <directory run_p2pool.py is in>/data)',
362         type=str, action='store', default=None, dest='datadir')
363     parser.add_argument('--logfile',
364         help='''log to this file (default: data/<NET>/log)''',
365         type=str, action='store', default=None, dest='logfile')
366     parser.add_argument('--merged',
367         help='call getauxblock on this url to get work for merged mining (example: http://ncuser:ncpass@127.0.0.1:10332/)',
368         type=str, action='append', default=[], dest='merged_urls')
369     parser.add_argument('--give-author', metavar='DONATION_PERCENTAGE',
370         help='donate this percentage of work towards the development of p2pool (default: 0.5)',
371         type=float, action='store', default=0.5, dest='donation_percentage')
372     parser.add_argument('--iocp',
373         help='use Windows IOCP API in order to avoid errors due to large number of sockets being open',
374         action='store_true', default=False, dest='iocp')
375     parser.add_argument('--irc-announce',
376         help='announce any blocks found on irc://irc.freenode.net/#p2pool',
377         action='store_true', default=False, dest='irc_announce')
378     parser.add_argument('--no-bugreport',
379         help='disable submitting caught exceptions to the author',
380         action='store_true', default=False, dest='no_bugreport')
381     
382     p2pool_group = parser.add_argument_group('p2pool interface')
383     p2pool_group.add_argument('--p2pool-port', metavar='PORT',
384         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())),
385         type=int, action='store', default=None, dest='p2pool_port')
386     p2pool_group.add_argument('-n', '--p2pool-node', metavar='ADDR[:PORT]',
387         help='connect to existing p2pool node at ADDR listening on port PORT (defaults to default p2pool P2P port) in addition to builtin addresses',
388         type=str, action='append', default=[], dest='p2pool_nodes')
389     parser.add_argument('--disable-upnp',
390         help='''don't attempt to use UPnP to forward p2pool's P2P port from the Internet to this computer''',
391         action='store_false', default=True, dest='upnp')
392     p2pool_group.add_argument('--max-conns', metavar='CONNS',
393         help='maximum incoming connections (default: 40)',
394         type=int, action='store', default=40, dest='p2pool_conns')
395     
396     worker_group = parser.add_argument_group('worker interface')
397     worker_group.add_argument('-w', '--worker-port', metavar='PORT or ADDR:PORT',
398         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())),
399         type=str, action='store', default=None, dest='worker_endpoint')
400     worker_group.add_argument('-f', '--fee', metavar='FEE_PERCENTAGE',
401         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)''',
402         type=float, action='store', default=0, dest='worker_fee')
403     
404     bitcoind_group = parser.add_argument_group('bitcoind interface')
405     bitcoind_group.add_argument('--bitcoind-address', metavar='BITCOIND_ADDRESS',
406         help='connect to this address (default: 127.0.0.1)',
407         type=str, action='store', default='127.0.0.1', dest='bitcoind_address')
408     bitcoind_group.add_argument('--bitcoind-rpc-port', metavar='BITCOIND_RPC_PORT',
409         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())),
410         type=int, action='store', default=None, dest='bitcoind_rpc_port')
411     bitcoind_group.add_argument('--bitcoind-rpc-ssl',
412         help='connect to JSON-RPC interface using SSL',
413         action='store_true', default=False, dest='bitcoind_rpc_ssl')
414     bitcoind_group.add_argument('--bitcoind-p2p-port', metavar='BITCOIND_P2P_PORT',
415         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())),
416         type=int, action='store', default=None, dest='bitcoind_p2p_port')
417     
418     bitcoind_group.add_argument(metavar='BITCOIND_RPCUSERPASS',
419         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)',
420         type=str, action='store', default=[], nargs='*', dest='bitcoind_rpc_userpass')
421     
422     args = parser.parse_args()
423     
424     if args.debug:
425         p2pool.DEBUG = True
426         defer.setDebugging(True)
427     
428     net_name = args.net_name + ('_testnet' if args.testnet else '')
429     net = networks.nets[net_name]
430     
431     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)
432     if not os.path.exists(datadir_path):
433         os.makedirs(datadir_path)
434     
435     if len(args.bitcoind_rpc_userpass) > 2:
436         parser.error('a maximum of two arguments are allowed')
437     args.bitcoind_rpc_username, args.bitcoind_rpc_password = ([None, None] + args.bitcoind_rpc_userpass)[-2:]
438     
439     if args.bitcoind_rpc_password is None:
440         conf_path = net.PARENT.CONF_FILE_FUNC()
441         if not os.path.exists(conf_path):
442             parser.error('''Bitcoin configuration file not found. Manually enter your RPC password.\r\n'''
443                 '''If you actually haven't created a configuration file, you should create one at %s with the text:\r\n'''
444                 '''\r\n'''
445                 '''server=1\r\n'''
446                 '''rpcpassword=%x\r\n'''
447                 '''\r\n'''
448                 '''Keep that password secret! After creating the file, restart Bitcoin.''' % (conf_path, random.randrange(2**128)))
449         conf = open(conf_path, 'rb').read()
450         contents = {}
451         for line in conf.splitlines(True):
452             if '#' in line:
453                 line = line[:line.index('#')]
454             if '=' not in line:
455                 continue
456             k, v = line.split('=', 1)
457             contents[k.strip()] = v.strip()
458         for conf_name, var_name, var_type in [
459             ('rpcuser', 'bitcoind_rpc_username', str),
460             ('rpcpassword', 'bitcoind_rpc_password', str),
461             ('rpcport', 'bitcoind_rpc_port', int),
462             ('port', 'bitcoind_p2p_port', int),
463         ]:
464             if getattr(args, var_name) is None and conf_name in contents:
465                 setattr(args, var_name, var_type(contents[conf_name]))
466         if args.bitcoind_rpc_password is None:
467             parser.error('''Bitcoin configuration file didn't contain an rpcpassword= line! Add one!''')
468     
469     if args.bitcoind_rpc_username is None:
470         args.bitcoind_rpc_username = ''
471     
472     if args.bitcoind_rpc_port is None:
473         args.bitcoind_rpc_port = net.PARENT.RPC_PORT
474     
475     if args.bitcoind_p2p_port is None:
476         args.bitcoind_p2p_port = net.PARENT.P2P_PORT
477     
478     if args.p2pool_port is None:
479         args.p2pool_port = net.P2P_PORT
480     
481     if args.worker_endpoint is None:
482         worker_endpoint = '', net.WORKER_PORT
483     elif ':' not in args.worker_endpoint:
484         worker_endpoint = '', int(args.worker_endpoint)
485     else:
486         addr, port = args.worker_endpoint.rsplit(':', 1)
487         worker_endpoint = addr, int(port)
488     
489     if args.address is not None:
490         try:
491             args.pubkey_hash = bitcoin_data.address_to_pubkey_hash(args.address, net.PARENT)
492         except Exception, e:
493             parser.error('error parsing address: ' + repr(e))
494     else:
495         args.pubkey_hash = None
496     
497     def separate_url(url):
498         s = urlparse.urlsplit(url)
499         if '@' not in s.netloc:
500             parser.error('merged url netloc must contain an "@"')
501         userpass, new_netloc = s.netloc.rsplit('@', 1)
502         return urlparse.urlunsplit(s._replace(netloc=new_netloc)), userpass
503     merged_urls = map(separate_url, args.merged_urls)
504     
505     if args.logfile is None:
506         args.logfile = os.path.join(datadir_path, 'log')
507     
508     logfile = logging.LogFile(args.logfile)
509     pipe = logging.TimestampingPipe(logging.TeePipe([logging.EncodeReplacerPipe(sys.stderr), logfile]))
510     sys.stdout = logging.AbortPipe(pipe)
511     sys.stderr = log.DefaultObserver.stderr = logging.AbortPipe(logging.PrefixPipe(pipe, '> '))
512     if hasattr(signal, "SIGUSR1"):
513         def sigusr1(signum, frame):
514             print 'Caught SIGUSR1, closing %r...' % (args.logfile,)
515             logfile.reopen()
516             print '...and reopened %r after catching SIGUSR1.' % (args.logfile,)
517         signal.signal(signal.SIGUSR1, sigusr1)
518     task.LoopingCall(logfile.reopen).start(5)
519     
520     class ErrorReporter(object):
521         def __init__(self):
522             self.last_sent = None
523         
524         def emit(self, eventDict):
525             if not eventDict["isError"]:
526                 return
527             
528             if self.last_sent is not None and time.time() < self.last_sent + 5:
529                 return
530             self.last_sent = time.time()
531             
532             if 'failure' in eventDict:
533                 text = ((eventDict.get('why') or 'Unhandled Error')
534                     + '\n' + eventDict['failure'].getTraceback())
535             else:
536                 text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
537             
538             from twisted.web import client
539             client.getPage(
540                 url='http://u.forre.st/p2pool_error.cgi',
541                 method='POST',
542                 postdata=p2pool.__version__ + ' ' + net.NAME + '\n' + text,
543                 timeout=15,
544             ).addBoth(lambda x: None)
545     if not args.no_bugreport:
546         log.addObserver(ErrorReporter().emit)
547     
548     reactor.callWhenRunning(main, args, net, datadir_path, merged_urls, worker_endpoint)
549     reactor.run()