Newer
Older

Silas S. Brown
committed
if i==-1: return html # TODO: what of HTML documents that lack <body> (and frameset), do we add one somewhere? (after any /head ??)
i += 5 # after the "<body"

Silas S. Brown
committed
j = html.find(B('>'), i)

Silas S. Brown
committed
if j==-1: return html # ?!?
attrs = html[i:j]

Silas S. Brown
committed
attrsToAdd = B(attrsToAdd)
for a in re.findall(B(r'[A-Za-z_0-9]+\='),attrsToAdd): attrs = attrs.replace(a,B("old")+a) # disable corresponding existing attributes (if anyone still uses them these days)
return html[:i] + attrs + B(" ") + attrsToAdd + html[j:]

Silas S. Brown
committed
# --------------------------------------------------
# View-source support etc
# --------------------------------------------------

Silas S. Brown
committed
def ampEncode(t):
if type(t)==bytes: return t.replace(B("&"),B("&")).replace(B("<"),B("<")).replace(B(">"),B(">"))
else: return t.replace("&","&").replace("<","<").replace(">",">")
def txt2html(t):
t = ampEncode(t)
if type(t)==bytes: return t.replace(B("\n"),B("<br>"))
else: return t.replace("\n","<br>")

Silas S. Brown
committed

Silas S. Brown
committed
def ampDecode(t): return t.replace("<","<").replace(">",">").replace("&","&") # called with unicode in Python 2 and str in Python 3, either way this should just work

Silas S. Brown
committed
# ampDecode is needed if passing text with entity names to Renderer below (which ampEncode's its result and we might want it to render & < > chars)
# (shouldn't need to cope with other named entities: find_text_in_HTML already processes all known ones in htmlentitydefs, and LXML also decodes all the ones it knows about)
# --------------------------------------------------
# Support old phones etc: CJK characters to images
# --------------------------------------------------

Silas S. Brown
committed
class Renderer:
def __init__(self):
self.renderFont = None
self.hanziW,self.hanziH = 0,0
def font(self):
if not self.renderFont: # first time
try: import ImageFont

Silas S. Brown
committed
except:
try: import PIL.ImageFont as ImageFont
except: from PIL import ImageFont

Silas S. Brown
committed
if options.renderFont: self.renderFont = ImageFont.truetype(options.renderFont, options.renderSize, encoding="unic")
else: self.renderFont = ImageFont.load_default()
return self.renderFont
def getMarkup(self,unitext):
i=0 ; import unicodedata
width = 0 ; imgStrStart = -1
ret = [] ; copyFrom = 0

Silas S. Brown
committed
if options.renderWidth==0: doImgEnd=lambda:None
else:
def doImgEnd():

Silas S. Brown
committed
6052
6053
6054
6055
6056
6057
6058
6059
6060
6061
6062
6063
6064
6065
6066
6067
6068
6069
6070
6071
6072
6073
6074
6075
6076
6077
6078
6079
6080
6081
6082
6083
if imgStrStart >= 0 and width <= options.renderWidth and len(ret) > imgStrStart + 1:
ret.insert(imgStrStart,'<nobr>')
ret.append('</nobr>')
if options.renderBlocks: combining=lambda x:False
else: combining=unicodedata.combining
checkAhead = (options.renderNChar>1 or not options.renderBlocks)
while i<len(unitext):
if inRenderRange(unitext[i]) and not combining(unitext[i]): # (don't START the render with a combining character - that belongs to the character before)
j = i+1
if checkAhead:
charsDone = 1
while j<len(unitext) and ((charsDone < options.renderNChar and inRenderRange(unitext[j])) or combining(unitext[j])):
if not combining(unitext[j]): charsDone += 1
j += 1
rep,w=self.getMarkup_inner(unitext[i:j])
width += w
if i>copyFrom: ret.append(ampEncode(unitext[copyFrom:i]))
if imgStrStart==-1: imgStrStart = len(ret)
ret.append(rep)
copyFrom = i = j
else:
doImgEnd() ; i += 1
width = 0 ; imgStrStart = -1
doImgEnd()
if i>copyFrom: ret.append(ampEncode(unitext[copyFrom:i]))
return u"".join(ret)
def getMarkup_inner(self,unitext):
if options.renderBlocks and self.hanziW:
w,h = self.hanziW*len(unitext), self.hanziH
else:
w,h = self.font().getsize(unitext)
if options.renderBlocks:

