# SOCKSv5 proxy manager for network-sandbox
# Copyright 2015-2019 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

import errno
import os
import signal
import socket

import portage.data
from portage import _python_interpreter
from portage.data import portage_gid, portage_uid, userpriv_groups
from portage.process import atexit_register, spawn
from portage.util.futures.compat_coroutine import coroutine
from portage.util.futures import asyncio


class ProxyManager(object):
	"""
	A class to start and control a single running SOCKSv5 server process
	for Portage.
	"""

	def __init__(self):
		self.socket_path = None
		self._pids = []

	def start(self, settings):
		"""
		Start the SOCKSv5 server.

		@param settings: Portage settings instance (used to determine
		paths)
		@type settings: portage.config
		"""
		try:
			import asyncio  # NOQA
		except ImportError:
			raise NotImplementedError('SOCKSv5 proxy requires asyncio module')

		tmpdir = os.path.join(settings['PORTAGE_TMPDIR'], 'portage')
		ensure_dirs_kwargs = {}
		if portage.secpass >= 1:
			ensure_dirs_kwargs['gid'] = portage_gid
			ensure_dirs_kwargs['mode'] = 0o70
			ensure_dirs_kwargs['mask'] = 0
		portage.util.ensure_dirs(tmpdir, **ensure_dirs_kwargs)

		self.socket_path = os.path.join(tmpdir,
				'.portage.%d.net.sock' % os.getpid())
		server_bin = os.path.join(settings['PORTAGE_BIN_PATH'], 'socks5-server.py')
		spawn_kwargs = {}
		# The portage_uid check solves EPERM failures in Travis CI.
		if portage.data.secpass > 1 and os.geteuid() != portage_uid:
			spawn_kwargs.update(
				uid=portage_uid,
				gid=portage_gid,
				groups=userpriv_groups,
				umask=0o077)
		self._pids = spawn([_python_interpreter, server_bin, self.socket_path],
				returnpid=True, **spawn_kwargs)

	def stop(self):
		"""
		Stop the SOCKSv5 server.
		"""
		for p in self._pids:
			os.kill(p, signal.SIGINT)
			os.waitpid(p, 0)

		self.socket_path = None
		self._pids = []

	def is_running(self):
		"""
		Check whether the SOCKSv5 server is running.

		@return: True if the server is running, False otherwise
		"""
		return self.socket_path is not None


	@coroutine
	def ready(self):
		"""
		Wait for the proxy socket to become ready. This method is a coroutine.
		"""

		while True:
			try:
				wait_retval = os.waitpid(self._pids[0], os.WNOHANG)
			except OSError as e:
				if e.errno == errno.EINTR:
					continue
				raise

			if wait_retval is not None and wait_retval != (0, 0):
				raise OSError(3, 'No such process')

			try:
				s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
				s.connect(self.socket_path)
			except EnvironmentError as e:
				if e.errno != errno.ENOENT:
					raise
				yield asyncio.sleep(0.2)
			else:
				break
			finally:
				s.close()


proxy = ProxyManager()


def get_socks5_proxy(settings):
	"""
	Get UNIX socket path for a SOCKSv5 proxy. A new proxy is started if
	one isn't running yet, and an atexit event is added to stop the proxy
	on exit.

	@param settings: Portage settings instance (used to determine paths)
	@type settings: portage.config
	@return: (string) UNIX socket path
	"""

	if not proxy.is_running():
		proxy.start(settings)
		atexit_register(proxy.stop)

	return proxy.socket_path
