| 1 | |
| 2 | import os, sys, time |
| 3 | from twisted.internet import task, defer, reactor |
| 4 | from twisted.python.runtime import platformType |
| 5 | from twisted.python.failure import Failure |
| 6 | from twisted.protocols.basic import LineOnlyReceiver |
| 7 | |
| 8 | class ClientTimeoutError(Exception): |
| 9 | pass |
| 10 | class ClientFailedToConnectError(Exception): |
| 11 | pass |
| 12 | |
| 13 | class FakeTransport: |
| 14 | disconnecting = False |
| 15 | |
| 16 | class LogWatcher(LineOnlyReceiver): |
| 17 | POLL_INTERVAL = 0.1 |
| 18 | TIMEOUT_DELAY = 10.0 |
| 19 | delimiter = os.linesep |
| 20 | |
| 21 | def __init__(self, logfile, processtype): |
| 22 | self.logfile = logfile |
| 23 | self.in_startup = False |
| 24 | self.transport = FakeTransport() |
| 25 | self.f = None |
| 26 | self.processtype = processtype |
| 27 | |
| 28 | def start(self): |
| 29 | # return a Deferred that fires when the startup process has finished. |
| 30 | # It errbacks with TimeoutError if the finish line has not been seen |
| 31 | # within 10 seconds, and with ClientFailedToConnectError if the error |
| 32 | # line was seen. If the logfile could not be opened, it errbacks with |
| 33 | # an IOError. |
| 34 | self.running = True |
| 35 | d = defer.maybeDeferred(self._start) |
| 36 | return d |
| 37 | |
| 38 | def _start(self): |
| 39 | self.d = defer.Deferred() |
| 40 | try: |
| 41 | self.f = open(self.logfile, "rb") |
| 42 | self.f.seek(0, 2) # start watching from the end |
| 43 | except IOError: |
| 44 | pass |
| 45 | reactor.callLater(self.TIMEOUT_DELAY, self.timeout) |
| 46 | self.poller = task.LoopingCall(self.poll) |
| 47 | self.poller.start(self.POLL_INTERVAL) |
| 48 | return self.d |
| 49 | |
| 50 | def timeout(self): |
| 51 | self.d.errback(ClientTimeoutError()) |
| 52 | |
| 53 | def finished(self, results): |
| 54 | self.running = False |
| 55 | self.in_startup = False |
| 56 | self.d.callback(results) |
| 57 | |
| 58 | def lineReceived(self, line): |
| 59 | if not self.running: |
| 60 | return |
| 61 | if "Node constructed. tahoe version:" in line: |
| 62 | self.in_startup = True |
| 63 | if self.processtype == "client": |
| 64 | if "Initial Introducer connection succeeded:" in line: |
| 65 | print "CHILD:", line |
| 66 | self.finished(self.processtype) |
| 67 | return |
| 68 | if "Initial Introducer connection failed:" in line: |
| 69 | print "CHILD:", line |
| 70 | self.finished(Failure(ClientFailedToConnectError())) |
| 71 | return |
| 72 | else: |
| 73 | if "The node is now running." in line: |
| 74 | print "CHILD:", line |
| 75 | self.finished(self.processtype) |
| 76 | return |
| 77 | |
| 78 | if self.in_startup: |
| 79 | print "CHILD:", line |
| 80 | |
| 81 | def poll(self): |
| 82 | if not self.f: |
| 83 | try: |
| 84 | self.f = open(self.logfile, "rb") |
| 85 | except IOError: |
| 86 | return |
| 87 | while True: |
| 88 | data = self.f.read(1000) |
| 89 | if not data: |
| 90 | return |
| 91 | self.dataReceived(data) |
| 92 | |
| 93 | class Follower: |
| 94 | def follow(self, logfile, processtype): |
| 95 | self.rc = 0 |
| 96 | print "Following twistd.log until startup finished.." |
| 97 | lw = LogWatcher(logfile, processtype) |
| 98 | d = lw.start() |
| 99 | d.addCallbacks(self._success, self._failure) |
| 100 | reactor.run() |
| 101 | return self.rc |
| 102 | |
| 103 | def _success(self, processtype): |
| 104 | print "The %s appears to have (re)started correctly." % processtype |
| 105 | self.rc = 0 |
| 106 | reactor.stop() |
| 107 | |
| 108 | def _failure(self, why): |
| 109 | if why.check(ClientTimeoutError): |
| 110 | print """ |
| 111 | The node took more than 10 seconds to start, so we were unable to confirm |
| 112 | that it started correctly. Please 'tail logs/twistd.log' and look for a line |
| 113 | that says 'The node is now running.' to verify correct startup. |
| 114 | """ |
| 115 | else: |
| 116 | print """ |
| 117 | Unable to confirm that the node started correctly. You may need to stop it, |
| 118 | modify the configuration, and restart. |
| 119 | """ |
| 120 | print why |
| 121 | self.rc = 1 |
| 122 | reactor.stop() |
| 123 | |
| 124 | def get_node_type(basedir): |
| 125 | if os.path.exists(os.path.join(basedir, "client.tac")): |
| 126 | tac = "client.tac" |
| 127 | processtype = "client" |
| 128 | elif os.path.exists(os.path.join(basedir, "tahoe-client.tac")): |
| 129 | tac = "tahoe-client.tac" |
| 130 | processtype = "client" |
| 131 | elif os.path.exists(os.path.join(basedir, "introducer.tac")): |
| 132 | tac = "introducer.tac" |
| 133 | processtype = "introducer" |
| 134 | elif os.path.exists(os.path.join(basedir, "tahoe-introducer.tac")): |
| 135 | tac = "tahoe-introducer.tac" |
| 136 | processtype = "introducer" |
| 137 | else: |
| 138 | tac = None |
| 139 | processtype = None |
| 140 | return tac, processtype |
| 141 | |
| 142 | def launch(basedir, config): |
| 143 | # we are in a child process, so nothing we do will affect the parent |
| 144 | |
| 145 | # see if we can launch the application without actually having to |
| 146 | # spawn twistd, since spawning processes correctly is a real hassle |
| 147 | # on windows. |
| 148 | err = sys.stderr |
| 149 | |
| 150 | tac, processtype = get_node_type(basedir) |
| 151 | if processtype == None: |
| 152 | print >>err, "%s does not look like a node directory" % basedir |
| 153 | if not os.path.isdir(basedir): |
| 154 | print >>err, " in fact, it doesn't look like a directory at all!" |
| 155 | return 1 |
| 156 | |
| 157 | os.chdir(basedir) |
| 158 | |
| 159 | argv = ["twistd", |
| 160 | "--python", tac, |
| 161 | "--logfile", os.path.join("logs", "twistd.log"), |
| 162 | ] |
| 163 | if platformType == "win32": |
| 164 | argv.append("--reactor=win32") |
| 165 | sys.argv = argv |
| 166 | #print "in child, argv is", argv |
| 167 | |
| 168 | if True: |
| 169 | # this approach requires twisted-2.5.0 |
| 170 | from twisted.scripts import twistd |
| 171 | run = twistd.run |
| 172 | else: |
| 173 | # this is copied from bin/twistd. twisted-2.0.0 uses _twistw. |
| 174 | if platformType == "win32": |
| 175 | from twisted.scripts import _twistw |
| 176 | run = _twistw.run |
| 177 | else: |
| 178 | from twisted.scripts import twistd |
| 179 | run = twistd.run |
| 180 | #print "in child, about to call run()" |
| 181 | run() # this never returns, right? |
| 182 | sys.exit(0) # just in case it does |
| 183 | |
| 184 | |
| 185 | def start(basedir, config): |
| 186 | if config['quiet']: |
| 187 | return launch(basedir, config) |
| 188 | |
| 189 | # we probably can't do this os.fork under windows |
| 190 | if platformType == "win32": |
| 191 | return launch(basedir, config) |
| 192 | |
| 193 | # fork a child to launch the daemon, while the parent process tails the |
| 194 | # logfile |
| 195 | if os.fork(): |
| 196 | # this is the parent |
| 197 | logfile = os.path.join(basedir, "logs", "twistd.log") |
| 198 | tac, processtype = get_node_type(basedir) |
| 199 | rc = Follower().follow(logfile, processtype) |
| 200 | sys.exit(rc) |
| 201 | # this is the child: give the logfile-watching parent a chance to start |
| 202 | # watching it before we start the daemon. We want to make extra sure that |
| 203 | # the child never returns. |
| 204 | try: |
| 205 | time.sleep(0.2) |
| 206 | launch(basedir, config) |
| 207 | except: |
| 208 | sys.exit(-1) |
| 209 | |
| 210 | |