Silas S. Brown
committed
self.hanziW = int(w/len(unitext))

Silas S. Brown
committed
self.hanziH = h

Silas S. Brown
committed
return ('<img src="%s%s" width=%s height=%s>' % (options.renderPath,imgEncode(unitext),w,h)), w # (%s is faster than %d apparently, and format strings are faster than ''.join)

Silas S. Brown
committed
def getImage(self,uri):
if not options.render or not uri.startswith(options.renderPath): return False
try: import ImageDraw,Image
except:

Silas S. Brown
committed
try:
import PIL.ImageDraw as ImageDraw
import PIL.Image as Image
except:
from PIL import ImageDraw,Image

Silas S. Brown
committed
try: text=imgDecode(uri[len(options.renderPath):])
except: return False # invalid base64 = fall back to fetching the remote site
size = self.font().getsize(text) # w,h
if options.renderInvert: bkg,fill = 0,1
else: bkg,fill = 1,0
img=Image.new("1",size,bkg) # "1" is 1-bit
ImageDraw.Draw(img).text((0, 0),text,font=self.font(),fill=fill)

Silas S. Brown
committed
dat=BytesIO()

Silas S. Brown
committed
6104
6105
6106
6107
6108
6109
6110
6111
6112
6113
6114
6115
6116
6117
6118
6119
6120
6121
6122
6123
6124
img.save(dat,options.renderFormat)
return dat.getvalue()
Renderer=Renderer()
def create_inRenderRange_function(arg):
# create the inRenderRange function up-front (don't re-parse the options every time it's called)
global inRenderRange
if type(arg)==type(""): arg=arg.split(',')
arg = [tuple(int(x,16) for x in lh.split(':')) for lh in arg] # [(l,h), (l,h)]
if len(arg)==1: # Only one range - try to make it fast
l,h = arg[0]
while l==0 or not unichr(l).strip(): l += 1 # NEVER send whitespace to the renderer, it will break all hope of proper word-wrapping in languages that use spaces. And chr(0) is used as separator. (TODO: what if there's whitespace in the MIDDLE of one of the ranges? but don't add the check to inRenderRange itself unless we've confirmed it might be needed)
inRenderRange=lambda uc:(l <= ord(uc) <= h)
elif arg: # More than one range
inRenderRange=lambda uc:(ord(uc)>0 and uc.strip() and any(l <= ord(uc) <= h for l,h in arg))
else: inRenderRange=lambda uc:(ord(uc) and uc.strip())
def imgEncode(unitext):
# Encode unitext to something URL-safe, try to be efficient especially in small cases
# Normally base64-encoded UTF-8 (output will be a multiple of 4 bytes)
# but some single characters will be OK as-is, and 2 or 3 bytes could hex a unichr under U+1000

Silas S. Brown
committed
# This function needs to be FAST - it can be called thousands of times during a page render

Silas S. Brown
committed
if len(unitext)==1:

Silas S. Brown
committed
o = ord(unitext)
if o < 0x1000:
# TODO: create_inRenderRange_function can also re-create this function to omit the above test if we don't have any ranges under 0x1000 ? (but it should be quite quick)

Silas S. Brown
committed
if unitext in letters+digits+"_.-": return unitext

