#!/usr/bin/env python

"""This will behave badly under isinstance().
if isinstance(x,y) probably not isinstance(remote_x, y).
Also, dicts are passed by value instead of by reference,
so if you remotely modify a dict, you will not see the modification
locally.
"""


# import exceptions
import weakref
import types
import sys
import cPickle
import g_threading as T
import imp
import StringIO
import time
import traceback
import g_pipe
import g_mailbox as GM
import os

__version__ = '0.23'
DEBUG = 2
# DEBUG = False
LOGFILE = './g_proxy.client'


Id = id

TRANSPORTABLE_TYPES = (	# We transport only immutable types.
		# int, float, complex, list, tuple, str,
		int, float, complex, tuple, str, long,
		types.NoneType, types.BooleanType,
		# dict, types.ClassType, types.InstanceType, set, frozenset
		types.ClassType, types.InstanceType, frozenset,
		set,	# This ought not to be transported, but it fails the proxy
			# equality test.    This indicates some kind of bug.
		dict	# Dict needs to be here to make cPickle.load() work.
		)
TRANSPORTABLE_TYPES_O = frozenset( [ Id(q) for q in TRANSPORTABLE_TYPES] + [Id(object)] )

NEVER, ALWAYS, MAYBE = 0, 1, 2
_Names = ['INITIALIZATION', 'GET_CLASSINFO', 'R_TEST', 'RM_STORE',
	'SETA', 'SETAP',
	'DELA', 'GETA', 'RCALL', 'RMCALL', 'IMPORT_MOD', 'GLOBALS',
	'STDOUT', 'STDERR', 'VALUE', 'EXCEPTION',
	'QUIT_1', 'QUIT_2', 'ABORT'
	]

NO_CI_ATTR = frozenset(['__dict__', '__class__', '__weakref__', '__init__',
			'__getattribute__', '__setattr__', '__delattr__',
			'__del__',
			'__new__'
			]
		)


class ProtocolError(Exception):
	def __init__(self, *a):
		Exception.__init__(self, *a)

_loglock = T.Lock()
_logfd = None
def log(*args):
	global _logfd
	global _loglock
	if _logfd is None:
		return
	_loglock.acquire()
	for (i,a) in enumerate(args):
		if i == 0:
			_logfd.writelines( [str(a), '\n'] )
		else:
			_logfd.writelines( ['\t', str(a), '\n'] )
	_logfd.flush()
	_loglock.release()


class test0(object):
	pass

class test0classic:
	pass

class test1(object):
	whatami = 'I am the class'

	def __init__(self, x):
		self.x = x
		self.whatami = 'I am the instance'

	def __eq__(self, other):
		return self.x == other.x and hasattr(self, '__eq__') and hasattr(other, '__eq__')

	def w(self):
		prinfo('executing test1.w()')
		return self.whatami

assert test1(3) == test1(3)

class test1classic:
	whatami = 'I am the class'

	def __init__(self, x):
		self.x = x
		self.whatami = 'I am the instance'

	def __eq__(self, other):
		return self.x == other.x and hasattr(self, '__eq__') and hasattr(other, '__eq__')

	def w(self):
		prinfo('executing test1.w()')
		return self.whatami
assert test1classic(3) == test1classic(3)


TEST = tuple( [
		(None, False, 1, 2.0, frozenset([33,11]), (6,), 'seven', {8:9}, {9:{}}),
		complex(3.0, 4.0),
		( set([10]), ),
		test1(3),
		test1classic(3),
		"end of equality test",
		# Up to here, the local and remote test cases
		# should return equality.
		test0(),
		test0classic(),
		sys,
		(Exception(), Exception(5))
	])


class proxy_object(object):
	pass

def Prinfo(s):
	log(s)


if DEBUG:
	def Oid(o):
		return '%s%d' % (str(type(o)), Id(o))
	def diO(s):
		if s == '':
			return None
		return s
	# def prinfo(s):
		# sys.stderr.writelines([s, '\n'])
		# sys.stderr.flush()
	def prinfo(s):
		log(s)
	def S(s):
		if isinstance(s, proxy_object):
			return '<REMOTE %s>' % id(s)
		return str(s)

	for tmp in _Names:
		globals()[tmp] = tmp
	del tmp
else:
	def Oid(o):
		return '%d%d' % (Id(type(o)), Id(o))
	def diO(s):
		if s == '':
			return None
		return s
	def prinfo(s):
		pass
	def S(s):
		pass
	for (i, tmp) in enumerate(_Names):
		globals()[tmp] = i + 100
	del tmp
	del i
del _Names


class object_store(object):
	def __init__(self, storetype=dict):
		self.store = storetype()
		self.lock = T.Lock(verbose=DEBUG>1, name='object_store')
		# prinfo('OS:%d__init__: locked: %s' % (Id(self), self.lock.locked()))

	def put(self, xid, x):
		# prinfo('OS%d before put lock: %s' % (Id(self), self.lock.locked()))
		self.lock.acquire()
		self.store[xid] = x
		# prinfo('OS%d in put lock' % Id(self))
		self.lock.release()
		# prinfo('OS%d past put lock: locked: %s' % (Id(self), self.lock.locked()))
		return xid

	def putid(self, x):
		return self.put(Oid(x), x)


	def rm(self, oid):
		self.lock.acquire()
		del self.store[oid]
		# prinfo('OS in rm lock')
		self.lock.release()

	def get(self, xid):
		self.lock.acquire()
		try:
			tmp = self.store[xid]
		finally:
			self.lock.release()
		return tmp


	def disconnect(self):
		"""This is only called for the rowlp case,
		and the object store is a WeakDictionary, so
		if we encounter an object, we know that something
		else references it.   Consequently, we cannot
		depend on it being deleted and calling its __del__ item
		any time soon.
		"""
		while self.store:
			self.lock.acquire()
			oid, v = self.store.popitem()
			try:
				object.__getattribute__(v, 'conn')
			except AttributeError:
				pass
			else:
				object.__setattr__(v, 'conn', None)
			self.lock.release()


	def clear(self):
		self.lock.acquire()
		self.store.clear()
		self.lock.release()



