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