#!/usr/bin/env python

# USB Network Gate post-install/pre-remove script
# Copyright (C) 2013 Eltima Software

# The script must run successful even if launched repeatedly.
# It executes by package manager during installation or removal of package.

# This program must be compatible with python 2.4.3 or later, do not use:
# expr if condition else expr
# "{0}".format(expr)
# Exception as e - error on 2.4.X
# Exception, e - error on 3.X

import sys
import os
import subprocess

install_dir = "/opt/Eltima/eveusb"
daemon_cfg = "/etc/Eltima/eveusb/main.conf"

eveusbc = "eveusbc"
eveusbsh = "eveusbsh"
service = "eveusb" # SysV/Systemd/Upstart service/unit/job name
proc_owner = "eveusb" # daemon process owner and group

def check_call(cmd):
	if sys.version_info >= (2,5):
		subprocess.check_call(cmd)
	elif subprocess.call(cmd) != os.EX_OK:
		raise OSError(cmd) # subprocess.CalledProcessError

def check_output(cmd, optional, shell = False):
	if sys.version_info >= (2,7) and not optional:
		val = subprocess.check_output(cmd, shell = shell)
	else:
		val = subprocess.Popen(cmd, stdout = subprocess.PIPE, shell = shell).communicate()[0]

	val = val.strip().decode() # python3 fix
	assert optional or val
	return val

def have_program(executable):
	f = open("/dev/null", "w")
	res = subprocess.call(["which", executable], stdout = f, stderr = f)
	f.close()
	return res == os.EX_OK

def symlink(source, link_name):
	check_call(["rm", "-rf", link_name])
	os.symlink(source, link_name)

# FIXME: how to get X11 session user name
def get_x11_sesion_user():
	cmd = "ck-list-sessions"

	if have_program(cmd):
		multiline = check_output(cmd, False)
	else:
		return # :-(

	sessions = []

	for line in multiline.splitlines():
		if line.startswith("Session"):
			assert line[-1] == ':'
			sessions.append({})
		else:
			assert line[0] == '\t'
			
			key, val = line.split('=', 1)
			key = key.strip()
			val = val.strip()

			if val[0] == "'" and val[-1] == "'":
				val = val[1:-1]
			
			sessions[-1][key] = val
			
	uid = set()

	for d in filter(lambda x: x["active"] == "TRUE" and x["is-local"] == "TRUE", sessions):
		val = d["unix-user"]
		uid.add(int(val))
	
	if len(uid) == 1:
		import pwd
		try:
			usr = pwd.getpwuid(uid.pop())
			return usr.pw_name
		except KeyError:
			pass

# man getlogin:
# getlogin returns a pointer to a string containing the name of the user logged in 
# on the controlling terminal of the process, or a NULL pointer if this information 
# cannot be determined.
def getlogin():
	name = None

	try:
		name = os.getlogin()
	except:
		name = os.getenv("SUDO_USER")
		
	if not name:
		name = get_x11_sesion_user()

	return name

# See: desktop_file_install, xdg-user-dir, doc-base, install-docs.
# Ignore errors, some systems do not have X installed.
def process_menu(install):

	prog_res = "xdg-icon-resource"
	prog_menu = "xdg-desktop-menu"

	if not (have_program(prog_res) and have_program(prog_menu)):
		return

	name = "eltima-eveusb"
	pref = install_dir + "/desktop/" + name
	gui = pref + ".desktop"
	icon = pref + ".png"

	if install:
		cmd = "install"
	else:
		cmd = "uninstall"

	if not install or os.path.isfile(icon):
		if install:
			what = icon
		else:
			what = name

		subprocess.call([prog_res, cmd, "--noupdate", "--size", "256", what]) # FIXME: crashes without --noupdate on Fedora-17

	if os.path.isfile(gui):
		subprocess.call([prog_menu, cmd, gui]) # for all users, menu item disappears after logout
		user = getlogin()
		if user:
			cmd = prog_menu + ' ' + cmd + ' \"' + gui + '\"'
			subprocess.call(["su", "-c", cmd, user])

def add_user_to_group(user, group):

	f = open("/dev/null", "w")

	for args in(["gpasswd", "-a", user, group], # -a,--add
		    ["groupmod", "--add-user", user, group], # OpenSUSE
		    ["usermod", "--append", "--groups", group, user],
		    ["usermod", "--add-to-group", group, user] # OpenSUSE
		   ):
			if subprocess.call(args, stderr = f) == os.EX_OK:
				break
	else:
		msg = "Cannot add user " + user + " to group " + group
		sys.exit(msg)
		
	f.close()
	