def deal_with_arbitrary_exception(c, boxid):
	ty, ob, trb = sys.exc_info()
	if ty is None:
		log('Whoops: deal_with_arbitrary_exception', (ty, ob))
	tmp = tuple(traceback.format_tb(trb))
	del trb
	log('INFO: deal_with... %s.%s' % (ty.__module__, ty.__name__), *(ob.args))
	log('INFO: deal_with... traceback:', *tuple(tmp))
	c.send(EXCEPTION, boxid, (ty.__name__, ty.__module__,
					Oid(ty), ob.args, tmp
					),
			proxy=NEVER
		)
	del ob



class C_threadstate(object):
	IDLE = 0
	BUSY = 1
	STARTING = 2

	def __init__(self):
		self.lock = T.Condition(verbose=DEBUG>1, name='threadstate')
		self.state = {}
		# self.terminated = []
		self.expected_stops = 0

	def to_idle(self, me):
		self.lock.acquire()
		self.state[me] = self.IDLE
		prinfo('to_idle: %d %s' % (id(me), me))
		self._print()
		self.lock.notify()
		self.lock.release()

	def to_busy(self, me):
		self.lock.acquire()
		prinfo('to_busy: %d %s' % (id(me), me))
		self.state[me] = self.BUSY
		self._print()
		self.lock.notify()
		self.lock.release()

	def to_stopped(self, me):
		self.lock.acquire()
		prinfo('to_stopped: %d %s' % (id(me), me))
		# self.terminated.append(me)
		ime = me
		# if ime in self.state:
			# if self.expected_stops > 0:
				# self.expected_stops -= 1
			# del self.state[ime]
			# self.lock.notify()
		if self.expected_stops > 0:
			 self.expected_stops -= 1
		del self.state[ime]
		self._print()
		self.lock.notify()
		self.lock.release()

	def to_starting(self, me):
		self.lock.acquire()
		prinfo('to_starting: %d %s' % (id(me), me))
		self.state[me] = self.STARTING
		self._print()
		self.lock.notify()
		self.lock.release()

	def _print(self):
		"""Call within lock."""
		prinfo('threadstate:')
		m = {self.IDLE: 'idle', self.BUSY: 'busy', self.STARTING: 'starting'}
		for (k, v) in self.state.items():
			prinfo('\t%d : %s' % (k, m[v]))

	def anticipate_stop(self):
		prinfo('anticpate_stop')
		self.lock.acquire()
		self.expected_stops += 1
		self.lock.release()

	def n_busy(self):
		self.lock.acquire()
		n = 0
		for s in self.state.values():
			if s == self.BUSY:
				n += 1
		self.lock.release()
		return n

	def n_idle(self):
		self.lock.acquire()
		n = -self.expected_stops
		for s in self.state.values():
			if s == self.IDLE:
				n += 1
		self.lock.release()
		return max(0, n)


	def list(self):
		self.lock.acquire()
		tmp = self.state.keys()
		self.lock.release()
		return tmp

	def busylist(self):
		self.lock.acquire()
		tmp = []
		for (idme,s) in self.state.items():
			if s == self.BUSY:
				tmp.append(idme)
		self.lock.release()
		return tmp

	def wait_until_all_done(self):
		"""Called from main thread."""
		self.lock.acquire()
		# for thr in self.terminated:
			# prinfo('WUID: joining1 %d %s' % (id(thr), thr))
			# thr.join()
		while len(self.state) > 0:
			prinfo('WUID: waiting: %s' % str(self.state))
			self.lock.wait()
		# for thr in self.terminated:
			# prinfo('WUID: joining2 %d %s' % (id(thr), thr))
			# thr.join()
		self.lock.release()

	def reap_stopped_threads(self):
		"""Called from main thread."""
		pass


def thread_within_listen(inbox, threadstate, c):
	me = T.get_ident()
	prinfo('thread_within_listen starting')
	while True:
		threadstate.to_idle(me)
		try:
			# prinfo('twi: ready for inbox.get')
			info = inbox.get()
			threadstate.to_busy(me)
			# prinfo('twi: inbox.gotten')
		except GM.EOF:
			# prinfo('twibox.get got a EOF: %d %s' % (threadstate.n_idle(), threadstate.list()))
			threadstate.to_stopped(me)
			return
		except:
			threadstate.to_stopped(me)
			raise

		# if info is None:
			# break	# Shut a thread down per request.

		op, boxid, interior = info
		arg = c.pickler.load(interior)
		prinfo('TWI %s %s %s' % (S(op), S(boxid), S(arg)))

		if op == INITIALIZATION:
			v, oOidlist = arg
			assert v == __version__
			assert c.remote_special_objs is None
			c.remote_special_objs = dict(zip(oOidlist,
							[ proxy_object, type ]
							))
			c.s_listen_ready.put(None)
		elif op == QUIT_1:
			qc = 0
			while c._gc is not None and qc < 4:
				c._gc.collect()	# See what we can shake loose.
				time.sleep(0.1)	# Wait for it to have an effect on the other side,
						# and maybe shake loose something there.
				if threadstate.n_busy() <= 1:	# Wait till things quiet down.
					qc += 1
				else:
					prinfo('Busy - waiting: %s' % S(threadstate.busylist()))
			c.send(QUIT_2, None, None)
			c.reason = 'QUIT'

		elif op == STDOUT:
			if c.stdout is not None:
				c.stdout.fprint(arg)
		elif op == STDERR:
			if c.stderr is not None:
				c.stderr.fprint(arg)

		elif op == VALUE:
			# prinfo('value ready to find')
			mbox = c.box.find(boxid, 'value')
			# prinfo('value ready to put')
			mbox.put( (op, arg) )
		elif op == EXCEPTION:
			mbox = c.box.find(boxid, 'Exception')
			mbox.put( (op, arg) )


		# These are basically server calls below:
		elif op == RM_STORE:
			c.lowrp.rm(arg)
		elif op == GETA:
			objid, k = arg
			try:
				value = getattr(c.lowrp.get(objid), k)
			except:
				deal_with_arbitrary_exception(c, boxid)
			else:
				c.send(VALUE, boxid, value)

		elif op == R_TEST:
			c.send(VALUE, boxid, TEST[arg])
		elif op == GET_CLASSINFO:
			clobj = c.lo_clo.get(arg)
			try:
				bs = clobj.__bases__
			except AttributeError:
				bs = None
			if bs is not None:
				bs = tuple( [ c.lo_clo.putid(b) for b in bs ] )
			try:
				dct = tuple( clobj.__dict__.items() )
			except AttributeError:
				dct = None
			if dct is not None:
				dct = tuple( [ (k, callable(v))
					for (k,v) in getattr(clobj, '__dict__', {}).items()
						if k not in NO_CI_ATTR
					] )
			tmp = (clobj.__name__, bs, dct)
			c.send(VALUE, boxid, tmp, proxy=NEVER)
		elif op in (SETA, SETAP):
			objid, attr, value = arg
			if op == SETAP:
				value = c.lowrp.get(value)
			try:
				setattr(c.lowrp.get(objid), attr, value)
			except:
				deal_with_arbitrary_exception(c, boxid)
			else:
				c.send(VALUE, boxid, None, proxy=NEVER)
		elif op == DELA:
			objid, attr = arg
			try:
				delattr(c.lowrp.get(objid), attr)
			except:
				deal_with_arbitrary_exception(c, boxid)
			else:
				c.send(VALUE, boxid, None, proxy=NEVER)
		elif op in (RCALL, RMCALL):
			if op == RCALL:
				objid, args, kwpairs = arg
				obj = c.lowrp.get(objid)
			else:
				objid, aname, args, kwpairs = arg
				obj = getattr(c.lowrp.get(objid), aname)

			kwargs = dict(kwpairs)

			assert isinstance(args, tuple)
			assert isinstance(kwargs, dict)
			assert hasattr(obj, '__call__')
			try:
				value = obj(*args, **kwargs)
			except:
				log('R[M]CALL %s obj=%s' % (str(op), str(obj)), str(args), str(kwargs))
				deal_with_arbitrary_exception(c, boxid)
			else:
				c.send(VALUE, boxid, value)
		elif op == IMPORT_MOD:
			name, path = arg
			prinfo('import mod in twi: %s %s' % (S(name), S(path)))
			try:
				value = import_mod(name, path)
			except:
				deal_with_arbitrary_exception(c, boxid)
			else:
				prinfo('imported module %s' % S(value))
				c.send(VALUE, boxid, value, proxy=ALWAYS)
		elif op == GLOBALS:
			value = globals()
			c.send(VALUE, boxid, value, proxy=ALWAYS)
		else:
			threadstate.to_stopped(me)
			raise ProtocolError, "recv op=%s" % op

		# prinfo('TWI END %s, %d still busy' % (S(boxid), threadstate.n_busy()))