Silas S. Brown
committed
elif 0xf<ord(unitext): return hex(ord(unitext))[2:]
elif o <= 0xFFFF: # (TODO: don't need that test if true for all our render ranges)
# TODO: make this optional? hex(ord(u))[-4:] is nearly 5x faster than b64encode(u.encode('utf-8')) in the case of 1 BMP character (it's faster than even just the .encode('utf-8') part), but result could also decode with base64, so we have to add an extra '_' byte to disambiguate, which adds to the traffic (although only a small amount compared to IMG markup anyway)
return '_'+hex(o)[-4:]

Silas S. Brown
committed
return S(base64.b64encode(unitext.encode('utf-8')))

Silas S. Brown
committed
def imgDecode(code):
if len(code)==1: return code
elif len(code) <= 3: return unichr(int(code,16))

Silas S. Brown
committed
elif code.startswith("_"): return unichr(int(code[1:],16)) # (see TODO above)

Silas S. Brown
committed
else: return base64.b64decode(code).decode('utf-8','replace')
# --------------------------------------------------
# Support pinging watchdogs and Dynamic DNS services
# --------------------------------------------------

Silas S. Brown
committed
class Dynamic_DNS_updater:
def __init__(self):
if not (options.ip_query_url and options.ip_change_command): return

Silas S. Brown
committed
self.currentIP = None
self.forceTime=0

Silas S. Brown
committed
self.aggressive_mode = False
# check for user:password@ in ip_query_url2
global ip_query_url2,ip_query_url2_user,ip_query_url2_pwd,ip_url2_pwd_is_fname
ip_query_url2 = options.ip_query_url2
ip_query_url2_user=ip_query_url2_pwd=ip_url2_pwd_is_fname=None
if ip_query_url2 and not ip_query_url2=="upnp":
netloc = urlparse.urlparse(ip_query_url2).netloc
if '@' in netloc:
auth,rest = netloc.split('@',1)
ip_query_url2 = ip_query_url2.replace(netloc,rest,1)
ip_query_url2_user,ip_query_url2_pwd = auth.split(':',1)
ip_url2_pwd_is_fname = os.path.isfile(ip_query_url2_pwd)
# and start the updater

Silas S. Brown
committed
IOLoopInstance().add_callback(lambda *args:self.queryIP())

Silas S. Brown
committed
def queryLocalIP(self):
# Queries ip_query_url2 (if set, and if we know current IP). Depending on the response/situation, either passes control to queryIP (which sets the next timeout itself), or sets an ip_check_interval2 timeout.
if not ip_query_url2 or not self.currentIP:
return self.queryIP()
debuglog("queryLocalIP")
if ip_query_url2=="upnp":
def run():
try:
miniupnpc.discover() # every time - it might have rebooted or changed
miniupnpc.selectigd()

Silas S. Brown
committed
addr = S(miniupnpc.externalipaddress())

Silas S. Brown
committed
if addr == self.currentIP: IOLoopInstance().add_callback(lambda *args:(self.newIP(addr),IOLoopInstance().add_timeout(time.time()+options.ip_check_interval2,lambda *args:self.queryLocalIP()))) # (add_timeout doesn't always work when not from the IOLoop thread ??)
else: IOLoopInstance().add_callback(self.queryIP)
threading.Thread(target=run,args=()).start()
return

Silas S. Brown
committed
def handleResponse(r):

Silas S. Brown
committed
if r.error or not B(self.currentIP) in B(r.body):

Silas S. Brown
committed
return self.queryIP()
# otherwise it looks like the IP is unchanged:
self.newIP(self.currentIP) # in case forceTime is up

Silas S. Brown
committed
IOLoopInstance().add_timeout(time.time()+options.ip_check_interval2,lambda *args:self.queryLocalIP())

Silas S. Brown
committed
if ip_query_url2_user:
# some routers etc insist we send the non-auth'd request first, and the credentials only when prompted (that's what Lynx does with the -auth command line), TODO do we really need to do this every 60secs? (do it only if the other way gets an error??) but low-priority as this is all local-net stuff (and probably a dedicated link to the switch at that)