def install_usr_grp():
	
	import grp
	try:
		group = grp.getgrnam(proc_owner)
	except KeyError:
		check_call(["groupadd", "-r", proc_owner]) # -r,--system
		group = grp.getgrnam(proc_owner)

	import pwd
	try:
		usr = pwd.getpwnam(proc_owner)
		assert usr.pw_gid == group.gr_gid
	except KeyError:
		check_call(["useradd", "-M", "-r", "--gid", proc_owner, proc_owner]) # -r,--system;-M -> don't create the user's home directory

	user = getlogin()

	if user and user not in group.gr_mem:
		add_user_to_group(user, proc_owner)

def uninstall_usr_grp():
	subprocess.call(["userdel", "-r", proc_owner]) # -r => remove home
	subprocess.call(["groupdel", proc_owner])

def process_module(install):
	udevadm = "udevadm"
	have_udevadm = have_program(udevadm)

	if have_udevadm:
		check_call([udevadm, "control", "--reload-rules"])
		check_call([udevadm, "trigger", "--subsystem-match=usb", "--property-match=DEVTYPE=usb_device"]) # change owner of /dev/bus/usb/*/*
	elif install:
		print("Warning: " + udevadm + " not found, the daemon will run with root privileges")
		regex = "s/^user=" + proc_owner + "$/group=" + proc_owner + "/g"
		check_call(["sed", "-i", regex, daemon_cfg])

	dkms = "dkms"
	if not have_program(dkms):
		return

	name = "eveusb"
	mod = ["-m", name]
	ver = ["-v", "1.0.0"]
	
	subprocess.call(["modprobe", "-r", name], stderr = open("/dev/null", "w"))

	if check_output([dkms, "status"] + mod, True):
		check_call([dkms, "remove"] + mod + ver + ["--all"])

	if install:
		for action in("add", "build", "install"):
			check_call([dkms, action] + mod + ver)


def get_setup_py_cmd(tmp_dir, install_dir):
	return 'cd "' + tmp_dir + '/eveusb-"* && ' \
		'EVEUSB_INCLUDE="' + install_dir + '/include" ' \
		'EVEUSB_LIB="' + install_dir + '/lib" ./setup.py install'
	
# Depends: python-dev, g++.
def install_swig():
	arch = install_dir + "/redistr/swig_eveusb.tar.bz2"
	if not os.path.isfile(arch): # not oemevalkit
		return
	
	import tarfile
	tar = tarfile.open(arch)
		
	import tempfile
	tmp_dir = tempfile.mkdtemp()

	try:
		tar.extractall(tmp_dir)
	except AttributeError: # old python
		for f in tar:
			tar.extract(f, tmp_dir)	
	
	tar.close()
	
	cmd = get_setup_py_cmd(tmp_dir, install_dir)
	res = subprocess.call([cmd], shell = True)

	check_call(["rm", "-rf", tmp_dir])
	
	if res == os.EX_OK:
		path = check_output(["which", eveusbsh], False)
		assert not path.endswith(".py")
		symlink(path, install_dir + "/bin/" + eveusbsh)
		symlink(eveusbsh, path + ".py") # for compatibility

def uninstall_swig():
	import distutils.sysconfig
	import glob

	path = install_dir + "/bin/" + eveusbsh
	if os.path.islink(path):
		os.unlink(path)

	prefix = distutils.sysconfig.PREFIX # /usr
	path = check_output(["which", eveusbsh], True)

	for f in(path + ".py", path):
		if os.path.isfile(f):
			print("rm " + f)
			os.unlink(f)
			prefix = os.path.dirname(f)
			if prefix.endswith("/bin"):
				prefix = prefix[ : -len("/bin") ]

	eveusb = "eveusb"
	files = (eveusb + ".py*", '_' + eveusb + ".so", eveusb + "-*.egg-info")
	files_dir = distutils.sysconfig.get_python_lib(plat_specific = True, standard_lib = False, prefix = prefix)

	for f in map(lambda f: files_dir + os.sep + f, files):
		for x in glob.glob(f):
			try:
				print("rm " + x)
				os.unlink(x)
			except: # "Exception as e" is invalid on 2.4.3
				print("Cannot remove " + x)

def ldd_by_prefix(binary, lib_prefix, optional = False):

	libs = set()
	prefix = "lib" + lib_prefix

	multiline = check_output(["ldd", binary], False)

	for line in multiline.splitlines():
		line = line.lstrip()
		if line.startswith(prefix):
			lst = line.split("=>", 1) # libQtGui.so.4 => /usr/lib/x86_64-linux-gnu/libQtGui.so.4 (0x00007ff90e61b000)
			s = lst[1].lstrip()
			assert s != "not found"

			lst = s.rsplit(None, 1) # /usr/lib/x86_64-linux-gnu/libQtGui.so.4 (0x00002b5fc1adc000)
			s = os.path.realpath(lst[0])

			libs.add(s)

	assert libs or optional
	return libs