def thread_within_listen_wrapper(mbox, threadstate, conn):
	try:
		thread_within_listen(mbox, threadstate, conn)
	except GM.EOF:
		pass
	except:
		log('Exception in thread_within_listen:', traceback.format_exc())
		sys.exit(2)



class io_grabber(object):
	"""This buffers text, transmitting it in units of lines.
	That's important at the other end if there are multiple
	connections, because fancyprint adds a prefix at the
	beginning of each write, assuming it is the beginning
	of each line."""

	def __init__(self, name):
		log('iograbber.__init__')
		self.name = name
		self.c = None
		self.tmp = []
		self.tinl = []
		self.lock = T.Lock(verbose=DEBUG>1, name='io_grabber')

	def connect(self, c):
		log('iograbber.connect')
		self.c = c
		self.lock.acquire()
		self._dump()
		self.lock.release()


	def write(self, s):
		log('write:', s)
		return self.writelines( (s,) )


	def writelines(self, x):
		log('writelines:', x)
		if isinstance(x, str):
			x = (x,)
		self.lock.acquire()
		for s in x:
			if s:
				try:
					i = s.rindex('\n')
					self.tmp.append( s[:i+1] )
					self.tinl.append( len(self.tmp) )
					if i+1 < len(s):
						self.tmp.append( s[i+1:] )
				except ValueError:
					self.tmp.append(s)
		if self.c is not None:
			self._dump()
		self.lock.release()


	def _dump(self):
		log('_dump:')
		j = 0
		for i in self.tinl:
			try:
				jis = ''.join(self.tmp[j:i])
				self.c.send(self.name, None, (jis,), proxy=NEVER)
				j = i
			except IOError:
				pass
		self.tinl = []


	def flush(self):
		log('_flush:')
		if self.c is not None and self.tmp:
			try:
				self.c.send(self.name, None, tuple(self.tmp), proxy=NEVER)
			except IOError:
				pass
			self.tmp = []
			self.tinl = []


	def close(self):
		self.flush()





class io_labeler(object):
	def __init__(self, name):
		self.name = name
		self.rstderr = sys.stderr

	def writelines(self, x):
		self.rstderr.writelines( ('E', self.name, ':',) )
		self.rstderr.writelines(x)
		self.rstderr.flush()

	def write(self, s):
		self.rstderr.write('E')
		self.rstderr.write(self.name)
		self.rstderr.write(':')
		self.rstderr.write(s)
		self.rstderr.flush()

	def flush(self):
		pass


def import_mod(name, path):
	"""Import a module from the specified path, or, failing that,
	look in sys.path then for a builtin.   
	If path is None, only look in sys.path and builtins.
	If path is an array containing None, replace the None with sys.path.
	"""
	# print '#LM name=', name, 'path=', path
	# print 'MODULES=', sys.modules.keys()
	if path is None:
		pth = None
	else:
		pth = []
		for d in path:
			if d is None:
				pth.extend(sys.path)
			else:
				pth.append( d )
	fd = None
	imp.acquire_lock()
	try:
		fd, pn, desc = imp.find_module(name, pth)
	except ImportError:
		try:
			fd, pn, desc = imp.find_module(name, None)
		except:
			imp.release_lock()
			raise

	if name in sys.modules:
		if hasattr(sys.modules[name], '__file__'):
			if os.path.dirname(sys.modules[name].__file__) == os.path.dirname(pn):
				imp.release_lock()
				return sys.modules[name]
		else:
			imp.release_lock()
			return sys.modules[name]

	try:
		pymod = imp.load_module(name, fd, pn, desc)
	finally:
		if fd:
			fd.close()
		imp.release_lock()
	return pymod

assert import_mod('sys', None).path