Silas S. Brown
committed
if ip_url2_pwd_is_fname: pwd=open(ip_query_url2_pwd).read().strip() # re-read each time
else: pwd = ip_query_url2_pwd

Silas S. Brown
committed
callback = lambda r:(curlFinished(),doCallback(None,MyAsyncHTTPClient().fetch,handleResponse, ip_query_url2, auth_username=ip_query_url2_user,auth_password=pwd))

Silas S. Brown
committed
else: callback = handleResponse

Silas S. Brown
committed
doCallback(None,MyAsyncHTTPClient().fetch,callback,ip_query_url2)

Silas S. Brown
committed
def queryIP(self):

Silas S. Brown
committed
# Queries ip_query_url, and, after receiving a response (optionally via retries if ip_query_aggressive), sets a timeout to go back to queryLocalIP after ip_check_interval (not ip_check_interval2)

Silas S. Brown
committed
debuglog("queryIP")
def handleResponse(r):

Silas S. Brown
committed
if not r.error:

Silas S. Brown
committed
self.newIP(S(r.body.strip()))

Silas S. Brown
committed
if self.aggressive_mode:
logging.info("ip_query_url got response, stopping ip_query_aggressive")
self.aggressive_mode = False
elif options.ip_query_aggressive:
if not self.aggressive_mode:
logging.info("ip_query_url got error, starting ip_query_aggressive")
self.aggressive_mode = True
global pimote_may_need_override ; pimote_may_need_override = True # in case we're running that as well and it fails to detect the router issue via its own DNSRequest (happened once on Zyxel AMG1302-T11C: upnp stopped, connectivity stopped so got here, even DHCP stopped but somehow DNSRequest kept going without returning the internal-only IP or error, so better put this path in too)

Silas S. Brown
committed
return self.queryIP()

Silas S. Brown
committed
IOLoopInstance().add_timeout(time.time()+options.ip_check_interval,lambda *args:self.queryLocalIP())
doCallback(None,MyAsyncHTTPClient().fetch,handleResponse,options.ip_query_url)

Silas S. Brown
committed
def newIP(self,ip):
debuglog("newIP "+ip)
if ip==self.currentIP and (not options.ip_force_interval or time.time()<self.forceTime): return
try: socket.inet_aton(ip) # IPv4 only
except socket.error: # try IPv6
try: socket.inet_pton(socket.AF_INET6,ip)
except socket.error: return # illegal IP, maybe a temporary error from the server
self.currentIP = ip

Silas S. Brown
committed
cmd = options.ip_change_command+" "+ip

Silas S. Brown
committed
if len(options.ip_change_command) < 50: logging.info("ip_change_command: "+cmd)
else: logging.info("Running ip_change_command for "+ip)
sp=subprocess.Popen(cmd,shell=True,stdin=subprocess.PIPE)

Silas S. Brown
committed
self.forceTime=time.time()+options.ip_force_interval
def retry(sp):
triesLeft = options.ip_change_tries-1 # -ve if inf
while triesLeft:
if not sp.wait():
logging.info("ip_change_command succeeded for "+ip)
return
logging.info("ip_change_command failed for "+ip+", retrying in "+str(options.ip_change_delay)+" seconds")
time.sleep(options.ip_change_delay)
sp=subprocess.Popen(cmd,shell=True,stdin=subprocess.PIPE)
triesLeft -= 1
if sp.wait(): logging.error("ip_change_command failed for "+ip)
else: logging.info("ip_change_command succeeded for "+ip)
threading.Thread(target=retry,args=(sp,)).start()

Silas S. Brown
committed
def pimote_thread():
routerIP, ispDomain, internalResponse, deviceNo = options.pimote.split(',')
try: deviceNo = int(deviceNo)
except: pass # "all" etc is OK
import DNS # apt-get install python-dns
if deviceNo=="all": p17,p22,p23 = 1,1,0
elif deviceNo==1: p17,p22,p23 = 1,1,1
elif deviceNo==2: p17,p22,p23 = 1,0,1
elif deviceNo==3: p17,p22,p23 = 0,1,1
elif deviceNo==4: p17,p22,p23 = 0,0,1
# TODO: unofficial 1 0 0 and 0 1 0 can be programmed (taking number of devices up to 6), + a 7th can be programmed on 000 but only for switch-on (as '0000' just resets the board; can switch off w. 'all'). Can program multiple switches to respond to same signal.