def get_freerdp_plugin_dir():
	bin_xfreerdp = check_output(["which", "xfreerdp"], True)
	freerdp_libs = bin_xfreerdp and ldd_by_prefix(bin_xfreerdp, "freerdp-", True)
	plugins_dir  = freerdp_libs and freerdp_libs.pop().split("/libfreerdp-", 1)[0]
	return plugins_dir and os.path.join(plugins_dir, "freerdp")

def install_freerdp():
	def install_link(source, link):
		source = os.path.join(install_dir, source)
		if os.path.isfile(source):
			symlink(source, link)

	plugins = get_freerdp_plugin_dir()
	if plugins:
		install_link("bin/xfreerdp-usb", "/usr/bin/xfreerdp-usb")
		install_link("lib/libusb4rdp.so", os.path.join(plugins, "usb4rdp.so"))

def uninstall_freerdp():
	def uninstall_link(link):
		if os.path.islink(link):
			os.unlink(link)
		
	plugins = get_freerdp_plugin_dir()
	if plugins:
		uninstall_link("/usr/bin/xfreerdp-usb")
		uninstall_link(os.path.join(plugins, "usb4rdp.so"))

# Debian: "postrm purge" will uninstall it.
# See: DEBIAN/conffiles.
def install_init(install):

	if have_program("systemctl") or have_program("initctl"): # systemd or upstart
		pass # see process_daemon()

	elif have_program("update-rc.d"): # Debian, SysV

		if install:
			start_point = 80 # [1..100]
			check_call(["update-rc.d", service, "defaults",
				     str(start_point), str(100 - start_point)
				   ]) # from sysv-rc package
		else:
			check_call(["update-rc.d", service, "remove"])

	elif have_program("chkconfig"): # rpm, SysV
		if install:
			action = "--add"
		else:
			action = "--del"

		check_call(["chkconfig", action, service])

def process_daemon(start):

	if start:
		subprocess.call(["rm", "-rf", "/var/tmp/com.eltima.eveusbd"]) # dir with unix socket for daemon control
	
	if start:
		action = "start"
	else:
		action = "stop"

	if have_program("systemctl"): # systemd

		unit = service + ".service"

		if start:
			pre_action = "enable"
		else:
			pre_action = "disable"

		for cmd in(pre_action, action):
			subprocess.call(["systemctl", cmd, unit])
	
	elif have_program("initctl"): # upstart
		subprocess.call(["initctl", action, service])
	else:
		for cmd in("invoke-rc.d", "service"): # SysV
			if have_program(cmd):
				subprocess.call([cmd, service, action])
				break

def try_register():
	name = os.getenv("EVENAME")
	key = os.getenv("EVEKEY")
	
	if not(name and key):
		return

	cmd = "license " + name + " " + key

	for binary in(eveusbc, eveusbsh):
		path = install_dir + "/bin/" + binary
		if os.access(path, os.R_OK | os.X_OK):
			for i in range(3):
				if subprocess.call([path, "-c", cmd]) == os.EX_OK:
					print("registered to: " + name)
					return
				# daemon process has started but not yet initialized
				if i == 0:
					print("waiting for daemon's readiness ...")
					import time
				
				time.sleep(3)

			print("cannot register license, try later")
			break

def get_script_name():
	name = os.path.basename(sys.argv[0])
	root, ext = os.path.splitext(name)

	if ext:
		script = ext
	else:
		script = root

	if not script.startswith('.'):
		script = '.' + script
		
	return script

def modify_path():
	name = "PATH"
	val = os.environ[name]
	lst = val.split(':')
	
	for path in("/usr/local/sbin", "/usr/sbin", "/sbin"):
		if path not in lst:
			val = path + ':' + val

	if val != os.environ[name]:
		os.environ[name] = val

def make_all():
	modify_path()

	script = get_script_name()

	if script == ".postinst":
		process_menu(True)
		install_usr_grp()
		install_init(True)
		process_module(True)
		install_swig()
		install_freerdp()
		process_daemon(True)
		try_register()
	elif script == ".prerm":
		process_menu(False)
		process_daemon(False)
		install_init(False)
		process_module(False)
		uninstall_usr_grp()
		uninstall_swig()
		uninstall_freerdp()
	else:
		msg = "Unexpected script: " + sys.argv[0]
		sys.exit(msg)

if __name__ == '__main__':
	if os.geteuid():
		prog = os.path.basename(sys.argv[0])
		sys.exit("Usage: sudo " + prog)
	else:
		print(' '.join(sys.argv))
		make_all()