def s_listen(c):	# Run in separate thread
	"""This reads answers and puts them in the correct mailboxes."""
	prinfo('Starting s_listen')
	# TWI_thread_name = 'Twi-thread%d' % id(c)	# Must be unique!
	MAXIDLE = 6
	twibox = GM.mailbox()
	threadstate = C_threadstate()

	c.send(INITIALIZATION, None,
			(__version__,
				(Oid(object), Oid(type))
			)
		)
	c.reason = None
	while True:
		prinfo('s_listen LOOP')
		try:
			op, boxid, interior = cPickle.load(c.rfd)
		except cPickle.UnpicklingError:
			prinfo('Unpickling Error - bad instruction from other side')
			c.reason = 'ERROR: Unpickling'
			break
		except EOFError:
			prinfo('EOF in s_listen')
			c.reason = 'EOF'
			break
		prinfo('s_listen op=%s, boxid=%s' % (S(op), S(boxid)))

		if op in (ABORT, QUIT_2):
			c.reason = op
			break

		n_idle = threadstate.n_idle()
		if n_idle == 0:
			prinfo('s_listen: add one more thread')
			t = T.start_new_thread(thread_within_listen_wrapper,
						(twibox, threadstate, c)
					)
			GM.Lockstate.setname(me=t, name='twl%d' % t)
			# t = T.Thread( target=thread_within_listen_wrapper,
						# args=(twibox, threadstate, c),
						# name=TWI_thread_name
							# )
			threadstate.to_starting(t)
			# t.setDaemon(True) # KLUGE
			# t.start()
		elif n_idle > MAXIDLE:
			prinfo('s_listen: shutting down one thread')
			# twibox.put(None)
			threadstate.anticipate_stop()
			twibox.putclose(for_get=GM.EOF(), ngx=1)
		# prinfo('s_listen: ready to twibox.put: idle=%d' % threadstate.n_idle())
		twibox.put( ( op, boxid, interior) )
		threadstate.reap_stopped_threads()
		# prinfo('s_listen<')
	prinfo('s_listen shutdown')
	twibox.putclose()
	del twibox
	prinfo('waiting for twi_threads')
	threadstate.wait_until_all_done()
	prinfo('twi_threads joined')
	c.close_wfd()
	c.lowrp.clear()
	c.rowlp.disconnect()
	c.class_of_inst.disconnect()
	c.lo_clo.disconnect()
	prinfo('S_listen DONE - putting reason=%s' % c.reason)



def s_listen_wrapper(c, s_listen_done):	# Run in separate thread
	try:
		s_listen(c)
		s_listen_done.put('OK')
	except:
		log('Exception in s_listen', traceback.format_exc())
		s_listen_done.put('exception')
		sys.exit(2)


class dropsite(object):
	def __init__(self):
		self.lock = T.Lock(verbose=DEBUG>1, name='dropsite')
		self.boxes = {}

	def make(self, dbg=''):
		self.lock.acquire()
		wbox = GM.waitbox()
		boxid = '%s#%d' % (dbg, Id(wbox))
		self.boxes[boxid] = wbox
		self.lock.release()
		return (wbox, boxid)


	def find(self, boxid, dbg=None):
		"""For internal use only."""
		assert dbg is not None
		# prinfo('dropsite: pre get %s %s' % (boxid, dbg))
		self.lock.acquire()
		tmp = self.boxes.pop(boxid)
		self.lock.release()
		# prinfo('dropsite: gotten %s %s' % (boxid, dbg))
		return tmp

	def count(self):
		self.lock.acquire()
		tmp = len(self.boxes)
		self.lock.release()
		return tmp




class fancyprint(object):
	def __init__(self, fd, prefix='', lock=None):
		if lock is None:
			self.lock = T.Lock(verbose=DEBUG>1, name='fancyprint')
		else:
			self.lock = lock
		self.fd = fd
		self.prefix = prefix

	def fprint(self, s):
		assert isinstance(s, tuple)
		self.lock.acquire()
		if self.fd is not None:
			for ls in s:
				self.fd.writelines( (self.prefix, ls) )
			self.fd.flush()
		self.lock.release()


def get_raise(mbox, conn):
	# prinfo('get_raise waiting for get')
	op, rv = mbox.get()
	if op == EXCEPTION:
		ex_name, ex_mod, ex_clid, ex_args, trb = rv
		log('get_raise: trb: %s.%s' % (ex_mod, ex_name), *tuple(trb))
		tmp = construct_enhanced_exception(conn, ex_name, ex_mod,
							ex_clid, ex_args, trb)
		log('get_raise: c_e_e done', str(tmp), tmp.args, tmp.remote_traceback)
		raise tmp
	elif op == VALUE:
		pass
	else:
		log('get_raise: unknown op: %s' % str(op))
		raise RuntimeError, 'bad op in get_raise'
	return rv


class pickler(object):
	def __init__(self, conn):
		assert isinstance(conn, Connection)
		self.conn = conn
		self.check = hash(self)

	def dump(self, obj, proxy=NEVER):
		buf = StringIO.StringIO()
		p = cPickle.Pickler(buf, cPickle.HIGHEST_PROTOCOL)
		if proxy == ALWAYS:
			p.persistent_id = self.oid
		elif proxy == MAYBE:
			p.persistent_id = self.persistent_id
		p.dump(obj)
		return buf.getvalue()


	def persistent_id(self, obj):
		assert hash(self) == self.check
		if isinstance(obj, TRANSPORTABLE_TYPES):
			rv = None
		elif Id(obj) in TRANSPORTABLE_TYPES_O:
			rv = None
		else:
			rv = self.oid(obj)
		prinfo('persistent_id (pid) for [%s]%s = %s' % (S(obj), S(type(obj)), S(rv)))
		return rv


	def oid(self, obj):
		tmp = self.conn.lowrp.putid(obj)
		try:
			clobj = obj.__class__
		except AttributeError:
			clobj = None
		if clobj is not None:
			cloid = self.conn.lo_clo.putid(clobj)
		else:
			cloid = ''
		rv = '%s~%s' % (str(tmp), str(cloid))
		prinfo('persistent_id (oid) for [%s]%s = %s' % (S(obj), S(type(obj)), S(rv)))
		return rv


	def load(self, s):
		prinfo('unpickle.load')
		up = cPickle.Unpickler(StringIO.StringIO(s))
		up.persistent_load = self.persistent_load
		tmp = up.load()
		prinfo('unpickled')
		return tmp
	

	def persistent_load(self, oidstr):
		prinfo('beginning persistent_load on %s' % S(oidstr))
		assert hash(self) == self.check
		oid, cloid = oidstr.split('~')
		return proxy_factory(diO(oid), diO(cloid), self.conn)