Silas S. Brown
committed
else: raise Exception("Invalid Pi-mote device number "+repr(deviceNo))
def t():
lastOK = True
helper_threads.append('PiMote')

Silas S. Brown
committed
while options.pimote:
if pimote_may_need_override: ok=False
else:
try:

Silas S. Brown
committed
r = DNS.DnsRequest(server=routerIP,timeout=5).req(name=ispDomain,qtype="A")
ok = r.answers and not(any(i['data']==internalResponse for i in r.answers))
except: ok = False

Silas S. Brown
committed
if ok or lastOK:
for i in xrange(30): # TODO: configurable?
if options.pimote and not pimote_may_need_override: time.sleep(1)

Silas S. Brown
committed
else: break
lastOK = ok ; continue
if pimote_may_need_override: reason = "" # will be evident from previous log line
else: reason = " (%s lookup fail)" % ispDomain
logging.info("PiMote: power-cycling the router"+reason)

Silas S. Brown
committed
# Takes 2 minutes (because we have no direct way to verify
# the sockets have received our signal, so just have to make
# the hopefully-worst-case assumptions)
def w(g,v):
f = "/sys/class/gpio/gpio%d/value" % g
try: open(f,'w').write(str(v)+"\n") # not sure if \n really needed
except: logging.error("PiMote: unable to write to "+f)

Silas S. Brown
committed
for OnOff in [0, 1]:
w(25,0) # stop TX, just in case
w(17,p17), w(22,p22), w(23,p23), w(27,OnOff)
for Try in range(10): # sometimes the signal fails
w(25,1) # start TX
time.sleep(4)
w(25,0) # stop TX
time.sleep(1)
time.sleep(199) # give it time to start up before we test it again, plus time to ssh in and fix if we're in a reboot cycle due to ISP being taken over and deleting their old domain or something

Silas S. Brown
committed
helper_threads.remove('PiMote')
threading.Thread(target=t,args=()).start()

Silas S. Brown
committed
def open_upnp():
if options.ip_query_url2=="upnp":
global miniupnpc ; import miniupnpc # sudo pip install miniupnpc or apt-get install python-miniupnpc
miniupnpc = miniupnpc.UPnP()
miniupnpc.discoverdelay=200
watchdog = None
def openWatchdog():
global watchdog
watchdog = WatchdogPings()

Silas S. Brown
committed
class WatchdogPings:

Silas S. Brown
committed
self.wFile = open(options.watchdogDevice, 'wb')
else: self.wFile = None
# then call start() after privileges are dropped
def start(self):
if not self.wFile: return # no watchdog
if options.watchdogWait: threading.Thread(target=self.separate_thread,args=()).start()

Silas S. Brown
committed
self.ping()
def stop(self):
if not self.wFile: return # no watchdog
options.watchdog = 0 # tell any separate_thread() to stop (that thread is not counted in helper_threads)
self.wFile.write('V') # this MIGHT be clean exit, IF the watchdog supports it (not all of them do, so it might not be advisable to use the watchdog option if you plan to stop the server without restarting it)
self.wFile.close()

Silas S. Brown
committed
def separate_thread(self): # version for watchdogWait
# (does not adjust helper_threads / can't be "runaway")

Silas S. Brown
committed
global watchdog_mainServerResponded # a flag. Do NOT timestamp with time.time() - it can go wrong if NTP comes along and re-syncs the clock by a large amount

Silas S. Brown
committed
def respond(*args):
global watchdog_mainServerResponded
debuglog("watchdogWait: responding",stillIdle=True)

