X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?p=hippotat.git;a=blobdiff_plain;f=hippotatd;h=40f4c69d32ebcb0d0ab659bd76e0da44828e9e49;hp=06f98ecd7e52bc6e1788ea6db9e14b32489bbd57;hb=343c8cf482d0a5a7fbd34be06d0d190e36c982e4;hpb=0256fc104ccd33103127dffb48b449aca8d46fae diff --git a/hippotatd b/hippotatd index 06f98ec..40f4c69 100755 --- a/hippotatd +++ b/hippotatd @@ -5,30 +5,34 @@ # # Copyright 2017 Ian Jackson # -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version, with the "CAF Login -# Exception" as published by Ian Jackson (version 2, or at your option -# any later version) as an Additional Permission. +# AGPLv3+ + CAFv2+ # -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public +# License as published by the Free Software Foundation, either +# version 3 of the License, or (at your option) any later version, +# with the "CAF Login Exception" as published by Ian Jackson +# (version 2, or at your option any later version) as an Additional +# Permission. # -# You should have received a copy of the GNU Affero General Public -# License and the CAF Login Exception along with this program, in the -# file AGPLv3+CAFv2. If not, email Ian Jackson -# . - +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public +# License and the CAF Login Exception along with this program, in +# the file AGPLv3+CAFv2. If not, email Ian Jackson +# . +#@ import sys; sys.path.append('@PYBUILD_INSTALL_DIR@') from hippotatlib import * import os import tempfile import atexit import shutil +import subprocess import twisted.internet from twisted.web.server import NOT_DONE_YET @@ -60,7 +64,7 @@ def route(packet, iface, saddr, daddr): elif daddr == c.vaddr or daddr not in c.vnetwork: lt('inbound') queue_inbound(ipif, packet) - elif daddr == c.relay: + elif daddr == c.vrelay: lt('discard relay') log_discard(packet, iface, saddr, daddr, 'relay') else: @@ -100,7 +104,8 @@ class Client(): def _req_cancel(self, request): self._log(DBG.HTTP_CTRL, 'cancel', idof=request) - request.finish() + try: request.finish() + except Exception: pass def _req_error(self, err, request): self._log(DBG.HTTP_CTRL, 'error %s' % err, idof=request) @@ -121,7 +126,7 @@ class Client(): nf = request.notifyFinish() nf.addErrback(self._req_error, request) nf.addCallback(self._req_fin, request, cl) - self._rq.append(request) + self._rq.append((request,nf)) self._check_outbound() def _req_write(self, req, d): @@ -131,9 +136,9 @@ class Client(): def _check_outbound(self): log_debug(DBG.HTTP_CTRL, 'CHKO') while True: - try: request = self._rq[0] + try: (request,nf) = self._rq[0] except IndexError: request = None - if request and request.finished: + if request and nf.called: self._log(DBG.HTTP_CTRL, 'CHKO req finished, discard', idof=request) self._rq.popleft() continue @@ -161,7 +166,7 @@ class Client(): # round again, looking for more to do while len(self._rq) > self.cc.target_requests_outstanding: - request = self._rq.popleft() + (request, nf) = self._rq.popleft() self._log(DBG.HTTP, 'CHKO above target, returning empty', idof=request) request.finish() @@ -231,52 +236,71 @@ class IphttpResource(NotStupidResource): log_debug(DBG.HTTP_CTRL, '...', idof=id(request)) return NOT_DONE_YET - def render_GET(self, request): - log_debug(DBG.HTTP, 'GET request') - return b''' - -hippotat -