class Connection(object):
	"""You need to call close()."""
	sys = sys
	try:
		import gc as _gc
	except ImportError:
		_gc = None

	def __init__(self, wfd, rfd, stderr, stdout):
		self.stderr = stderr	# used in thread_within_listen
		self.stdout = stdout	# used in thread_within_listen
		self.pickler = pickler(self)

		# local objects with remote proxies:
		self.lowrp = object_store(dict)
		# remote objects with local proxies:
		self.rowlp = object_store(weakref.WeakValueDictionary)
		self.class_of_inst = object_store(weakref.WeakValueDictionary)
		self.lo_clo = object_store(weakref.WeakValueDictionary)

		self.qmlock = T.Lock(verbose=DEBUG>1, name='Connection.qmlock')
		self.remote_special_objs = None	# will be replaced
		self.box = dropsite()
		self.wfd = wfd
		self.rfd = rfd
		self.slock = T.Lock(verbose=DEBUG>1, name='Connection.slock')
		self.closed = False
		self.reason = None
		self.s_listen_ready = GM.waitbox()
		self.s_listen_done = GM.waitbox()
		# self.s_listen = T.Thread(target=s_listen_wrapper,
						# args= (self,),
						# name='s_listen_thread'
						# )

		t = T.start_new_thread(s_listen_wrapper, (self, self.s_listen_done))
		GM.Lockstate.setname(me=t, name='s_listen_wrapper')

		# self.s_listen.setDaemon(True)	# KLUGE
		# Start the listener:
		# self.s_listen.start()
		# ...and wait until the listener is running.
		self.s_listen_ready.get()


	def send(self, op, boxid, args, proxy=MAYBE):
		# prinfo('c.send( op=%s id=%s ...)' % (op, boxid))
		interior = self.pickler.dump( args, proxy=proxy )
		tmp = cPickle.dumps( (op, boxid, interior) )
		# prinfo('Pickle done in send')
		self.slock.acquire()
		try:
			self.wfd.write(tmp)
			self.wfd.flush()
		finally:
			self.slock.release()
		# prinfo('send done (%s %s)' % (op, boxid))

	def close_wfd(self):
		"""Called as part of the QUIT sequence."""
		self.slock.acquire()
		try:
			self.wfd.close()
		finally:
			self.slock.release()
		self.wfd = None

	def remote_remove_from_store(self, oid):
		try:
			self.send(RM_STORE, None, oid)
		except IOError:
			# These IO errors happen in the shutdown process
			# when the file descriptors get closed before
			# all the proxies are deleted.  They are of no
			# importance, as the cached objects on the other
			# side will be deleted anyway.
			prinfo('rm from store - IO error')
			pass

	def remote_test(self, i):
		mbox, boxid = self.box.make('remote_test%d' % i)
		self.send(R_TEST, boxid, i)
		return get_raise(mbox, self)

	def remote_setattr(self, objid, k, v):
		# prinfo('remote_setattr')
		mbox, boxid = self.box.make('remote_setattr:%s' % k)
		try:
			self.send(SETA, boxid, (objid, str(k), v))
		except cPickle.PicklingError:
			self.send(SETAP, boxid, (objid, str(k), self.lowrp.putid(v)))
		get_raise(mbox, self)

	def remote_delattr(self, objid, k):
		# prinfo('remote_delattr')
		mbox, boxid = self.box.make('remote_delattr:%s' % k)
		self.send(DELA, boxid, (objid, str(k)))
		get_raise(mbox, self)

	def remote_getattr(self, objid, k):
		prinfo('remote_getattr: %s . %s' % (S(objid), S(k)))
		mbox, boxid = self.box.make('remote_getattr:%s' % S(k))
		self.send(GETA, boxid, (objid, str(k)))
		return get_raise(mbox, self)


	def remote_call(self, objid, a, kw):
		prinfo('remote_call')
		mbox, boxid = self.box.make('remote_call:%s(...)' % objid)
		kwtuple = tuple(kw.items())
		prinfo('kwtuple (RC)= %s' % S(kwtuple))
		self.send(RCALL, boxid, (objid, a, kwtuple ))
		return get_raise(mbox, self)

	def remote_call_method(self, objid, aname, a, kw):
		prinfo('remote_call_method: %s . %s(...)' % (objid, aname))
		mbox, boxid = self.box.make('remote_call_method:%s.%s(...)' % (objid, aname))
		kwtuple = tuple(kw.items())
		prinfo('kwtuple (RCM)= %s' % S(kwtuple))
		self.send(RMCALL, boxid, (objid, str(aname), a, kwtuple ))
		prinfo('RCM sent')
		return get_raise(mbox, self)
		
	def remote_import_mod(self, name, path):
		"""Import a module from the specified path, or, failing that,
		look in sys.path then for a builtin.   
		If path is None, only look in the remote machine's sys.path and builtins.
		If path is an array containing None, replace the None with the remote machine's sys.path.
		"""
		# prinfo('remote_import_mod %s %s' % (name, path))
		mbox, boxid = self.box.make('remote_import_mod:%s' % name)
		if path is not None:
			path = tuple(path)
		self.send(IMPORT_MOD, boxid, (str(name), path))
		# prinfo('Waiting for response (rim) on %s' % boxid)
		return get_raise(mbox, self)

	def remote_globals(self):
		mbox, boxid = self.box.make('remote_globals')
		self.send(GLOBALS, boxid, None)
		# prinfo('Waiting for response (rg) on %s' % boxid)
		return get_raise(mbox, self)

	def remote_get_classinfo(self, cloid):
		"""Gets information about obj.__class__."""
		mbox, boxid = self.box.make('remote_get_classinfo:%s' % cloid)
		self.send(GET_CLASSINFO, boxid, cloid)
		prinfo('Waiting for response (rci) on %s' % S(boxid))
		return get_raise(mbox, self)

	def serve(self):
		prinfo('Connection.serve()')
		self.s_listen_done.get()
		# self.s_listen.join()
		prinfo('Connection.serve() joined')
		# The other side normally initiates the termination with a
		# QUIT_1 or an ABORT.
		# We initiate it only on error.
		if self.reason is not None and self.reason.startswith('ERROR:'):
			self.send(ABORT, None, self.reason)
		self.terminate()

	def close(self):
		self.closed = True
		if self._gc is not None:
			self._gc.collect()
		time.sleep(0.1)
		self.send(QUIT_1, None, None)
		prinfo('Connection.close ready for join')
		# self.s_listen.join()
		self.s_listen_done.get()
		prinfo('Connection.close() joined.')
		self.terminate()

	def terminate(self):
		self.closed = True
		for t in T.enumerate():
			prinfo('Thread name=%s' % t.getName())
		del self.stderr
		del self.stdout
		del self.pickler
		del self.lowrp
		del self.rowlp
		del self.remote_special_objs
		del self.box
		del self.wfd
		del self.rfd
		if self._gc is not None:
			self._gc.collect()



	def __del__(self):
		if not self.closed:
			self.close()







