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