config: fix client config (no SERVER)
[hippotat] / hippotatlib / __init__.py
CommitLineData
b0cfbfce 1# -*- python -*-
0256fc10
IJ
2#
3# Hippotat - Asinine IP Over HTTP program
4# hippotatlib/__init__.py - common library code
5#
6# Copyright 2017 Ian Jackson
7#
f85d143f 8# GPLv3+
0256fc10 9#
f85d143f
IJ
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
0256fc10 14#
f85d143f
IJ
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program, in the file GPLv3. If not,
22# see <http://www.gnu.org/licenses/>.
23
b0cfbfce 24
37ab4cdc
IJ
25import signal
26signal.signal(signal.SIGINT, signal.SIG_DFL)
27
1321ad5f 28import sys
cae50358 29import os
1321ad5f 30
b83d422a
IJ
31from zope.interface import implementer
32
040ff511
IJ
33import twisted
34from twisted.internet import reactor
1d023c89 35import twisted.internet.endpoints
8c3b6620
IJ
36import twisted.logger
37from twisted.logger import LogLevel
38import twisted.python.constants
39from twisted.python.constants import NamedConstant
b0cfbfce
IJ
40
41import ipaddress
42from ipaddress import AddressValueError
43
ae7c7784 44from optparse import OptionParser
5510890e 45import configparser
ae7c7784
IJ
46from configparser import ConfigParser
47from configparser import NoOptionError
48
c13ee6e6
IJ
49from functools import partial
50
ae7c7784 51import collections
84e763c7 52import time
8c3b6620 53import codecs
eedc8b30 54import traceback
ae7c7784 55
1321ad5f
IJ
56import re as regexp
57
5a37bac8 58import hippotatlib.slip as slip
1321ad5f 59
d579a048 60class DBG(twisted.python.constants.Names):
380ed56c 61 INIT = NamedConstant()
cae50358 62 CONFIG = NamedConstant()
d579a048 63 ROUTE = NamedConstant()
b68c0739 64 DROP = NamedConstant()
4a780703 65 OWNSOURCE = NamedConstant()
d579a048
IJ
66 FLOW = NamedConstant()
67 HTTP = NamedConstant()
380ed56c 68 TWISTED = NamedConstant()
d579a048 69 QUEUE = NamedConstant()
380ed56c 70 HTTP_CTRL = NamedConstant()
d579a048 71 QUEUE_CTRL = NamedConstant()
297b3ebf 72 HTTP_FULL = NamedConstant()
0accf0d3 73 CTRL_DUMP = NamedConstant()
380ed56c 74 SLIP_FULL = NamedConstant()
9acb0eca 75 DATA_COMPLETE = NamedConstant()
d579a048 76
b68c0739 77_hex_codec = codecs.getencoder('hex_codec')
8c3b6620 78
b83d422a
IJ
79#---------- logging ----------
80
81org_stderr = sys.stderr
82
8c3b6620
IJ
83log = twisted.logger.Logger()
84
2e68eb10
IJ
85debug_set = set()
86debug_def_detail = DBG.HTTP
3e35fc99 87
8c3b6620 88def log_debug(dflag, msg, idof=None, d=None):
3e35fc99 89 if dflag not in debug_set: return
e8fcf3b7 90 #print('---------------->',repr((dflag, msg, idof, d)), file=sys.stderr)
8c3b6620 91 if idof is not None:
e8ed0029 92 msg = '[%#x] %s' % (id(idof), msg)
8c3b6620 93 if d is not None:
9acb0eca
IJ
94 trunc = ''
95 if not DBG.DATA_COMPLETE in debug_set:
96 if len(d) > 64:
97 d = d[0:64]
98 trunc = '...'
b68c0739 99 d = _hex_codec(d)[0].decode('ascii')
9acb0eca 100 msg += ' ' + d + trunc
8c3b6620
IJ
101 log.info('{dflag} {msgcore}', dflag=dflag, msgcore=msg)
102
80e963a1
IJ
103def logevent_is_boringtwisted(event):
104 try:
105 if event.get('log_level') != LogLevel.info:
106 return False
107 dflag = event.get('dflag')
108 if dflag is False : return False
109 if dflag in debug_set: return False
110 if dflag is None and DBG.TWISTED in debug_set: return False
111 return True
112 except Exception:
113 print(traceback.format_exc(), file=org_stderr)
114 return False
115
b83d422a
IJ
116@implementer(twisted.logger.ILogFilterPredicate)
117class LogNotBoringTwisted:
118 def __call__(self, event):
80e963a1
IJ
119 return (
120 twisted.logger.PredicateResult.no
121 if logevent_is_boringtwisted(event) else
122 twisted.logger.PredicateResult.yes
123 )
b83d422a
IJ
124
125#---------- default config ----------
126
ca732796
IJ
127defcfg = '''
128[DEFAULT]
9e445690
IJ
129max_batch_down = 65536
130max_queue_time = 10
131target_requests_outstanding = 3
132http_timeout = 30
133http_timeout_grace = 5
134max_requests_outstanding = 6
135max_batch_up = 4000
136http_retry = 5
c7fb640e 137port = 80
8d374606 138vroutes = ''
ca732796
IJ
139
140#[server] or [<client>] overrides
141ipif = userv root ipif %(local)s,%(peer)s,%(mtu)s,slip %(rnets)s
ca732796 142
9e445690 143# relating to virtual network
ca732796 144mtu = 1500
ca732796 145
9e445690 146# addrs = 127.0.0.1 ::1
9e445690
IJ
147# url
148
149# relating to virtual network
8d374606
IJ
150vvnetwork = 172.24.230.192
151# vnetwork = <prefix>/<len>
152# vadd r = <ipaddr>
153# vrelay = <ipaddr>
9e445690 154
ca732796
IJ
155
156# [<client-ip4-or-ipv6-address>]
157# password = <password> # used by both, must match
158
c7fb640e 159[LIMIT]
9e445690
IJ
160max_batch_down = 262144
161max_queue_time = 121
162http_timeout = 121
163target_requests_outstanding = 10
ca732796
IJ
164'''
165
87a7c0c7 166# these need to be defined here so that they can be imported by import *
cae50358 167cfg = ConfigParser(strict=False)
ae7c7784
IJ
168optparser = OptionParser()
169
e4006ac4 170_mimetrans = bytes.maketrans(b'-'+slip.esc, slip.esc+b'-')
7b07f0b5
IJ
171def mime_translate(s):
172 # SLIP-encoded packets cannot contain ESC ESC.
173 # Swap `-' and ESC. The result cannot contain `--'
174 return s.translate(_mimetrans)
175
87a7c0c7 176class ConfigResults:
c7fb640e
IJ
177 def __init__(self):
178 pass
87a7c0c7
IJ
179 def __repr__(self):
180 return 'ConfigResults('+repr(self.__dict__)+')'
181
a8827d59 182def log_discard(packet, iface, saddr, daddr, why):
b68c0739 183 log_debug(DBG.DROP,
a8827d59 184 'discarded packet [%s] %s -> %s: %s' % (iface, saddr, daddr, why),
b68c0739 185 d=packet)
1321ad5f 186
b0cfbfce
IJ
187#---------- packet parsing ----------
188
189def packet_addrs(packet):
190 version = packet[0] >> 4
191 if version == 4:
192 addrlen = 4
193 saddroff = 3*4
194 factory = ipaddress.IPv4Address
195 elif version == 6:
196 addrlen = 16
197 saddroff = 2*4
198 factory = ipaddress.IPv6Address
199 else:
200 raise ValueError('unsupported IP version %d' % version)
201 saddr = factory(packet[ saddroff : saddroff + addrlen ])
202 daddr = factory(packet[ saddroff + addrlen : saddroff + addrlen*2 ])
203 return (saddr, daddr)
204
205#---------- address handling ----------
206
207def ipaddr(input):
208 try:
209 r = ipaddress.IPv4Address(input)
210 except AddressValueError:
211 r = ipaddress.IPv6Address(input)
212 return r
213
214def ipnetwork(input):
215 try:
216 r = ipaddress.IPv4Network(input)
217 except NetworkValueError:
218 r = ipaddress.IPv6Network(input)
219 return r
040ff511
IJ
220
221#---------- ipif (SLIP) subprocess ----------
222
a95cfeb2 223class SlipStreamDecoder():
db6ba584 224 def __init__(self, desc, on_packet):
040ff511 225 self._buffer = b''
a95cfeb2 226 self._on_packet = on_packet
db6ba584
IJ
227 self._desc = desc
228 self._log('__init__')
229
230 def _log(self, msg, **kwargs):
3297cac1 231 log_debug(DBG.SLIP_FULL, 'slip %s: %s' % (self._desc, msg), **kwargs)
a95cfeb2
IJ
232
233 def inputdata(self, data):
db6ba584 234 self._log('inputdata', d=data)
7fa9c132
IJ
235 data = self._buffer + data
236 self._buffer = b''
237 packets = slip.decode(data, True)
040ff511
IJ
238 self._buffer = packets.pop()
239 for packet in packets:
a95cfeb2 240 self._maybe_packet(packet)
54890d4d 241 self._log('bufremain', d=self._buffer)
a95cfeb2
IJ
242
243 def _maybe_packet(self, packet):
54890d4d 244 self._log('maybepacket', d=packet)
db6ba584
IJ
245 if len(packet):
246 self._on_packet(packet)
a95cfeb2 247
4f991c0c 248 def flush(self):
54890d4d 249 self._log('flush')
7fa9c132 250 data = self._buffer
a95cfeb2 251 self._buffer = b''
7fa9c132
IJ
252 packets = slip.decode(data)
253 assert(len(packets) == 1)
254 self._maybe_packet(packets[0])
4f991c0c 255
e4006ac4 256class _IpifProcessProtocol(twisted.internet.protocol.ProcessProtocol):
4f991c0c
IJ
257 def __init__(self, router):
258 self._router = router
db6ba584 259 self._decoder = SlipStreamDecoder('ipif', self.slip_on_packet)
a95cfeb2
IJ
260 def connectionMade(self): pass
261 def outReceived(self, data):
262 self._decoder.inputdata(data)
263 def slip_on_packet(self, packet):
4f991c0c
IJ
264 (saddr, daddr) = packet_addrs(packet)
265 if saddr.is_link_local or daddr.is_link_local:
a8827d59 266 log_discard(packet, 'ipif', saddr, daddr, 'link-local')
4f991c0c
IJ
267 return
268 self._router(packet, saddr, daddr)
040ff511
IJ
269 def processEnded(self, status):
270 status.raiseException()
271
272def start_ipif(command, router):
040ff511
IJ
273 ipif = _IpifProcessProtocol(router)
274 reactor.spawnProcess(ipif,
275 '/bin/sh',['sh','-xc', command],
ff613365
IJ
276 childFDs={0:'w', 1:'r', 2:2},
277 env=None)
909e0ff3 278 return ipif
040ff511 279
909e0ff3 280def queue_inbound(ipif, packet):
15407d80 281 log_debug(DBG.FLOW, "queue_inbound", d=packet)
040ff511
IJ
282 ipif.transport.write(slip.delimiter)
283 ipif.transport.write(slip.encode(packet))
284 ipif.transport.write(slip.delimiter)
285
650a3251
IJ
286#---------- packet queue ----------
287
288class PacketQueue():
d579a048
IJ
289 def __init__(self, desc, max_queue_time):
290 self._desc = desc
8718b02c 291 assert(desc + '')
650a3251
IJ
292 self._max_queue_time = max_queue_time
293 self._pq = collections.deque() # packets
294
b68c0739 295 def _log(self, dflag, msg, **kwargs):
8c3b6620 296 log_debug(dflag, self._desc+' pq: '+msg, **kwargs)
d579a048 297
650a3251 298 def append(self, packet):
8c3b6620 299 self._log(DBG.QUEUE, 'append', d=packet)
650a3251
IJ
300 self._pq.append((time.monotonic(), packet))
301
302 def nonempty(self):
8c3b6620 303 self._log(DBG.QUEUE, 'nonempty ?')
650a3251
IJ
304 while True:
305 try: (queuetime, packet) = self._pq[0]
8c3b6620
IJ
306 except IndexError:
307 self._log(DBG.QUEUE, 'nonempty ? empty.')
308 return False
650a3251
IJ
309
310 age = time.monotonic() - queuetime
84e763c7 311 if age > self._max_queue_time:
650a3251 312 # strip old packets off the front
8c3b6620 313 self._log(DBG.QUEUE, 'dropping (old)', d=packet)
650a3251
IJ
314 self._pq.popleft()
315 continue
316
8c3b6620 317 self._log(DBG.QUEUE, 'nonempty ? nonempty.')
650a3251
IJ
318 return True
319
7b07f0b5
IJ
320 def process(self, sizequery, moredata, max_batch):
321 # sizequery() should return size of batch so far
322 # moredata(s) should add s to batch
8c3b6620 323 self._log(DBG.QUEUE, 'process...')
7b07f0b5
IJ
324 while True:
325 try: (dummy, packet) = self._pq[0]
8c3b6620
IJ
326 except IndexError:
327 self._log(DBG.QUEUE, 'process... empty')
328 break
329
330 self._log(DBG.QUEUE_CTRL, 'process... packet', d=packet)
7b07f0b5
IJ
331
332 encoded = slip.encode(packet)
333 sofar = sizequery()
334
8c3b6620
IJ
335 self._log(DBG.QUEUE_CTRL,
336 'process... (sofar=%d, max=%d) encoded' % (sofar, max_batch),
b68c0739 337 d=encoded)
8c3b6620 338
7b07f0b5
IJ
339 if sofar > 0:
340 if sofar + len(slip.delimiter) + len(encoded) > max_batch:
8c3b6620 341 self._log(DBG.QUEUE_CTRL, 'process... overflow')
7b07f0b5
IJ
342 break
343 moredata(slip.delimiter)
344
345 moredata(encoded)
84e763c7 346 self._pq.popleft()
ae7c7784
IJ
347
348#---------- error handling ----------
349
b68c0739
IJ
350_crashing = False
351
ae7c7784 352def crash(err):
b68c0739
IJ
353 global _crashing
354 _crashing = True
e8ed0029
IJ
355 print('========== CRASH ==========', err,
356 '===========================', file=sys.stderr)
ae7c7784
IJ
357 try: reactor.stop()
358 except twisted.internet.error.ReactorNotRunning: pass
359
360def crash_on_defer(defer):
361 defer.addErrback(lambda err: crash(err))
362
e4006ac4 363def crash_on_critical(event):
ae7c7784
IJ
364 if event.get('log_level') >= LogLevel.critical:
365 crash(twisted.logger.formatEvent(event))
366
87a7c0c7
IJ
367#---------- config processing ----------
368
c7fb640e
IJ
369def _cfg_process_putatives():
370 servers = { }
371 clients = { }
372 # maps from abstract object to canonical name for cs's
87a7c0c7 373
c7fb640e
IJ
374 def putative(cmap, abstract, canoncs):
375 try:
376 current_canoncs = cmap[abstract]
377 except KeyError:
378 pass
379 else:
380 assert(current_canoncs == canoncs)
381 cmap[abstract] = canoncs
382
383 server_pat = r'[-.0-9A-Za-z]+'
384 client_pat = r'[.:0-9a-f]+'
385 server_re = regexp.compile(server_pat)
386 serverclient_re = regexp.compile(server_pat + r' ' + client_pat)
88487243 387
c7fb640e 388 for cs in cfg.sections():
8d374606 389 if cs == 'LIMIT':
c7fb640e
IJ
390 # plan A "[LIMIT]"
391 continue
88487243 392
c7fb640e
IJ
393 try:
394 # plan B "[<client>]" part 1
395 ci = ipaddr(cs)
396 except AddressValueError:
88487243 397
c7fb640e
IJ
398 if server_re.fullmatch(cs):
399 # plan C "[<servername>]"
400 putative(servers, cs, cs)
401 continue
402
403 if serverclient_re.fullmatch(cs):
404 # plan D "[<servername> <client>]" part 1
405 (pss,pcs) = cs.split(' ')
406
8d374606 407 if pcs == 'LIMIT':
c7fb640e
IJ
408 # plan E "[<servername> LIMIT]"
409 continue
410
411 try:
412 # plan D "[<servername> <client>]" part 2
413 ci = ipaddr(pc)
414 except AddressValueError:
415 # plan F "[<some thing we do not understand>]"
416 # well, we ignore this
417 print('warning: ignoring config section %s' % cs, file=sys.stderr)
418 continue
419
420 else: # no AddressValueError
421 # plan D "[<servername> <client]" part 3
422 putative(clients, ci, pcs)
423 putative(servers, pss, pss)
424 continue
425
426 else: # no AddressValueError
427 # plan B "[<client>" part 2
428 putative(clients, ci, cs)
429 continue
430
431 return (servers, clients)
432
74934d63 433def cfg_process_common(c, ss):
c7fb640e
IJ
434 c.mtu = cfg.getint(ss, 'mtu')
435
436def cfg_process_saddrs(c, ss):
437 class ServerAddr():
438 def __init__(self, port, addrspec):
439 self.port = port
440 # also self.addr
441 try:
442 self.addr = ipaddress.IPv4Address(addrspec)
443 self._endpointfactory = twisted.internet.endpoints.TCP4ServerEndpoint
444 self._inurl = b'%s'
445 except AddressValueError:
446 self.addr = ipaddress.IPv6Address(addrspec)
447 self._endpointfactory = twisted.internet.endpoints.TCP6ServerEndpoint
448 self._inurl = b'[%s]'
449 def make_endpoint(self):
3b69fba9
IJ
450 return self._endpointfactory(reactor, self.port,
451 interface= '%s' % self.addr)
c7fb640e
IJ
452 def url(self):
453 url = b'http://' + (self._inurl % str(self.addr).encode('ascii'))
454 if self.port != 80: url += b':%d' % self.port
455 url += b'/'
456 return url
3b69fba9
IJ
457 def __repr__(self):
458 return 'ServerAddr'+repr((self.port,self.addr))
c7fb640e
IJ
459
460 c.port = cfg.getint(ss,'port')
461 c.saddrs = [ ]
462 for addrspec in cfg.get(ss, 'addrs').split():
463 sa = ServerAddr(c.port, addrspec)
464 c.saddrs.append(sa)
465
466def cfg_process_vnetwork(c, ss):
c7f134ce
IJ
467 c.vnetwork = ipnetwork(cfg.get(ss,'vnetwork'))
468 if c.vnetwork.num_addresses < 3 + 2:
469 raise ValueError('vnetwork needs at least 2^3 addresses')
88487243 470
8d374606 471def cfg_process_vaddr(c, ss):
88487243 472 try:
c7f134ce 473 c.vaddr = cfg.get(ss,'vaddr')
88487243 474 except NoOptionError:
8d374606 475 cfg_process_vnetwork(c, ss)
c7f134ce 476 c.vaddr = next(c.vnetwork.hosts())
88487243 477
c7fb640e
IJ
478def cfg_search_section(key,sections):
479 for section in sections:
480 if cfg.has_option(section, key):
481 return section
8d374606 482 raise NoOptionError(key, repr(sections))
c7fb640e
IJ
483
484def cfg_search(getter,key,sections):
485 section = cfg_search_section(key,sections)
486 return getter(section, key)
487
488def cfg_process_client_limited(cc,ss,sections,key):
489 val = cfg_search(cfg.getint, key, sections)
8d374606 490 lim = cfg_search(cfg.getint, key, ['%s LIMIT' % ss, 'LIMIT'])
c7fb640e
IJ
491 cc.__dict__[key] = min(val,lim)
492
493def cfg_process_client_common(cc,ss,cs,ci):
494 # returns sections to search in, iff password is defined, otherwise None
495 cc.ci = ci
496
8d374606 497 sections = ['%s %s' % (ss,cs),
c7fb640e
IJ
498 cs,
499 ss,
500 'DEFAULT']
501
502 try: pwsection = cfg_search_section('password', sections)
503 except NoOptionError: return None
88487243 504
c7fb640e 505 pw = cfg.get(pwsection, 'password')
c7f134ce 506 cc.password = pw.encode('utf-8')
88487243 507
c7fb640e
IJ
508 cfg_process_client_limited(cc,ss,sections,'target_requests_outstanding')
509 cfg_process_client_limited(cc,ss,sections,'http_timeout')
88487243 510
c7fb640e
IJ
511 return sections
512
8d374606 513def cfg_process_ipif(c, sections, varmap):
c7fb640e
IJ
514 for d, s in varmap:
515 try: v = getattr(c, s)
516 except AttributeError: continue
517 setattr(c, d, v)
518
c7f134ce 519 #print('CFGIPIF',repr((varmap, sections, c.__dict__)),file=sys.stderr)
8d374606 520
c7fb640e
IJ
521 section = cfg_search_section('ipif', sections)
522 c.ipif_command = cfg.get(section,'ipif', vars=c.__dict__)
88487243 523
ae7c7784
IJ
524#---------- startup ----------
525
8c771381
IJ
526def log_debug_config(m):
527 if not DBG.CONFIG in debug_set: return
528 print('DBG.CONFIG:', m)
529
5510890e 530def common_startup(process_cfg):
c7fb640e
IJ
531 # calls process_cfg(putative_clients, putative_servers)
532
82302bac 533 # ConfigParser hates #-comments after values
c7fb640e 534 trailingcomments_re = regexp.compile(r'#.*')
82302bac 535 cfg.read_string(trailingcomments_re.sub('', defcfg))
cae50358
IJ
536 need_defcfg = True
537
538 def readconfig(pathname, mandatory=True):
539 def log(m, p=pathname):
540 if not DBG.CONFIG in debug_set: return
8c771381 541 log_debug_config('%s: %s' % (m, pathname))
cae50358
IJ
542
543 try:
544 files = os.listdir(pathname)
545
546 except FileNotFoundError:
547 if mandatory: raise
548 log('skipped')
549 return
550
551 except NotADirectoryError:
552 cfg.read(pathname)
553 log('read file')
554 return
555
556 # is a directory
557 log('directory')
558 re = regexp.compile('[^-A-Za-z0-9_]')
2b13e1cc 559 for f in os.listdir(pathname):
cae50358
IJ
560 if re.search(f): continue
561 subpath = pathname + '/' + f
562 try:
563 os.stat(subpath)
564 except FileNotFoundError:
565 log('entry skipped', subpath)
566 continue
567 cfg.read(subpath)
568 log('entry read', subpath)
569
570 def oc_config(od,os, value, op):
571 nonlocal need_defcfg
572 need_defcfg = False
573 readconfig(value)
2e68eb10 574
26f04eff
IJ
575 def oc_extra_config(od,os, value, op):
576 readconfig(value)
577
7852bfc8
IJ
578 def read_defconfig():
579 readconfig('/etc/hippotat/config.d', False)
580 readconfig('/etc/hippotat/passwords.d', False)
581 readconfig('/etc/hippotat/master.cfg', False)
582
26f04eff
IJ
583 def oc_defconfig(od,os, value, op):
584 nonlocal need_defcfg
585 need_defcfg = False
586 read_defconfig(value)
587
9acb0eca
IJ
588 def dfs_less_detailed(dl):
589 return [df for df in DBG.iterconstants() if df <= dl]
590
591 def ds_default(od,os,dl,op):
2e68eb10 592 global debug_set
ff0fc3fa
IJ
593 debug_set.clear
594 debug_set |= set(dfs_less_detailed(debug_def_detail))
2e68eb10 595
9acb0eca 596 def ds_select(od,os, spec, op):
9acb0eca
IJ
597 for it in spec.split(','):
598
9acb0eca
IJ
599 if it.startswith('-'):
600 mutator = debug_set.discard
601 it = it[1:]
602 else:
603 mutator = debug_set.add
2cf75145
IJ
604
605 if it == '+':
606 dfs = DBG.iterconstants()
607
608 else:
609 if it.endswith('+'):
610 mapper = dfs_less_detailed
611 it = it[0:len(it)-1]
612 else:
613 mapper = lambda x: [x]
614
615 try:
616 dfspec = DBG.lookupByName(it)
617 except ValueError:
618 optparser.error('unknown debug flag %s in --debug-select' % it)
619
620 dfs = mapper(dfspec)
621
622 for df in dfs:
623 mutator(df)
9acb0eca
IJ
624
625 optparser.add_option('-D', '--debug',
2e68eb10
IJ
626 nargs=0,
627 action='callback',
9acb0eca
IJ
628 help='enable default debug (to stdout)',
629 callback= ds_default)
630
631 optparser.add_option('--debug-select',
632 nargs=1,
633 type='string',
2cf75145 634 metavar='[-]DFLAG[+]|[-]+,...',
9acb0eca 635 help=
2cf75145
IJ
636'''enable (`-': disable) each specified DFLAG;
637`+': do same for all "more interesting" DFLAGSs;
638just `+': all DFLAGs.
639 DFLAGS: ''' + ' '.join([df.name for df in DBG.iterconstants()]),
9acb0eca
IJ
640 action='callback',
641 callback= ds_select)
2e68eb10 642
cae50358
IJ
643 optparser.add_option('-c', '--config',
644 nargs=1,
645 type='string',
646 metavar='CONFIGFILE',
647 dest='configfile',
648 action='callback',
649 callback= oc_config)
650
26f04eff
IJ
651 optparser.add_option('--extra-config',
652 nargs=1,
653 type='string',
654 metavar='CONFIGFILE',
655 dest='configfile',
656 action='callback',
657 callback= oc_extra_config)
658
659 optparser.add_option('--default-config',
660 action='callback',
661 callback= oc_defconfig)
662
f022d67f
IJ
663 (opts, args) = optparser.parse_args()
664 if len(args): optparser.error('no non-option arguments please')
2e68eb10 665
cae50358 666 if need_defcfg:
7852bfc8 667 read_defconfig()
9acb0eca 668
c7fb640e 669 try:
8d374606 670 (pss, pcs) = _cfg_process_putatives()
1cc6968f 671 process_cfg(opts, pss, pcs)
5510890e
IJ
672 except (configparser.Error, ValueError):
673 traceback.print_exc(file=sys.stderr)
674 print('\nInvalid configuration, giving up.', file=sys.stderr)
675 sys.exit(12)
676
ff0fc3fa
IJ
677
678 #print('X', debug_set, file=sys.stderr)
2e68eb10 679
8c3b6620 680 log_formatter = twisted.logger.formatEventAsClassicLogText
389236df
IJ
681 stdout_obs = twisted.logger.FileLogObserver(sys.stdout, log_formatter)
682 stderr_obs = twisted.logger.FileLogObserver(sys.stderr, log_formatter)
683 pred = twisted.logger.LogLevelFilterPredicate(LogLevel.error)
b83d422a 684 stdsomething_obs = twisted.logger.FilteringLogObserver(
389236df
IJ
685 stderr_obs, [pred], stdout_obs
686 )
ec2c9312
IJ
687 global file_log_observer
688 file_log_observer = twisted.logger.FilteringLogObserver(
b83d422a
IJ
689 stdsomething_obs, [LogNotBoringTwisted()]
690 )
691 #log_observer = stdsomething_obs
8c3b6620 692 twisted.logger.globalLogBeginner.beginLoggingTo(
ec2c9312 693 [ file_log_observer, crash_on_critical ]
8c3b6620 694 )
ae7c7784 695
ae7c7784 696def common_run():
b68c0739
IJ
697 log_debug(DBG.INIT, 'entering reactor')
698 if not _crashing: reactor.run()
2eecd19c 699 print('ENDED', file=sys.stderr)
207f5042 700 sys.exit(16)