def transform_callable(onm):
	def tmp(self, *a, **kw):
		# prinfo('Transformed %s function' % onm)
		oid = object.__getattribute__(self, 'oid')
		conn = object.__getattribute__(self, 'conn')
		assert isinstance(conn, Connection)
		return conn.remote_call_method(oid, onm, a, kw)
	return tmp


def create_property(onm):
	def fget(self):
		oid = object.__getattribute__(self, 'oid')
		conn = object.__getattribute__(self, 'conn')
		return conn.remote_getattr(oid, onm)
	def fset(self, v):
		oid = object.__getattribute__(self, 'oid')
		conn = object.__getattribute__(self, 'conn')
		return conn.remote_setattr(oid, onm, v)
	def fdel(self):
		oid = object.__getattribute__(self, 'oid')
		conn = object.__getattribute__(self, 'conn')
		return conn.remote_delattr(oid, onm)
	return property(fget, fset, fdel)



class proxy_to_a_class(proxy_object):
	def __init__(self, oid, conn):
		object.__setattr__(self, 'oid', oid)
		object.__setattr__(self, 'conn', conn)

	def __setattr__(self, k, v):
		oid = object.__getattribute__(self, 'oid')
		conn = object.__getattribute__(self, 'conn')
		conn.remote_setattr(oid, k, v)

	def __delattr__(self, k):
		oid = object.__getattribute__(self, 'oid')
		conn = object.__getattribute__(self, 'conn')
		conn.remote_delattr(oid, k)

	def __getattribute__(self, k):
		if k == '__class__':
			return object.__getattribute__(self, k)
		oid = object.__getattribute__(self, 'oid')
		conn = object.__getattribute__(self, 'conn')
		return conn.remote_getattr(oid, k)


	def __call__(self, *a, **kw):
		oid = object.__getattribute__(self, 'oid')
		conn = object.__getattribute__(self, 'conn')
		return conn.remote_call(oid, a, kw)


def class_under_proxied_instance(ocid, conn):
	assert isinstance(conn, Connection)
	NO_REM_GET = ('__class__',)
	nm, bsidlist, ocd_items = conn.remote_get_classinfo(ocid)
	b = [ cached_class_u_p_i(bsid, conn) for bsid in bsidlist ]
	prinfo('Proxyclass: bases done: %s' % S(b))
	d = {}
	for (onm, is_callable) in ocd_items:
		prinfo('d_item %s = %s' % (S(onm), S(is_callable)))
		if is_callable:
			d[onm] = transform_callable(onm)
		else:
			d[onm] = create_property(onm)
	prinfo('Proxyclass: transforms/properties done')

	def __init__(self, oid, conn):
		# proxyclass.__init__. Oid here is only the id of the object itself,
		# not the (object-id,class-id) tuple produced by Oid().
		prinfo('proxyclass __init__ %s' % S(oid))
		assert isinstance(conn, Connection)
		object.__setattr__(self, 'oid', oid)
		object.__setattr__(self, 'conn', conn)

	def __setattr__(self, k, v):
		oid = object.__getattribute__(self, 'oid')
		conn = object.__getattribute__(self, 'conn')
		conn.remote_setattr(oid, k, v)

	def __delattr__(self, k):
		oid = object.__getattribute__(self, 'oid')
		conn = object.__getattribute__(self, 'conn')
		conn.remote_delattr(oid, k)

	def __getattribute__(self, k):
		if k in NO_REM_GET:
			return object.__getattribute__(self, k)
		oid = object.__getattribute__(self, 'oid')
		conn = object.__getattribute__(self, 'conn')
		return conn.remote_getattr(oid, k)

	def __del__(self):
		oid = object.__getattribute__(self, 'oid')
		conn = object.__getattribute__(self, 'conn')
		if conn is not None:
			# conn is None after disconnection.
			# It is of no consequence, because the cached object
			# is being deleted on the other side anyway.
			conn.remote_remove_from_store( oid )

	d['__init__'] = __init__
	d['__setattr__'] = __setattr__
	d['__delattr__'] = __delattr__
	d['__getattribute__'] = __getattribute__
	d['__del__'] = __del__
	return type(nm, tuple(b), d)



def enh_exc_class_maker(ex_nm, ex_mod):
	nm = ex_nm + '_proxy_enhanced'
	log('Making class: %s.%s' % (ex_mod, nm))
	basemod = import_mod(ex_mod, sys.path)
	b = ( getattr(basemod, ex_nm), )

	def __init__(self, args, trb):
		log('initializing base %s' % b[0].__name__, *args)
		b[0].__init__(self, *args)
		self.remote_traceback = trb
		log('initialized base %s' % b[0].__name__)

	d = { '__init__': __init__ }
	if getattr(b[0], '__class__', None) is type(b[0]):	# New style class
		tmp = type(nm, b, d)
	else:
		tmp = (types.ClassType)(nm, b, d)
	log('Constructed enh_exc_class', type(tmp))
	return tmp


def construct_enhanced_exception(conn, ex_nm, ex_mod, ex_clid, ex_args, trb):
	assert isinstance(conn, Connection)
	try:
		enh_cl = conn.class_of_inst.get(ex_clid)
	except KeyError:
		enh_cl = enh_exc_class_maker(ex_nm, ex_mod)
		conn.class_of_inst.put(ex_clid, enh_cl)
	return enh_cl(ex_args, trb)


def test_ee():
	cl = enh_exc_class_maker('KeyError', 'exceptions')
	q = cl(('test',), ['r',])
	try:
		raise q
	except KeyError, x:
		assert x.args[0]=='test' and x.remote_traceback[0]=='r'
	else:
		raise AssertionError, "Failure of enh_exc_class_maker"


test_ee()