-source available - -''' - -class SourceResource(twisted.web.static.File): + # instantiator should set + # self.hippotat_sources = (source_names[0], source_names[1]) def __init__(self): - td = tempfile.mkdtemp() - - def cleanup(): - try: shutil.rmtree(td) - except FileNotFoundError: pass - - cleanups.append(cleanup) + self.hippotat_sources = [None, None] + super().__init__() - self._ssp = SourceShipmentPreparer(td) - self._ssp.logger = self.log - self._ssp.generate() - - super().__init__(self._ssp.output_path) - - def log(self, m): - log_debug(DBG.OWNSOURCE, m) + def render_GET(self, request): + log_debug(DBG.HTTP, 'GET request') + s = 'hippotat\n' + (s0,s1) = self.hippotat_sources + if s0: + s += '

source\n' % s0 + if self.hippotat_sources[1]: + s += ('(and that of dependency packages)\n' % s1) + s += 'available' + else: + s += 'TESTING' + s += '' + return s.encode('utf-8') def start_http(): resource = IphttpResource() - resource.putChild(b'source',SourceResource()) site = twisted.web.server.Site(resource) + for sa in c.saddrs: ep = sa.make_endpoint() crash_on_defer(ep.listen(site)) log_debug(DBG.INIT, 'listening on %s' % sa) + td = tempfile.mkdtemp() + + def cleanup(): + try: shutil.rmtree(td) + except FileNotFoundError: pass + + cleanups.append(cleanup) + + ssp = SourceShipmentPreparer(td) + ssp.logger = partial(log_debug, DBG.OWNSOURCE) + if DBG.OWNSOURCE in debug_set: ssp.stream_debug = sys.stdout + ssp.download_packages = opts.ownsource >= 2 + if opts.ownsource >= 1: ssp.generate() + + for ix in (0,1): + bn = ssp.output_names[ix] + op = ssp.output_paths[ix] + if op is None: continue + resource.hippotat_sources[ix] = bn + subresource =twisted.web.static.File(op) + resource.putChild(bn.encode('utf-8'), subresource) + + reactor.callLater(0.1, (lambda: log.info('hippotatd started', dflag=False))) + #---------- config and setup ---------- -def process_cfg(putative_servers, putative_clients): +def process_cfg(_opts, putative_servers, putative_clients): + global opts + opts = _opts + global c c = ConfigResults() - c.server = cfg.get('SERVER','server') + try: c.server = cfg1get('SERVER','server') + except NoOptionError: c.server = 'SERVER' - cfg_process_common(c, c.server) + cfg_process_general(c, c.server) cfg_process_saddrs(c, c.server) cfg_process_vnetwork(c, c.server) cfg_process_vaddr(c, c.server) @@ -290,19 +314,28 @@ def process_cfg(putative_servers, putative_clients): Client(ci, cc) try: - c.vrelay = cfg.get(c.server, 'vrelay') + c.vrelay = cfg1get(c.server, 'vrelay') except NoOptionError: for search in c.vnetwork.hosts(): if search == c.vaddr: continue c.vrelay = search break + try: c.ifname = cfg1get(c.server, 'ifname_server', raw=True) + except NoOptionError: pass + cfg_process_ipif(c, - [c.server, 'DEFAULT'], + [c.server, 'COMMON'], (('local','vaddr'), ('peer', 'vrelay'), ('rnets','vnetwork'))) + if opts.printconfig is not None: + try: val = cfg1get(c.server, opts.printconfig) + except NoOptionError: pass + else: print(val) + sys.exit(0) + def catch_termination(): def run_cleanups(): for cleanup in cleanups: @@ -318,10 +351,128 @@ def catch_termination(): raise RuntimeError('did not die due to signal %s !' % name) for sig in (signal.SIGINT, signal.SIGTERM): - signal.signal(sig, partial(signal_handler, sig.name)) + try: signame = sig.name + except AttributeError: signame = "signal %d" % sig + signal.signal(sig, partial(signal_handler, signame)) + +def daemonise(): + global syslogfacility + if opts.daemon and opts.syslogfacility is None: + opts.syslogfacility = 'daemon' + + if opts.syslogfacility is not None: + facilnum = syslog.__dict__['LOG_' + opts.syslogfacility.upper()] + syslog.openlog('hippotatd', + facility=facilnum, + logoption=syslog.LOG_PID) + def emit(event): + if logevent_is_boringtwisted(event): return + m = twisted.logger.formatEvent(event) + #print(repr(event), m, file=org_stderr) + level = event.get('log_level') + if event.get('dflag',None) is not None: sl = syslog.LOG_DEBUG + elif level == LogLevel.critical : sl = syslog.LOG_CRIT + elif level == LogLevel.error : sl = syslog.LOG_ERR + elif level == LogLevel.warn : sl = syslog.LOG_WARNING + else : sl = syslog.LOG_INFO + syslog.syslog(sl,m) + failure = event.get('log_failure') + if failure is not None: + for l in failure.getTraceback().split('\n'): + syslog.syslog(sl,l) + glp = twisted.logger.globalLogPublisher + glp.addObserver(emit) + log_debug(DBG.INIT, 'starting to log to syslog') + + #log.crit('daemonic hippotatd crashed', dflag=False) + if opts.daemon: + daemonic_reactor = (twisted.internet.interfaces.IReactorDaemonize + .providedBy(reactor)) + if daemonic_reactor: reactor.beforeDaemonize() + if opts.pidfile is not None: + pidfile_h = open(opts.pidfile, 'w') + rfd, wfd = os.pipe() + childpid = os.fork() + if childpid: + # we are the parent + os.close(wfd) + st = os.read(rfd, 1) + try: + st = st[0] + except IndexError: + st = 127 + log.critical('daemonic hippotatd crashed', dflag=False) + os._exit(st) + os.close(rfd) + os.setsid() + grandchildpid = os.fork() + if grandchildpid: + # we are the intermediate child + if opts.pidfile is not None: + print(grandchildpid, file=pidfile_h) + pidfile_h.close() + os._exit(0) + + if opts.pidfile is not None: + pidfile_h.close() + + logger = subprocess.Popen(['logger','-d', + '-t','hippotat[%d](stderr)' % os.getpid(), + '-p',opts.syslogfacility + '.err'], + stdin=subprocess.PIPE, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + restore_signals=True) + + nullfd = os.open('/dev/null', os.O_RDWR) + os.dup2(nullfd, 0) + os.dup2(nullfd, 1) + os.dup2(logger.stdin.fileno(), 2) + os.close(nullfd) + if daemonic_reactor: reactor.afterDaemonize() + log_debug(DBG.INIT, 'daemonised') + os.write(wfd, b'\0') + os.close(wfd) + + if opts.syslogfacility is not None: + glp.removeObserver(hippotatlib.file_log_observer) + +optparser.add_option('--ownsource', default=2, + action='store_const', dest='ownsource', const=2, + help='source download fully enabled (default)') + +optparser.add_option('--ownsource-local', + action='store_const', dest='ownsource', const=1, + help='source download is local source code only') + +optparser.add_option('--no-ownsource', + action='store_const', dest='ownsource', const=0, + help='source download disabled (for testing only)') + +optparser.add_option('--daemon', + action='store_true', dest='daemon', default=False, + help='daemonize (and log to syslog)') + +optparser.add_option('--pidfile', + nargs=1, type='string', + action='store', dest='pidfile', default=None, + help='write pid to this file') + +optparser.add_option('--syslog-facility', + nargs=1, type='string',action='store', + metavar='FACILITY', dest='syslogfacility', + default=None, + help='log to syslog, with specified facility') + +optparser.add_option('--print-config', + nargs=1, type='string',action='store', + metavar='OPTION', dest='printconfig', + default=None, + help='print one config option value and exit') common_startup(process_cfg) catch_termination() -ipif = start_ipif(c.ipif_command, (lambda p,s,d: route(p,"[ipif]",s,d))) start_http() +daemonise() +ipif = start_ipif(c.ipif_command, (lambda p,s,d: route(p,"[ipif]",s,d))) common_run()