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