def cached_class_u_p_i(ocid, conn):
	assert isinstance(conn, Connection)
	try:
		tmp = conn.remote_special_objs[ocid]
		prinfo('From Cache: %s' % S(tmp))
		return tmp
	except KeyError:
		pass
	try:
		return conn.class_of_inst.get(ocid)
	except KeyError:
		tmp = class_under_proxied_instance(ocid, conn)
		conn.class_of_inst.put(ocid, tmp)
	return tmp


def proxy_factory(oid, cloid, conn):
	assert isinstance(conn, Connection)
	prinfo('Proxy factory %s' % S(oid))
	try:
		tmp = conn.rowlp.get(oid)
		prinfo('Proxy object found in rowlp cache: %s' % S(tmp))
		return tmp
	except KeyError:
		pass
	if conn.remote_special_objs.get(cloid, None) is type:
		# We construct a class object, not an instance.
		# Don't use cached_proxyclass, because that may find an
		# proxy for this class that was created to underly an
		# proxy for instance of the class,
		# rather than a direct proxy for the class.
		# The two things are not the same!
		return proxy_to_a_class(oid, conn)

	prinfo('proxy_factory step 2')
	prinfo('Proxy Factory cloid=%s' % S(cloid))
	assert cloid is not None
	tmp = cached_class_u_p_i(cloid, conn)
	prinfo('proxyclass made: %s %s' % (S(tmp), S(tmp.__class__.__name__)))
	prinfo('dir(tmp)= %s' % S(dir(tmp)))
	prinfo('tmp.__class__= %s' % S(tmp.__class__))
	prinfo('tmp.__dict__= %s' %
			' '.join('%s=%s' % (S(k),S(v)) for (k,v) in tmp.__dict__.items())
			)
	prinfo('tmp.__class__ is callable? %s' % S(callable(tmp.__class__)))
	ins = tmp(oid, conn)
	prinfo('proxy instance made: %s' % S(tmp))
	conn.rowlp.put(oid, ins)
	return ins




class remote_exec(object):
	try:
		import gc as _gc
	except ImportError:
		_gc = None

	def __init__(self, path, arglist, cname=None, printlock=None):
		if cname is not None:
			stderr = fancyprint(sys.stderr, 'E:%s:' % cname, lock=printlock)
			stdout = fancyprint(sys.stdout, 'O:%s:' % cname, lock=printlock)
		else:
			stderr = fancyprint(sys.stderr, lock=printlock)
			stdout = fancyprint(sys.stdout, lock=printlock)
		self.wfd, self.rfd = g_pipe.popen2(path, arglist)
		self.c = Connection(self.wfd, self.rfd, 
					stderr=stderr, stdout=stdout)

	def import_mod(self, name, path=None):
		"""Import a module from the specified path, or, failing that,
		look in sys.path then for a builtin.   
		If path is None, only look in the remote machine's sys.path and builtins.
		If path is an array containing None, replace the None with the remote machine's sys.path.
		The default is None.
		"""
		return self.c.remote_import_mod(name, path)

	def test(self, i):
		return self.c.remote_test(i)

	def globals(self):
		return self.c.remote_globals()

	def close(self):
		if self.c is None:
			return
		prinfo( "Closing connection for remote exec" )
		self.c.close()
		prinfo( "Closing remote exec - c closed" )
		self.wfd.close()
		prinfo( "Closing remote exec - wfd closed" )
		self.rfd.close()
		prinfo( "Closing remote exec - rfd closed" )
		self.c = None
		if self._gc is not None:
			self._gc.collect()


	def __del__(self):
		if self.c is not None:
			self.close()


def test_minimal():
	prinfo( '%TEST_MINIMAL %%%%%%%%%%%%%%%%%START')
	re = remote_exec(sys.executable, ['python', '-m', 'g_proxy'] )
	prinfo( '%%%%%%%%%%%%%%%%%%MID')
	re.close()
	prinfo( '%%%%%%%%%%%%%%%%%%END')


def test_simpleclass():
	prinfo( '%TEST_SIMPLECLASS ++++++++++++++++++++++++++' )
	re = remote_exec(sys.executable, ['python', '-m', 'g_proxy'] )
	for (itest, t) in enumerate(TEST):
		if isinstance(t, test1):
			break
	pt1 = re.test(itest)
	prinfo( 'pt1.dict= %s' % repr( pt1.__dict__ ))
	prinfo( 'pt1.cl= %s' % repr( pt1.__class__))
	prinfo( 'pt1.cl.d = %s' % repr( pt1.__class__.__dict__))
	prinfo( 'pt1.ga = %s' % repr( pt1.__class__.__dict__['__getattribute__']))
	prinfo( 'pt1.whatami=%s' % pt1.whatami)
	prinfo( '+++++++++++++++---------------')
	prinfo( 'pt1.w()= %s' %  pt1.w() )
	prinfo( '+++++++++++++++---------------********')
	prinfo( 'pt1.x= %s' % pt1.x )
	assert pt1.x == TEST[itest].x
	prinfo('-----------2')
	assert TEST[itest] == pt1
	assert pt1 == TEST[itest]
	assert pt1.whatami == TEST[itest].whatami
	assert pt1.w() == TEST[itest].w()
	assert isinstance(pt1, proxy_object)
	assert pt1.__class__.__name__ == TEST[itest].__class__.__name__
	re.close()
	

def test_minimalclass():
	prinfo( '%TEST_MINIMALCLASS +++++++++++++++++++++++++++' )
	re = remote_exec(sys.executable, ['python', '-m', 'g_proxy'] )
	for (i, t) in enumerate(TEST):
		if isinstance(t, test0):
			pt1 = re.test(i)
			assert isinstance(pt1, proxy_object)
			prinfo( 'names: %s vs. %s' % (pt1.__class__.__name__, t.__class__.__name__))
			assert pt1.__class__.__name__ == t.__class__.__name__
		if isinstance(t, test0classic):
			pt1 = re.test(i)
			assert pt1.__dict__ == t.__dict__
			assert pt1.__class__.__name__ == t.__class__.__name__
	re.close()
	

def test_EQ():
	prinfo( '%TEST_EQ QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ')
	re = remote_exec(sys.executable, ['python', '-m', 'g_proxy'] )
	for (i, t) in enumerate(TEST):
		prinfo( 'TEST EQ %d' % i )
		if isinstance(t, test0):
			break
		tmp = re.test(i)
		assert tmp == t
		prinfo('Passed test %d' % i)
	re.close()