Silas S. Brown
committed
watchdog_mainServerResponded = True
respond() ; stopped = 0 ; sleptSinceResponse = 0
while options.watchdog:
if watchdog_mainServerResponded:

Silas S. Brown
committed
self.ping()
if stopped:
logging.info("Main thread responded, restarting watchdog ping")
stopped = 0

Silas S. Brown
committed
watchdog_mainServerResponded = False
sleptSinceResponse = 0

Silas S. Brown
committed
IOLoopInstance().add_callback(respond)

Silas S. Brown
committed
elif sleptSinceResponse < options.watchdogWait: self.ping() # keep waiting for it

Silas S. Brown
committed
elif not stopped:
logging.error("Main thread unresponsive, stopping watchdog ping. lastDebugMsg: "+lastDebugMsg)

Silas S. Brown
committed
stopped = 1 # but don't give up (it might respond just in time)
time.sleep(options.watchdog)

Silas S. Brown
committed
sleptSinceResponse += options.watchdog # "dead reckoning" to avoid time.time()

Silas S. Brown
committed
def ping(self):
if not options.watchdogWait: debuglog("pinging watchdog",logRepeats=False,stillIdle=True) # ONLY if run from MAIN thread, otherwise it might overwrite the real lastDebugMsg of where we were stuck

Silas S. Brown
committed
self.wFile.write('a') ; self.wFile.flush()
if not options.watchdogWait: # run from main thread

Silas S. Brown
committed
IOLoopInstance().add_timeout(time.time()+options.watchdog,lambda *args:self.ping())

Silas S. Brown
committed
# else one ping only (see separate_thread)
# --------------------------------------------------
# Support for "slow server delegates to fast server"
# (e.g. always-on Raspberry Pi + sometimes-on dev box)
# --------------------------------------------------

Silas S. Brown
committed
fasterServer_up = False

Silas S. Brown
committed
def FSU_set(new_FSU,interval):
# sets new fasterServer_up state, and returns interval to next check
global fasterServer_up
fsu_old = fasterServer_up
fasterServer_up = new_FSU
if not fasterServer_up == fsu_old:
if fasterServer_up: logging.info("fasterServer %s came up - forwarding traffic to it" % options.fasterServer)
else: logging.info("fasterServer %s went down - handling traffic ourselves" % options.fasterServer)
# debuglog("fasterServer_up="+repr(fasterServer_up)+" (err="+repr(r.error)+")",logRepeats=False,stillIdle=True)

Silas S. Brown
committed
if fasterServer_up: return 1 # TODO: configurable? fallback if timeout when we try to connect to it as well?
elif interval < 60: interval *= 2 # TODO: configurable?
return interval

Silas S. Brown
committed
class checkServer:

Silas S. Brown
committed
def __init__(self):
self.client = self.pendingClient = None
self.count = 0
self.interval=1
def setup(self):
if not options.fasterServer: return
if not ':' in options.fasterServer: options.fasterServer += ":80" # needed for the new code
logging.getLogger("tornado.general").disabled=1 # needed to suppress socket-level 'connection refused' messages from ping2 code in Tornado 3
class NoConErrors:
def filter(self,record): return not record.getMessage().startswith("Connect error on fd")
logging.getLogger().addFilter(NoConErrors()) # ditto in Tornado 2 (which uses the root logger) (don't be tempted to combine this by setting tornado.general to a filter, as the message might change in future Tornado 3 releases)

Silas S. Brown
committed
IOLoopInstance().add_callback(self)

Silas S. Brown
committed
def __call__(self):

Silas S. Brown
committed
if options.fasterServerNew:

Silas S. Brown
committed
# TODO: might be bytes in the queue if this server somehow gets held up. Could try readUntilClose(client,close,stream)

Silas S. Brown
committed
if (self.client and self.count >= 2) or self.pendingClient: # it didn't call serverOK on 2 consecutive seconds (TODO: customizable?), or didn't connect within 1sec - give up
try: self.pendingClient.close()
except: pass
try: self.client.close()
except: pass
self.pendingClient = self.client = None
self.interval = FSU_set(False,self.interval)

