4 ### Connect to remote peers, and keep track of them
6 ### (c) 2007 Straylight/Edgeware
9 ###----- Licensing notice ---------------------------------------------------
11 ### This file is part of Trivial IP Encryption (TrIPE).
13 ### TrIPE is free software: you can redistribute it and/or modify it under
14 ### the terms of the GNU General Public License as published by the Free
15 ### Software Foundation; either version 3 of the License, or (at your
16 ### option) any later version.
18 ### TrIPE is distributed in the hope that it will be useful, but WITHOUT
19 ### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
20 ### FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
23 ### You should have received a copy of the GNU General Public License
24 ### along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
28 ###--------------------------------------------------------------------------
29 ### External dependencies.
31 from optparse import OptionParser
42 import subprocess as PROC
46 ###--------------------------------------------------------------------------
47 ### Running auxiliary commands.
49 class SelLineQueue (M.SelLineBuffer):
50 """Glues the select-line-buffer into the coroutine queue system."""
52 def __new__(cls, file, queue, tag, kind):
53 """See __init__ for documentation."""
54 return M.SelLineBuffer.__new__(cls, file.fileno())
56 def __init__(me, file, queue, tag, kind):
58 Initialize a new line-reading adaptor.
60 The adaptor reads lines from FILE. Each line is inserted as a message of
61 the stated KIND, bearing the TAG, into the QUEUE. End-of-file is
72 me._q.put((me._tag, me._kind, line))
77 me._q.put((me._tag, me._kind, None))
79 class ErrorWatch (T.Coroutine):
81 An object which watches stderr streams for errors and converts them into
84 WARN connect INFO stderr LINE
86 The INFO is a list of tokens associated with the file when it was
89 Usually there is a single ErrorWatch object, called errorwatch.
93 """Initialization: there are no arguments."""
94 T.Coroutine.__init__(me)
99 def watch(me, file, info):
101 Adds FILE to the collection of files to watch.
103 INFO will be written in the warning messages from this FILE. Returns a
104 sequence number which can be used to unregister the file again.
108 me._map[seq] = info, SelLineQueue(file, me._q, seq, 'stderr')
111 def unwatch(me, seq):
112 """Stop watching the file with sequence number SEQ."""
118 Coroutine function: read items from the queue and report them.
120 Unregisters files automatically when they reach EOF.
123 seq, _, line = me._q.get()
127 S.warn(*['connect'] + me._map[seq][0] + ['stderr', line])
131 Coroutine function: wake up every minute and notice changes to the
132 database. When a change happens, tell the Pinger (q.v.) to rescan its
135 cr = T.Coroutine.getcurrent()
137 fw = M.FWatch(opts.cdb)
139 timer = M.SelTimer(time() + 60, lambda: cr.switch())
143 S.notify('connect', 'peerdb-update')
145 class ChildWatch (M.SelSignal):
147 An object which watches for specified processes exiting and reports
148 terminations by writing items of the form (TAG, 'exit', RESULT) to a queue.
150 There is usually only one ChildWatch object, called childwatch.
154 """Initialize the child-watcher."""
155 return M.SelSignal.__new__(cls, SIG.SIGCHLD)
158 """Initialize the child-watcher."""
162 def watch(me, pid, queue, tag):
164 Register PID as a child to watch. If it exits, write (TAG, 'exit', CODE)
165 to the QUEUE, where CODE is one of
167 * None (successful termination)
168 * ['exit-nonzero', CODE] (CODE is a string!)
169 * ['exit-signal', 'S' + CODE] (CODE is the signal number as a string)
170 * ['exit-unknown', STATUS] (STATUS is the entire exit status, in hex)
172 me._pid[pid] = queue, tag
175 def unwatch(me, pid):
176 """Unregister PID as a child to watch."""
183 Called when child processes exit: collect exit statuses and report
188 pid, status = OS.waitpid(-1, OS.WNOHANG)
190 if exc.errno == E.ECHILD:
194 if pid not in me._pid:
196 queue, tag = me._pid[pid]
197 if OS.WIFEXITED(status):
198 exit = OS.WEXITSTATUS(status)
202 code = ['exit-nonzero', str(exit)]
203 elif OS.WIFSIGNALED(status):
204 code = ['exit-signal', 'S' + str(OS.WTERMSIG(status))]
206 code = ['exit-unknown', hex(status)]
207 queue.put((tag, 'exit', code))
209 class Command (object):
211 Represents a running command.
213 This class is the main interface to the machery provided by the ChildWatch
214 and ErrorWatch objects. See also potwatch.
217 def __init__(me, info, queue, tag, args, env):
219 Start a new child process.
221 The ARGS are a list of arguments to be given to the child process. The
222 ENV is either None or a dictionary of environment variable assignments to
223 override the extant environment. INFO is a list of tokens to be included
224 in warnings about the child's stderr output. If the child writes a line
225 to standard output, put (TAG, 'stdout', LINE) to the QUEUE. When the
226 child exits, write (TAG, 'exit', CODE) to the QUEUE.
231 myenv = OS.environ.copy()
232 if env: myenv.update(env)
233 me._proc = PROC.Popen(args = args, env = myenv, bufsize = 1,
234 stdout = PROC.PIPE, stderr = PROC.PIPE)
235 me._lq = SelLineQueue(me._proc.stdout, queue, tag, 'stdout')
236 errorwatch.watch(me._proc.stderr, info)
237 childwatch.watch(me._proc.pid, queue, tag)
241 If I've been forgotten then stop watching for termination.
243 childwatch.unwatch(me._proc.pid)
245 def potwatch(what, name, q):
247 Watch the queue Q for activity as reported by a Command object.
249 Information from the process's stdout is reported as
251 NOTE WHAT NAME stdout LINE
253 abnormal termination is reported as
257 where CODE is what the ChildWatch wrote.
260 while not deadp or not eofp:
261 _, kind, more = q.get()
266 S.notify('connect', what, name, 'stdout', more)
268 if more: S.warn('connect', what, name, *more)
271 ###--------------------------------------------------------------------------
272 ### Peer database utilities.
274 _magic = ['_magic'] # An object distinct from all others
277 """Representation of a peer in the database."""
279 def __init__(me, peer, cdb = None):
281 Create a new peer, named PEER.
283 Information about the peer is read from the database CDB, or the default
284 one given on the command-line.
287 record = (cdb or CDB.init(opts.cdb))['P' + peer]
288 me.__dict__.update(M.URLDecode(record, semip = True))
290 def get(me, key, default = _magic, filter = None):
292 Get the information stashed under KEY from the peer's database record.
294 If DEFAULT is given, then use it if the database doesn't contain the
295 necessary information. If no DEFAULT is given, then report an error. If
296 a FILTER function is given then apply it to the information from the
297 database before returning it.
300 attr = me.__dict__[key]
302 if default is _magic:
303 raise T.TripeJobError('malformed-peer', me.name, 'missing-key', key)
306 if filter is not None: attr = filter(attr)
311 Return whether the peer's database record has the KEY.
313 return key in me.__dict__
317 Iterate over the available keys in the peer's database record.
319 return me.__dict__.iterkeys()
322 """Parse VALUE as a boolean."""
323 return value in ['t', 'true', 'y', 'yes', 'on']
325 ###--------------------------------------------------------------------------
326 ### Waking up and watching peers.
328 def run_connect(peer, cmd):
330 Start the job of connecting to the passive PEER.
332 The CMD string is a shell command which will connect to the peer (via some
333 back-channel, say ssh and userv), issue a command
335 SVCSUBMIT connect passive [OPTIONS] USER
337 and write the resulting challenge to standard error.
340 cmd = Command(['connect', peer.name], q, 'connect',
341 ['/bin/sh', '-c', cmd], None)
342 _, kind, more = q.peek()
345 S.warn('connect', 'connect', peer.name, 'unexpected-eof')
348 S.greet(peer.name, chal)
350 potwatch('connect', peer.name, q)
352 def run_disconnect(peer, cmd):
354 Start the job of disconnecting from a passive PEER.
356 The CMD string is a shell command which will disconnect from the peer.
359 cmd = Command(['disconnect', peer.name], q, 'disconnect',
360 ['/bin/sh', '-c', cmd], None)
361 potwatch('disconnect', peer.name, q)
364 class PingPeer (object):
366 Object representing a peer which we are pinging to ensure that it is still
369 PingPeer objects are held by the Pinger (q.v.). The Pinger maintains an
370 event queue -- which saves us from having an enormous swarm of coroutines
371 -- but most of the actual work is done here.
373 In order to avoid confusion between different PingPeer instances for the
374 same actual peer, each PingPeer has a sequence number (its `seq'
375 attribute). Events for the PingPeer are identified by a (PEER, SEQ) pair.
376 (Using the PingPeer instance itself will prevent garbage collection of
377 otherwise defunct instances.)
380 def __init__(me, pinger, queue, peer, pingnow):
382 Create a new PingPeer.
384 The PINGER is the Pinger object we should send the results to. This is
385 used when we remove ourselves, if the peer has been explicitly removed.
387 The QUEUE is the event queue on which timer and ping-command events
390 The PEER is a `Peer' object describing the peer.
392 If PINGNOW is true, then immediately start pinging the peer. Otherwise
393 wait until the usual retry interval.
409 me._min = me._max = '-'
415 me._timer = M.SelTimer(now + me._every, me._time)
416 me._last_reconn = now
418 def update(me, peer):
420 Refreshes the timer parameters for this peer. We don't, however,
421 immediately reschedule anything: that will happen next time anything
424 if peer is None: peer = Peer(me._peer)
425 assert peer.name == me._peer
426 me._every = peer.get('every', filter = T.timespec, default = 120)
427 me._timeout = peer.get('timeout', filter = T.timespec, default = 10)
428 me._retries = peer.get('retries', filter = int, default = 5)
429 me._connectp = peer.has('connect')
430 me._knockp = peer.has('knock')
435 Send a ping to the peer; the result is sent to the Pinger's event queue.
437 S.rawcommand(T.TripeAsynchronousCommand(
438 me._q, (me._peer, me.seq),
440 '-background', S.bgtag(),
441 '-timeout', str(me._timeout),
445 def _reconnect(me, now):
448 peer = Peer(me._peer)
449 if me._connectp or me._knockp:
450 S.warn('connect', 'reconnecting', me._peer)
451 S.forcekx(me._peer, quiet = not me._knockp)
452 if me._connectp: T.spawn(run_connect, peer, peer.get('connect'))
453 me._timer = M.SelTimer(now + me._every, me._time)
455 me._last_reconn = now
458 except T.TripeError, e:
459 if e.args[0] == 'unknown-peer': me._pinger.kill(me._peer)
463 Attempt reconnection to the peer.
465 Applies rate-limiting so that we don't hammer a remote peer just because
466 we notice several problems in a short time interval.
469 if now >= me._last_reconn + 5: me._reconnect(now)
471 def event(me, code, stuff):
473 Respond to an event which happened to this peer.
475 Timer events indicate that we should start a new ping. (The server has
476 its own timeout which detects lost packets.)
478 We trap unknown-peer responses and detach from the Pinger.
480 If the ping fails and we run out of retries, we attempt to restart the
488 S.notify('connect', 'ping-failed', me._peer, *stuff)
490 elif stuff[0] == 'unknown-peer': me._pinger.kill(me._peer)
491 elif stuff[0] == 'ping-send-failed': me._reconnect(now)
494 if outcome == 'ping-ok' and me._sabotage:
495 outcome = 'ping-timeout'
496 if outcome == 'ping-ok':
497 if me._failures > 0: S.warn('connect', 'ping-ok', me._peer)
499 me._last = '%.1fms' % t
503 if me._min == '-' or t < me._min: me._min = t
504 if me._max == '-' or t > me._max: me._max = t
505 me._timer = M.SelTimer(now + me._every, me._time)
506 elif outcome == 'ping-timeout':
509 S.warn('connect', 'ping-timeout', me._peer,
510 'attempt', str(me._failures), 'of', str(me._retries))
511 if me._failures < me._retries:
516 me._last = 'reconnect'
517 elif outcome == 'ping-peer-died':
518 me._pinger.kill(me._peer)
521 """Sabotage the peer, for testing purposes."""
523 if me._timer: me._timer.kill()
528 mean = sd = min = max = '-'
530 meanval = me._sigma_t/me._nping
531 mean = '%.1fms' % meanval
532 sd = '%.1fms' % sqrt(me._sigma_t2/me._nping - meanval*meanval)
533 min = '%.1fms' % me._min
534 max = '%.1fms' % me._max
535 n = me._nping + me._nlost
536 if not n: pclost = '-'
537 else: pclost = '%d' % ((100*me._nlost + n//2)//n)
538 return { 'last-ping': me._last,
541 'n-ping': '%d' % me._nping,
542 'n-lost': '%d' % me._nlost,
543 'percent-lost': pclost,
546 'state': me._timer and 'idle' or 'check',
547 'failures': str(me._failures) }
552 Handle timer callbacks by posting a timeout event on the queue.
555 me._q.put(((me._peer, me.seq), 'TIMER', None))
558 return 'PingPeer(%s, %d, f = %d)' % (me._peer, me.seq, me._failures)
562 class Pinger (T.Coroutine):
564 The Pinger keeps track of the peers which we expect to be connected and
565 takes action if they seem to stop responding.
567 There is usually only one Pinger, called pinger.
569 The Pinger maintains a collection of PingPeer objects, and an event queue.
570 The PingPeers direct the results of their pings, and timer events, to the
571 event queue. The Pinger's coroutine picks items off the queue and
572 dispatches them back to the PingPeers as appropriate.
576 """Initialize the Pinger."""
577 T.Coroutine.__init__(me)
583 Coroutine function: reads the pinger queue and sends events to the
584 PingPeer objects they correspond to.
587 (peer, seq), code, stuff = me._q.get()
588 if peer in me._peers and seq == me._peers[peer].seq:
589 try: me._peers[peer].event(code, stuff)
591 SYS.excepthook(*SYS.exc_info())
593 def add(me, peer, pingnow):
595 Add PEER to the collection of peers under the Pinger's watchful eye.
596 The arguments are as for PingPeer: see above.
598 me._peers[peer.name] = PingPeer(me, me._q, peer, pingnow)
601 def kill(me, peername):
602 """Remove PEER from the peers being watched by the Pinger."""
603 try: del me._peers[peername]
604 except KeyError: pass
607 def rescan(me, startup):
609 General resynchronization method.
611 We scan the list of peers (with connect scripts) known at the server.
612 Any which are known to the Pinger but aren't known to the server are
613 removed from our list; newly arrived peers are added. (Note that a peer
614 can change state here either due to the server sneakily changing its list
615 without issuing notifications or, more likely, the database changing its
616 idea of whether a peer is interesting.) Finally, PingPeers which are
617 still present are prodded to update their timing parameters.
619 This method is called once at startup to pick up the peers already
620 installed, and again by the dbwatcher coroutine when it detects a change
623 if T._debug: print '# rescan peers'
626 for name in S.list():
627 try: peer = Peer(name)
628 except KeyError: continue
629 if peer.get('watch', filter = boolean, default = False):
630 if T._debug: print '# interesting peer %s' % peer
631 correct[peer.name] = start[peer.name] = peer
633 if T._debug: print '# peer %s ready for adoption' % peer
634 start[peer.name] = peer
635 for name, obj in me._peers.items():
639 if T._debug: print '# peer %s vanished' % name
643 for name, peer in start.iteritems():
644 if name in me._peers: continue
646 if T._debug: print '# setting up peer %s' % name
647 ifname = S.ifname(name)
649 T.defer(adoptpeer, peer, ifname, *addr)
651 if T._debug: print '# adopting new peer %s' % name
657 Returns the list of peers being watched by the Pinger.
659 return me._peers.keys()
662 """Return the PingPeer with the given name."""
663 return me._peers[name]
665 ###--------------------------------------------------------------------------
668 def encode_envvars(env, prefix, vars):
670 Encode the variables in VARS suitably for including in a program
671 environment. Lowercase letters in variable names are forced to uppercase;
672 runs of non-alphanumeric characters are replaced by single underscores; and
673 the PREFIX is prepended. The resulting variables are written to ENV.
675 for k, v in vars.iteritems():
676 env[prefix + r_bad.sub('_', k.upper())] = v
678 r_bad = RX.compile(r'[\W_]+')
681 Translate the database information for a PEER into a dictionary of
682 environment variables with plausible upper-case names and a P_ prefix.
683 Also collect the crypto information into A_ variables.
686 encode_envvars(env, 'P_', dict([(k, peer.get(k)) for k in peer.list()]))
687 encode_envvars(env, 'A_', S.algs(peer.name))
690 def run_ifupdown(what, peer, *args):
692 Run the interface up/down script for a peer.
694 WHAT is 'ifup' or 'ifdown'. PEER names the peer in question. ARGS is a
695 list of arguments to pass to the script, in addition to the peer name.
697 The command is run and watched in the background by potwatch.
700 c = Command([what, peer.name], q, what,
701 M.split(peer.get(what), quotep = True)[0] +
702 [peer.name] + list(args),
704 potwatch(what, peer.name, q)
706 def adoptpeer(peer, ifname, *addr):
708 Add a new peer to our collection.
710 PEER is the `Peer' object; IFNAME is the interface name for its tunnel; and
711 ADDR is the list of tokens representing its address.
713 We try to bring up the interface and provoke a connection to the peer if
717 T.Coroutine(run_ifupdown, name = 'ifup %s' % peer.name) \
718 .switch('ifup', peer, ifname, *addr)
719 cmd = peer.get('connect', default = None)
721 T.Coroutine(run_connect, name = 'connect %s' % peer.name) \
723 if peer.get('watch', filter = boolean, default = False):
724 pinger.add(peer, False)
726 def disownpeer(peer):
727 """Drop the PEER from the Pinger and put its interface to bed."""
728 try: pinger.kill(peer)
729 except KeyError: pass
730 cmd = peer.get('disconnect', default = None)
732 T.Coroutine(run_disconnect, name = 'disconnect %s' % peer.name) \
734 if peer.has('ifdown'):
735 T.Coroutine(run_ifupdown, name = 'ifdown %s' % peer.name) \
736 .switch('ifdown', peer)
738 def addpeer(peer, addr, ephemp):
740 Process a connect request from a new peer PEER on address ADDR.
742 Any existing peer with this name is disconnected from the server. EPHEMP
743 is the default ephemeral-ness state for the new peer.
745 if peer.name in S.list():
749 tunnel = peer.get('tunnel', default = None),
750 keepalive = peer.get('keepalive', default = None),
751 key = peer.get('key', default = None),
752 priv = peer.get('priv', default = None),
753 mobile = peer.get('mobile', filter = boolean, default = False),
754 knock = peer.get('knock', default = None),
755 cork = peer.get('cork', filter = boolean, default = False),
756 ephemeral = peer.get('ephemeral', filter = boolean,
759 except T.TripeError, exc:
760 raise T.TripeJobError(*exc.args)
762 ## Dictionary mapping challenges to waiting passive-connection coroutines.
765 def notify(_, code, *rest):
767 Watch for notifications.
769 We trap ADD and KILL notifications, and send them straight to adoptpeer and
770 disownpeer respectively; and dispatch GREET notifications to the
771 corresponding waiting coroutine.
774 try: p = Peer(rest[0])
775 except KeyError: pass
776 else: adoptpeer(p, *rest[1:])
778 try: p = Peer(rest[0])
779 except KeyError: pass
780 else: disownpeer(p, *rest[1:])
782 try: p = pinger.find(rest[0])
783 except KeyError: pass
785 elif code == 'GREET':
787 try: cr = chalmap[chal]
788 except KeyError: pass
789 else: cr.switch(rest[1:])
790 elif code == 'KNOCK':
791 try: p = Peer(rest[0])
793 S.warn(['connect', 'knock-unknown-peer', rest[0]])
795 if p.get('peer') != 'PASSIVE':
796 S.warn(['connect', 'knock-active-peer', p.name])
798 dot = p.name.find('.')
799 if dot >= 0: kname = p.name[dot + 1:]
801 ktag = p.get('key', p.name)
803 S.warn(['connect', 'knock-tag-mismatch',
804 'peer', pname, 'public-key-tag', ktag])
806 T.spawn(addpeer, p, rest[1:], True)
808 ###--------------------------------------------------------------------------
809 ### Command implementation.
813 kick NAME: Force a new connection attempt for the NAMEd peer.
815 try: pp = pinger.find(name)
816 except KeyError: raise T.TripeJobError('peer-not-adopted', name)
817 try: peer = Peer(name)
818 except KeyError: raise T.TripeJobError('unknown-peer', name)
819 conn = peer.get('connect', None)
820 if conn: T.spawn(run_connect, peer, peer.get('connect'))
821 else: T.spawn(lambda p: S.forcekx(p.name), peer)
825 adopted: Report a list of adopted peers.
827 for name in pinger.adopted():
830 def cmd_active(name):
832 active NAME: Handle an active connection request for the peer called NAME.
834 The appropriate address is read from the database automatically.
836 try: peer = Peer(name)
837 except KeyError: raise T.TripeJobError('unknown-peer', name)
838 addr = peer.get('peer')
839 if addr == 'PASSIVE':
840 raise T.TripeJobError('passive-peer', name)
841 addpeer(peer, M.split(addr, quotep = True)[0], True)
843 def cmd_listactive():
845 list: Report a list of the available active peers.
847 cdb = CDB.init(opts.cdb)
848 for key in cdb.keys():
849 if key.startswith('P') and Peer(key[1:]).get('peer', '') != 'PASSIVE':
854 info NAME: Report the database entries for the named peer.
856 try: peer = Peer(name)
857 except KeyError: raise T.TripeJobError('unknown-peer', name)
859 try: pp = pinger.find(name)
860 except KeyError: pass
861 else: d.update(pp.info())
862 items = list(peer.list()) + d.keys()
866 except KeyError: v = peer.get(i)
867 T.svcinfo('%s=%s' % (i, v.replace('\n', ' ')))
869 def cmd_userpeer(user):
871 userpeer USER: Report the peer name for the named user.
873 try: name = CDB.init(opts.cdb)['U' + user]
874 except KeyError: raise T.TripeJobError('unknown-user', user)
877 def cmd_passive(*args):
879 passive [OPTIONS] USER: Await the arrival of the named USER.
881 Report a challenge; when (and if!) the server receives a greeting quoting
882 this challenge, add the corresponding peer to the server.
886 op = T.OptParse(args, ['-timeout'])
888 if opt == '-timeout':
889 timeout = T.timespec(op.arg())
890 user, = op.rest(1, 1)
891 try: name = CDB.init(opts.cdb)['U' + user]
892 except KeyError: raise T.TripeJobError('unknown-user', user)
893 try: peer = Peer(name)
894 except KeyError: raise T.TripeJobError('unknown-peer', name)
896 cr = T.Coroutine.getcurrent()
897 timer = M.SelTimer(now + timeout, lambda: cr.switch(None))
901 addr = cr.parent.switch()
903 raise T.TripeJobError('connect-timeout')
904 addpeer(peer, addr, True)
908 def cmd_sabotage(name):
910 sabotage NAME: Sabotage the NAMEd peer so that we think it can't be pinged.
912 try: pp = pinger.find(name)
913 except KeyError: raise T.TripeJobError('unknown-peer', name)
916 ###--------------------------------------------------------------------------
923 Register the notification watcher, rescan the peers, and add automatic
926 S.handler['NOTE'] = notify
929 pinger.rescan(opts.startup)
932 cdb = CDB.init(opts.cdb)
937 for name in M.split(autos)[0]:
939 peer = Peer(name, cdb)
940 addpeer(peer, M.split(peer.get('peer'), quotep = True)[0], False)
941 except T.TripeJobError, err:
942 S.warn('connect', 'auto-add-failed', name, *err.args)
946 Initialization to be done before service startup.
948 global errorwatch, childwatch, pinger
949 errorwatch = ErrorWatch()
950 childwatch = ChildWatch()
952 T.Coroutine(dbwatch, name = 'dbwatch').switch()
958 Parse the command-line options.
960 Automatically changes directory to the requested configdir, and turns on
961 debugging. Returns the options object.
963 op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
964 version = '%%prog %s' % VERSION)
966 op.add_option('-a', '--admin-socket',
967 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
968 help = 'Select socket to connect to [default %default]')
969 op.add_option('-d', '--directory',
970 metavar = 'DIR', dest = 'dir', default = T.configdir,
971 help = 'Select current diretory [default %default]')
972 op.add_option('-p', '--peerdb',
973 metavar = 'FILE', dest = 'cdb', default = T.peerdb,
974 help = 'Select peers database [default %default]')
975 op.add_option('--daemon', dest = 'daemon',
976 default = False, action = 'store_true',
977 help = 'Become a daemon after successful initialization')
978 op.add_option('--debug', dest = 'debug',
979 default = False, action = 'store_true',
980 help = 'Emit debugging trace information')
981 op.add_option('--startup', dest = 'startup',
982 default = False, action = 'store_true',
983 help = 'Being called as part of the server startup')
985 opts, args = op.parse_args()
986 if args: op.error('no arguments permitted')
988 T._debug = opts.debug
991 ## Service table, for running manually.
992 service_info = [('connect', VERSION, {
993 'adopted': (0, 0, '', cmd_adopted),
994 'kick': (1, 1, 'PEER', cmd_kick),
995 'passive': (1, None, '[OPTIONS] USER', cmd_passive),
996 'active': (1, 1, 'PEER', cmd_active),
997 'info': (1, 1, 'PEER', cmd_info),
998 'list-active': (0, 0, '', cmd_listactive),
999 'userpeer': (1, 1, 'USER', cmd_userpeer),
1000 'sabotage': (1, 1, 'PEER', cmd_sabotage)
1003 if __name__ == '__main__':
1004 opts = parse_options()
1005 OS.environ['TRIPESOCK'] = opts.tripesock
1006 T.runservices(opts.tripesock, service_info,
1007 init = init, setup = setup,
1008 daemon = opts.daemon)
1010 ###----- That's all, folks --------------------------------------------------