def test_sys():
	prinfo( '%TEST_SYS QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ')
	re = remote_exec(sys.executable, ['python', '-m', 'g_proxy'] )
	tmp = re.import_mod('sys')
	assert isinstance(sys.path, types.ListType)
	assert len(tmp.path) > 0
	assert len(sys.path) > 0
	ltp = len(tmp.path)
	tmp.path.append('.')
	tmp.path.insert(0, '.')
	assert len(tmp.path) == ltp+2
	assert tmp.path[0] == tmp.path[-1]
	assert 'sys' in tmp.modules
	# assert 'sys' in sys.modules
	# assert 'threading' in tmp.modules
	# assert 'threading' in sys.modules
	print tmp
	print 'R:', tmp.argv, sys.argv
	assert tmp.argv[-1].split('/')[-1] == 'g_proxy.py', 'tmp.argv= %s' % str(tmp.argv)
	assert tmp.argv != sys.argv
	re.close()

def test_builtins():
	prinfo( '%TEST_BUILTINS QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ')
	re = remote_exec(sys.executable, ['python', '-m', 'g_proxy'] )
	tmp = re.import_mod('__builtin__')
	print tmp, tmp.__name__
	q1 = getattr(tmp, 'abs')
	q = tmp.abs
	assert q is q1
	print q
	assert q(3) == 3
	assert q(-3) == 3
	re.close()


def test_exit():
	re = remote_exec(sys.executable, ['python', '-m', 'g_proxy'] )
	raise SystemExit()
	re.close()
	

def test_globals():
	prinfo('%TEST_GLOBALS*******************************')
	re = remote_exec(sys.executable, ['python', '-m', 'g_proxy'] )
	tmp = re.globals()
	tmp['x'] = 3
	assert tmp['x'] == 3
	foo = test0()
	tmp['x'] = foo
	prinfo( 'Type(foo)= %s; type(tmp[x])=%s' % (str(type(foo)), str(type(tmp['x']))))
	# assert type(foo) == type(tmp['x'])
	re.close()


def test_file():
	prinfo('%TEST_FILE $$$$$$$$$$$$$$$$$$')
	re = remote_exec(sys.executable, ['python', '-m', 'g_proxy'] )
	tmp = re.import_mod('__builtin__')
	# print 'tmp=', tmp
	# print 'tmp.__class__=', tmp.__class__
	# print 'tmp.__class__.__dict__=', tmp.__class__.__dict__
	# print 'tcdk=', tmp.__class__.__dict__.keys()
	# print 'dir(tmp)=', dir(tmp)
	rfd = tmp.open('/dev/null', 'r')
	assert rfd.readline() == ''
	opn = tmp.open
	rfd = opn('/dev/null', 'r')
	assert rfd.readline() == ''
	wfd = opn('/tmp/fooproxytest', 'w')
	wfd.write('xyzzy\n')
	wfd.close()
	rfd = opn('/tmp/fooproxytest', 'r')
	assert rfd.readline() == 'xyzzy\n'
	re.close()


def test_numpy():
	prinfo('%TEST_numpy !+!+!+!+!+!+!+!+!+!+!+!+!+!+!+!+!+!+!')
	import numpy
	re = remote_exec(sys.executable, ['python', '-m', 'g_proxy'] )
	q = numpy.array((6,7,8))
	tmp = re.import_mod('numpy')
	print tmp.sum(q)
	print tmp.array((1,2,3))[1]
	re.close()

def test_exception():
	prinfo('%TEST_EXCEPTION !!!!!!!!!!!!!!!!!!!')
	re = remote_exec(sys.executable, ['python', '-m', 'g_proxy'] )
	tmp = re.import_mod('sys')
	try:
		sys.foobar
	except AttributeError, x:
		prinfo('OK: x=%s' % str(x))
	else:
		raise AssertionError, 'Wrong exception!'
	del tmp
	prinfo('-o-o-o-o-o-o-o-o-o')
	rmath = re.import_mod('math')
	try:
		rmath.sqrt(-1)
	except ValueError, x:
		assert 'domain' in str(x)
	else:
		raise AssertionError, 'Bad Exception!'
	re.close()
	prinfo('-c-c-c-c-c-c-c-c-c')


def server(name):
	sys.stdout.flush()
	sys.stderr.flush()

	tmp = sys.stdout
	if DEBUG:
		# sys.stdout = sys.stderr
		sys.stdout = open('foo.out', 'w')
		sys.stderr = open('foo.err', 'w')
		# sys.stderr = io_labeler('stderr')
	else:
		sys.stdout = io_grabber(STDOUT)
		# sys.stderr = open('foo.err', 'w')
		sys.stderr = io_grabber(STDERR)
	assert sys.stdout is not tmp
	c = Connection(sys.stdout, sys.stdin, stdout=None, stderr=None)
	if isinstance(sys.stdout, io_grabber):
		sys.stdout.connect(c)
	if isinstance(sys.stderr, io_grabber):
		sys.stderr.connect(c)

	prinfo('Connection started [main thread]')
	c.serve()
	prinfo('Connection served [main thread]')
	sys.stdout.close()


if __name__ == '__main__':
	arglist = sys.argv[1:]
	while arglist and arglist[0].startswith('-'):
		arg = arglist.pop(0)
		if arg == '--':
			break
		elif arg == '-test':
			_logfd = open('g_proxy.client', 'w')
			# test_numpy()
			# sys.exit(0)
			# test_exception()
			test_minimal()
			test_minimalclass()
			sys.setcheckinterval(1)
			test_simpleclass()
			sys.setcheckinterval(3)
			test_simpleclass()
			sys.setcheckinterval(9)
			test_simpleclass()
			sys.setcheckinterval(27)
			test_simpleclass()
			sys.setcheckinterval(81)
			test_simpleclass()
			sys.setcheckinterval(300)
			test_simpleclass()
			sys.setcheckinterval(1000)
			test_simpleclass()
			test_EQ()
			test_builtins()
			test_sys()
			# test_exit()
			test_globals()
			test_file()
			test_exception()
			sys.exit(0)
		else:
			print 'Unrecognized flag: %s' % arg
			sys.exit(1)
	if arglist:
		name = arglist[0]
		_logfd = open('g_proxy.client', 'w')
	else:
		name = 'server'
		_logfd = open('g_proxy.server', 'w')
	server(name)
else:
	_logfd = open('g_proxy.client', 'w')