Silas S. Brown
committed
return IOLoopInstance().add_timeout(time.time()+self.interval,lambda *args:checkServer())

Silas S. Brown
committed
elif self.client: self.count += 1
else: # create new self.pendingClient
server,port = options.fasterServer.rsplit(':',1)
self.pendingClient = tornado.iostream.IOStream(socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0))
def send_request(*args):
try:

Silas S. Brown
committed
self.pendingClient.write(B('GET /ping2 HTTP/1.0\r\nUser-Agent: ping\r\n\r\n'))

Silas S. Brown
committed
self.client = self.pendingClient
self.pendingClient = None

Silas S. Brown
committed
readUntilClose(self.client,lambda *args:True,lambda *args:self.serverOK())

Silas S. Brown
committed
except: pass

Silas S. Brown
committed
doCallback(None,self.pendingClient.connect,send_request,(server,int(port)))
IOLoopInstance().add_timeout(time.time()+1,lambda *args:checkServer()) # check back in 1sec to see if it connected OK (should do if it's local)

Silas S. Brown
committed
else: # old version - issue HTTP requests to /ping
def callback(r):
self.interval = FSU_set(not r.error,self.interval)
if not fasterServer_up: self.client = None

Silas S. Brown
committed
IOLoopInstance().add_timeout(time.time()+self.interval,lambda *args:checkServer())
if not self.client:
self.client=MyAsyncHTTPClient()
curlFinished() # we won't count it here

Silas S. Brown
committed
doCallback(None,self.client.fetch,callback,"http://"+options.fasterServer+"/ping",connect_timeout=1,request_timeout=1,user_agent="ping",use_gzip=False)

Silas S. Brown
committed
def serverOK(self):
# called when any chunk is available from the stream (normally once a second, but might catch up a few bytes if we've delayed for some reason)
self.interval = FSU_set(True,0)
self.count = 0

Silas S. Brown
committed
checkServer=checkServer()
# --------------------------------------------------
# Debugging and status dumps
# --------------------------------------------------

Silas S. Brown
committed
lastDebugMsg = "None" # for 'stopping watchdog ping'
def debuglog(msg,logRepeats=True,stillIdle=False):
global lastDebugMsg, profileIdle

Silas S. Brown
committed
if logRepeats or not msg==lastDebugMsg:
if not options.logDebug: logging.debug(msg)
elif options.background: logging.info(msg)
else: sys.stderr.write(time.strftime("%X ")+msg+"\n")
lastDebugMsg = msg ; global status_dump_requested
if status_dump_requested:
status_dump_requested = False
showProfile(pjsOnly=True) # TODO: document that SIGUSR1 also does this? (but doesn't count reqsInFlight if profile wasn't turned on, + it happens on next debuglog call (and shown whether toggled on OR off, to allow rapid toggle just to show this))
def initLogDebug():
if hasattr(signal,"SIGUSR1") and not wsgi_mode:
signal.signal(signal.SIGUSR1, toggleLogDebug)
if hasattr(signal,"SIGUSR2"):
signal.signal(signal.SIGUSR2, requestStatusDump)
def toggleLogDebug(*args):
"SIGUSR1 handler (see logDebug option help)"
# Don't log anything from the signal handler
# (as another log message might be in progress)
# Just toggle the logDebug flag
options.logDebug = not options.logDebug
requestStatusDump()
def requestStatusDump(*args):
"SIGUSR2 handler (requests status dump, currently from JS proxy only)" # TODO: document this (and that SIGUSR1 also calls it)
global status_dump_requested ; status_dump_requested = True

Silas S. Brown
committed
# --------------------------------------------------
# And finally...
# --------------------------------------------------

Silas S. Brown
committed
if __name__ == "__main__": main()
else: setup_defined_globals() # for wrapper import