#!/usr/bin/env python # Copyright (C) 2010, Adam Fourney <afourney@cs.uwaterloo.ca> # # This file is part of Adaptable GIMP # # Adaptable GIMP is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # import sys, subprocess, socket, os, errno, random, hashlib, time, re if sys.platform == 'win32': from threading import Thread import array class Value: def __init__(self, typecode, arg): self.__arr = array.array(typecode, [arg]) def getv(self): return self.__arr[0] def setv(self, val): self.__arr[0] = val value = property(getv, setv) else: from multiprocessing import Process, Value # CONSTANTS PROXY_TO = 'www.uwaterloo.ca' HOST = '127.0.0.1' PREFERRED_PORT = 8080 HEARTBEAT_PERIOD = 15 APPLICATION = './gimp-2.6.exe' if sys.platform == 'win32' else './gimp-2.6' ##################################################################### def main(): thread_q = [] shutdown = Value('i', 0) is_online = Value('i', 1) # Set up the socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Try finding a free port to listen on. # Stop after 10 tries. tries = 10 port = PREFERRED_PORT while (tries > 0): tries = tries - 1 try: s.bind((HOST, port)) s.listen(5) print "Listening on port {0}".format(port) break except socket.error, (err, msg): if (tries == 0): raise elif (err == errno.EADDRINUSE): port = random.randint(1024, 49151) continue else: raise # Set socket timeout s.settimeout(0.5) # Spawn the heartbeat heartbeat_thread = spawn_best_thread(target=start_heartbeat, args=(shutdown,is_online)) heartbeat_thread.start() # Spawn our process app_thread = spawn_best_thread(target=spawn_app, args=(shutdown,port)) app_thread.start() # Handle incoming connections while shutdown.value == 0: # Poll the children and reap zombies new_q = [] for p in thread_q: if p.is_alive(): new_q.append(p) thread_q = new_q # Accept a new connection try: conn, addr = s.accept() except socket.timeout: continue except socket.error, err: if (err == errno.EINTR): continue else: raise # Service the request in a new thread conn.settimeout(None) p = spawn_best_thread(target=service_request, args=(conn,is_online)) thread_q.append(p) p.start() s.close() ##################################################################### def spawn_best_thread(target, args): if sys.platform == 'win32': return Thread(target=target, args=args) else: return Process(target=target, args=args) ##################################################################### def start_heartbeat(shutdown, is_online): sleep_for = 0 while shutdown.value == 0: # Sleep for half a second at a time to allow for checking of the # shutdown condition. if (sleep_for > 0): time.sleep(0.5) sleep_for = sleep_for - 0.5 continue # Do actual work start_time = time.clock() previous_status = is_online.value new_status = previous_status try: client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.settimeout(HEARTBEAT_PERIOD) client.connect((PROXY_TO, 80)) client.sendall("HEAD / HTTP/1.1\r\nHost: {0}\r\nConnection: close\r\n\r\n".format(PROXY_TO)) response = "" while 1: data = client.recv(1024) if not data: break response += data if re.search('^HTTP\/\d\.\d\s+200\s+OK', response): new_status = 1 else: new_status = 0 except socket.error: new_status = 0 except socket.timeout: new_status = 0 # Shutdown and close the connection, but report no errors try: client.shutdown(socket.SHUT_RDWR); except socket.error: pass try: client.close() except socket.error: pass if new_status != previous_status: print "Connection status changed. Now {0}".format('Online' if new_status else 'Offline') is_online.value = new_status # Arrange to sleep a little sleep_for = HEARTBEAT_PERIOD - (time.clock() - start_time) ##################################################################### def service_request(conn, is_online): # Read the request, respond and exit. request= "" while 1: data = conn.recv(1024) if not data: break request += data # Requests are terminated by the following sequence pos = request.find("\r\n\r\n") if (pos > -1): data = data[0:pos+4] break response = make_request(request, is_online) conn.sendall(response) try: conn.shutdown(socket.SHUT_RDWR); except socket.error: pass conn.close() ##################################################################### def make_request(data, is_online): # Split the request into lines lines = data.split("\r\n") if data.endswith("\r\n"): lines.pop() # Hash the first line of the request for use as a key first_line = lines[0]; key = hashlib.md5(first_line).hexdigest() # Check for special PROXY messages if first_line == "PROXY GET STATUS": status_str = "Online" if is_online.value > 0 else "Offline" return "Status: {0}\r\n\r\n".format(status_str) # Exit early if we are offline if is_online.value == 0: return read_from_cache(key) # Modify the request for proxying data = ""; for line in lines: if line.startswith('Connection:'): data = data + 'Connection: close' + "\r\n" elif line.startswith('Host:'): data = data + 'Host: {0}'.format(PROXY_TO) + "\r\n" else: data = data + line + "\r\n" # Try to fetch from the server, but fall back on the cache if we're offline try: client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.settimeout(HEARTBEAT_PERIOD) client.connect((PROXY_TO, 80)) client.sendall(data) except: return read_from_cache(key) # Read the response response = "" while 1: data = client.recv(1024) if not data: break response += data client.close() # Cache the response and return write_to_cache(key, response) return response ##################################################################### # Read a response from the cache. Return 404 if there is a problem. def read_from_cache(key): try: f = open("web_cache/{0}.tmp".format(key), "r") f_data = f.read() f.close() except IOError as (errnum, strerror): if (errnum == errno.ENOENT): response = """HTTP/1.1 404 Not Found\r Content-Type: text/html\r Connection: close\r Content-Length: 78\r \r \r <HTML><HEAD><TITLE>404</TITLE></HEAD><BODY>Page Not Found: 404</BODY></HTML>""" return response else: raise return f_data ##################################################################### # Write a response to the cache. Create a web_cache directory if required. def write_to_cache(key, response): if not os.path.isdir('web_cache'): os.mkdir('web_cache') f = open('web_cache/{0}.tmp'.format(key), 'w') f.write(response) f.close() ##################################################################### # Spawn the main process def spawn_app(shutdown, port): os.environ['AGIMP_PROXY_PORT'] = str(port) subprocess.call([APPLICATION]) shutdown.value = 1 ######## CALL MAIN ######## if __name__ == '__main__': main()