#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# This program belongs to AKKODIS INGENIERIE PRODUIT SAS.
# It is considered a trade secret, and is not to be divulged or used
# by parties who have not received written authorization from the owner.
#

import sys

# currently default python version per OS are
# jammy : 3.10
# bookworm : 
# trixie : 
# Redhat 9.4 : 3.9
assert sys.version_info >= (3, 9)

import os, subprocess, glob, shutil, time, stat, yaml, ssl, base64, re, tempfile, zipfile, json, socket, configparser, re
import infinite_version

class MyParser(configparser.ConfigParser):

	def as_dict(self):
		d = dict(self._sections)
		for k in d:
			d[k] = dict(self._defaults, **d[k])
			d[k].pop('__name__', None)
		return d
			
#https://skippylovesmalorie.wordpress.com/tag/python-windows/
def user_token_is_admin(user_token):
	import ctypes
	from ctypes import wintypes
	"""
	using the win32 api, determine if the user with token user_token has administrator rights

	See MSDN entry here: http://msdn.microsoft.com/en-us/library/aa376389(VS.85).aspx
	"""
	class SID_IDENTIFIER_AUTHORITY(ctypes.Structure):
		_fields_ = [
			("byte0", ctypes.c_byte),
			("byte1", ctypes.c_byte),
			("byte2", ctypes.c_byte),
			("byte3", ctypes.c_byte),
			("byte4", ctypes.c_byte),
			("byte5", ctypes.c_byte),
		]
	nt_authority = SID_IDENTIFIER_AUTHORITY()
	nt_authority.byte5 = 5

	SECURITY_BUILTIN_DOMAIN_RID = 0x20
	DOMAIN_ALIAS_RID_ADMINS = 0x220
	administrators_group = ctypes.c_void_p()
	if ctypes.windll.advapi32.AllocateAndInitializeSid(ctypes.byref(nt_authority), 2,
		SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS,
		0, 0, 0, 0, 0, 0, ctypes.byref(administrators_group)) == 0:
		raise Exception("AllocateAndInitializeSid failed")

	is_admin = ctypes.wintypes.BOOL()
	if ctypes.windll.advapi32.CheckTokenMembership(
			user_token, administrators_group, ctypes.byref(is_admin)) == 0:
				raise Exception("CheckTokenMembership failed")
	ctypes.windll.advapi32.FreeSid(administrators_group)
	return is_admin.value != 0

# creates a symlink (on windows)
# http://stackoverflow.com/questions/6260149/os-symlink-support-in-windows
def symlink(source, link_name):
	os_symlink = getattr(os, "symlink", None)
	if callable(os_symlink):
		os_symlink(source, link_name)
	else:
		import ctypes
		csl = ctypes.windll.kernel32.CreateSymbolicLinkW
		csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
		csl.restype = ctypes.c_ubyte
		flags = 1 if os.path.isdir(source) else 0
		if csl(link_name, source, flags) == 0:
			raise ctypes.WinError()

#http://stackoverflow.com/questions/23598289/how-to-get-windows-short-file-name-in-python
def get_short_path_name(long_name):
	import ctypes
	from ctypes import wintypes
	_GetShortPathNameW = ctypes.windll.kernel32.GetShortPathNameW
	_GetShortPathNameW.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD]
	_GetShortPathNameW.restype = wintypes.DWORD
	os.makedirs(long_name,exist_ok=True)
	output_buf_size = 0
	while True:
		output_buf = ctypes.create_unicode_buffer(output_buf_size)
		needed = _GetShortPathNameW(long_name, output_buf, output_buf_size)
		if output_buf_size >= needed:
			return output_buf.value
		else:
			output_buf_size = needed
			
# tells if the user has administrative rights (required)
# http://stackoverflow.com/questions/1026431/cross-platform-way-to-check-admin-rights-in-a-python-script-under-windows
def checkIsAdmin () :
	
	if sys.platform == 'win32':
		is_admin = user_token_is_admin(0) 
	else :
		is_admin = os.getuid() == 0
		if not '/usr/sbin' in os.environ['PATH'] or not '/sbin' in os.environ['PATH']:
			raise Exception('"/usr/sbin" or "/sbin" are not available in $PATH (use "su -" instead of "su", or ALWAYS_SET_PATH yes in /etc/login.defs)')
		
	if not is_admin :
		raise Exception("This script needs administrative rights, exiting")

def cmdToString(pCmd):
	return '"' + str('" "'.join(pCmd)) + '"'
	
# Executes a program
# returns A tuple with (stdout, stderr, error code)
def shellExecExceptOnError (cmd, disableException = False):
	stdout = ''
	stderr = ''
	errorCode = 1
	lMsg = ''
	# make sure we have a list
	if type(cmd) is str :
		import shlex
		cmd = shlex.split(cmd)
	
	try :
		proc = subprocess.Popen(cmd, 0, None, None, subprocess.PIPE, subprocess.PIPE)
		out = proc.communicate()
		stdout = out[0].decode('utf-8', errors="ignore")
		stderr = out[1].decode('utf-8', errors="ignore")
		errorCode = proc.returncode
		if proc.returncode != 0:
			lMsg = 'Error executing %s, return %d %s %s' % (cmdToString(cmd),proc.returncode,stdout,stderr)
	except Exception as e:
		lMsg = 'Error executing %s %s' % (cmdToString(cmd),str(e))
		stdout = ''
		stderr = ''
		errorCode = 1
	except :
		lMsg = 'Error executing %s ' % (cmdToString(cmd))
		stdout = ''
		stderr = ''
		errorCode = 1
	if len(lMsg) > 0 and not disableException:
		raise Exception(lMsg)
	return (stdout, stderr, errorCode)

def setFileOrFolderRights(pUser, pGroup, pRights, pRecurse, pFileOrFolder):
	lOpt = []
	if pRecurse:
		lOpt.append('-R')
	if not pUser is None:
		shellExecExceptOnError(['chown',pUser] + lOpt + [pFileOrFolder])
	if not pGroup is None:
		shellExecExceptOnError(['chgrp',pGroup] + lOpt + [pFileOrFolder])
	if not pRights is None:
		shellExecExceptOnError(['chmod',pRights] + lOpt + [pFileOrFolder])

def setCredentials (installation,pDir) :
	
	if sys.platform == 'win32':
		cmd = [os.path.join(installation._Djuump_installers,'install form','scripts','GetAccessRights.exe'),pDir]
		# check if the folder rights are sufficient
		(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
		if errorCode != 0:
			# change folder rights
			cmd = ['icacls',pDir,'/grant','*S-1-5-20:(OI)(CI)F','/q']
			shellExecExceptOnError(cmd)
	else:
		setFileOrFolderRights(installation.owner_user, installation.owner_grp,'770',True,pDir)

def getFile (url):
	import urllib.request as urllib2
	
	ctx = ssl.create_default_context()
	ctx.check_hostname = False
	ctx.verify_mode = ssl.CERT_NONE
	username = ''
	password = ''
	pattern = re.compile("(.*)/([^/:]*):([^/:]*)@(.*)")
	matchObj = pattern.match(url)
	if matchObj :
		resUrl = matchObj.group(1)+'/'+matchObj.group(4)
		username = matchObj.group(2)
		password = matchObj.group(3)
	else :
		resUrl = url
	
	request = urllib2.Request(resUrl)
	if (len(username) > 0) or (len(password) > 0) :
		creds = '%s:%s' % (username, password)
		creds = creds.encode('ascii')
		base64string = base64.b64encode(creds)
		base64string = base64string.decode('ascii')
		request.add_header("Authorization", "Basic %s" % base64string)
	response = urllib2.urlopen(request, context=ctx)
	
	return response.read()

def checkLocale (installation) :
	
	if sys.platform == 'win32':
		return
	
	if not isPackageInstalled('locales',installation) :
		installPackage('locales')
	
	cmd = ['locale','-a']
	(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
	if errorCode != 0 :
		raise Exception('fail to retrieve locale %d %s %s'%(errorCode,stdout,stderr))
		
	lines = stdout.splitlines()
	if not 'en_US.utf8' in lines :
		src = '/etc/locale.gen'
		regContent = '^#{0,1}\\s*%s\\s+%s.*$'
		reg = (re.compile(regContent % ('en_US.UTF-8', 'UTF-8')), 'en_US.UTF-8 UTF-8')
		with open(src,"r",encoding='UTF-8') as handle:
			content = handle.read()
		allLines = content.split('\n')
		newContent = ''
	
		for line in allLines :
			if reg[0].match(line) :
				newContent = newContent + reg[1] + '\n'
			else :
				newContent = newContent + line + '\n'
		with open(src,"w",encoding='UTF-8') as handle:
			handle.write(newContent)
		
		cmd = ['locale-gen']
		
		shellExecExceptOnError(cmd)

def shouldUpdateFileContent(path,content):
	if not os.path.exists(path) :
		return True
	
	with open(path,"r",encoding='UTF-8') as handle:
		oldContent = handle.read()
	return oldContent != content


def checkAptVersion(installation):
	lReRes = re.match(r'.*\/(.*)\/infinite\/[0-9]+.[0-9]+\/?',installation.general.juump_apt)
	if lReRes is None:
		raise Exception('Fail to analyze apt url')
	lPackagesUrl = installation.general.juump_apt
	if lPackagesUrl[-1] != '/':
		lPackagesUrl = lPackagesUrl + '/'
	lPackagesUrl = lPackagesUrl + 'dists/' + lReRes.group(1) + '/main/binary-amd64/Packages'
	
	packagelist = getFile(lPackagesUrl)
	
	lReRes = re.match(r'.*Package: lib3djuump-infinite-cli.*?Version: ([0-9]+.[0-9]+.[0-9]+.[0-9]+).*',packagelist.decode('utf-8'),re.MULTILINE | re.DOTALL)
	if lReRes is None:
		raise Exception('Fail to retrieve version number from apt server')
	
	if not os.environ['SV_VERSION_INFINITE_FULL'].startswith(lReRes.group(1)):
		raise Exception('This install package version (%s) is different from the package version on apt (%s)' % (os.environ['SV_VERSION_INFINITE_FULL'],lReRes.group(1)))


def __configureDebianSystem(installation)	:

	install_java = hasattr(installation.proxy,'linux_oracle_java')

	main_version = re.sub(r'([0-9]+)\.([0-9]+)\..*',r'\1.\2',os.environ['SV_VERSION_INFINITE'])
	
	inFile = "/etc/apt/sources.list.d/juump-infinite-%s.list" % main_version
	content='deb %s %s main' % (installation.general.juump_apt,installation.linux_distribution)
	if shouldUpdateFileContent(inFile,content) :
		url = installation.general.juump_apt+'/3djuump-infinite.key'
		with open('/etc/apt/trusted.gpg.d/3djuump-infinite.gpg','wb') as f:
			f.write(getFile(url))
		with open(inFile,"w",encoding='UTF-8') as handle:
			handle.write(content)
		
	
	inFile = os.path.join("/etc/apt/sources.list.d/pgdg.list")
	content='deb http://apt.postgresql.org/pub/repos/apt/ %s-pgdg main' % installation.linux_distribution
	if shouldUpdateFileContent(inFile,content) :
		url = 'https://www.postgresql.org/media/keys/ACCC4CF8.asc'
		with open('/etc/apt/trusted.gpg.d/postgresql.asc','wb') as f:
			f.write(getFile(url))
		with open(inFile,"w",encoding='UTF-8') as handle:
			handle.write(content)
		
	if install_java:
		lEsVersionMajor = os.environ['SV_VERSION_ES_FULL'].split('.')[0]
		
		inFile = os.path.join("/etc/apt/sources.list.d/elastic-%s.x.list" % lEsVersionMajor)
		content='deb https://artifacts.elastic.co/packages/%s.x/apt stable main'  % lEsVersionMajor
		if shouldUpdateFileContent(inFile,content) :
			url =  'https://artifacts.elastic.co/GPG-KEY-elasticsearch'
			with open('/etc/apt/trusted.gpg.d/elasticsearch.asc','wb') as f:
				f.write(getFile(url))
			with open(inFile,"w",encoding='UTF-8') as handle:
				handle.write(content)
			
	if install_java:
		if not isPackageInstalled('apt-transport-https',installation) :
			installPackage('apt-transport-https')
			
	print('Running apt-get update')
	
	lastfrontend = None
	if 'DEBIAN_FRONTEND' in os.environ:
		lastfrontend=os.environ['DEBIAN_FRONTEND']
	os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
	
	cmd = ['apt-get','-yq','-o','Dpkg::Options::=--force-confdef','update']
	shellExecExceptOnError(cmd)
	
	if lastfrontend is None :
		del os.environ['DEBIAN_FRONTEND']
	else :
		os.environ['DEBIAN_FRONTEND']= lastfrontend
	
	installPackage('ntp')
	checkLocale(installation)

def acceptEula(installation):
	import pydoc
	
	if '--accept-eula' in sys.argv:
		return

	inFile = os.path.join(installation._Djuump_installers,'install form','config','eula.txt')
	with open(inFile,"r",encoding='UTF-8') as handle:
		eula = handle.read()

	pydoc.pager(eula)
	if not askYesNoInput('Do you accept the terms of the license ?',False):
		sys.exit(1)

def configurePostgresqlCommons() :
	
	regContent = '^#{0,1}\\s*%s\\s*=.*$'
	
	src = '/etc/postgresql-common/createcluster.conf'
	reg = (re.compile(regContent % 'create_main_cluster'), "create_main_cluster = false")
			
	with open(src,"r",encoding='UTF-8') as handle:
		content = handle.read()
	
	allLines = content.split('\n')
	newContent = ''
	
	for line in allLines :
		if reg[0].match(line) :
			newContent = newContent + reg[1] + '\n'
		else :
			newContent = newContent + line + '\n'

	with open(src,"w",encoding='UTF-8') as handle:
		handle.write(newContent)

def createUserAndGroup(installation):
	if sys.platform == 'win32':
		return
	
	if installation.isDockerInstall():
		# we only need to create one group that will be added to each container
		cmd = ['groupadd','-f',installation.owner_grp]
		shellExecExceptOnError(cmd)
		return
		
	print("Ensure %s:%s exists" % (installation.owner_user,installation.owner_grp))
	script =  os.path.join(installation._Djuump_installers,'install form','scripts','create_group.sh')
	os.chmod(script, stat.S_IRWXU)
	
	cmd = [script,installation.owner_user,installation.owner_grp]
	shellExecExceptOnError(cmd)
	
	if not installation.isDockerInstall():
		print("Ensures that %s have access to %s" % (installation.owner_user,installation.general.install_basepath))
		cmd = ['sudo','-u',installation.owner_user,'namei','-m',installation.general.install_basepath]
		shellExecExceptOnError(cmd)
			
## creates the folder hierarchy
def createFolders (installation) :
	
	print("Creating folder hierarchy")
	
	lHostNameFile = None
	if installation.isDockerInstall():
		lHostNameFile = os.path.join(installation.general.install_basepath,'host_name_check.txt')
		if os.path.isfile(lHostNameFile):
			with open(lHostNameFile,encoding='UTF-8') as f:
				lFirstList = f.readline().strip()
				if lFirstList != installation.docker_project_name:
					lErrorMsg = ( 'It seems that install_folder is already used for an other instance' 
						+'\n%s/host_name_check.txt content "%s" != "%s"' % (installation.general.install_basepath,lFirstList,getattr(installation,'docker_project_name'))
						+'\nIf you want to change domaine name, you will have to rename docker volumes and drop host_name_check.txt file'
						+"\ndocker volume create --name NEW_VOLUME_NAME && docker run --rm -it -v OLD_VOLUME_NAME:/from -v NEW_VOLUME_NAME:/to docker.io/debian:trixie-slim bash -c 'cd /from ; cp -av . /to' && docker volume rm OLD_VOLUME_NAME"
					)
					raise Exception(lErrorMsg)
	
	lFoldersToCreate = [
		installation.general.install_basepath
	]
	if installation.isDirectoryInstall():
		lFoldersToCreate.append(installation.directory_log_folder)
	if installation.isProxyInstall():
		lFoldersToCreate.append(installation.proxy_log_folder)
	for f in lFoldersToCreate:
		os.makedirs(f,exist_ok=True)
		setCredentials(installation,f)

	for folder in installation.inferred_path:
		os.makedirs(folder,exist_ok=True)
		setCredentials(installation,folder)
		
	if sys.platform != 'win32' and not installation.isDockerInstall():
		installation._ssl_folder_pg = os.path.abspath(os.path.join(getattr(installation,'ssl_folder'),'..','ssl_pg'))
		os.makedirs(installation._ssl_folder_pg,exist_ok=True)
		setCredentials(installation,installation._ssl_folder_pg)
		installation._privatekey_file_pg = os.path.join(installation._ssl_folder_pg,os.path.basename(installation.privatekey_file))
		installation._certificate_file_pg = os.path.join(installation._ssl_folder_pg,os.path.basename(installation.certificate_file))

	if not lHostNameFile is None:
		with open(lHostNameFile,'w',encoding='UTF-8') as f:
			f.write(installation.docker_project_name)

## creates the ssl key certificate pair
def createSSLFiles (installation) :
	
	if hasattr(installation.general,'provided_mTLS_root_ca'):
		shutil.copy(installation.general.provided_mTLS_root_ca,installation.mTLS_root_ca)

	if hasattr(installation.general,'provided_certificate_file') and hasattr(installation.general,'provided_privatekey_file'):
		print("Deploying provided certificate")
		shutil.copy(installation.general.provided_privatekey_file,installation.privatekey_file)
		shutil.copy(installation.general.provided_certificate_file,installation.certificate_file)
		if len(installation._privatekey_file_pg) > 0:
			shutil.copy(installation.general.provided_privatekey_file,installation._privatekey_file_pg)
			shutil.copy(installation.general.provided_certificate_file,installation._certificate_file_pg)
	else:
	
		# generate a certificate if necessary
		shouldSkip = False
		if sys.platform == 'win32' :
			shouldSkip = os.path.exists(installation.privatekey_file) and os.path.exists(installation.certificate_file)
		else :
			shouldSkip = os.path.exists(installation.privatekey_file) and os.path.exists(installation.certificate_file) and os.path.exists(installation._privatekey_file_pg) and os.path.exists(installation._certificate_file_pg)
			
		if shouldSkip :
			print("Skipping certificate creation, files already exist")
			return
		else :
			print("Creating ssl key certificate pair")
		
		
		if sys.platform == 'win32' :
			opensslFolder = os.path.join(installation._Djuump_installers,'install form','scripts')
			openssl = os.path.join(opensslFolder,'createCertificate/openssl.exe')
			opensslcnf = os.path.join(opensslFolder,'createCertificate/openssl.cnf')
			
			if not os.path.exists(opensslcnf) :
				zfile = zipfile.ZipFile(os.path.join(opensslFolder,'createCertificate.zip'))
				zfile.extractall(opensslFolder)
				if not os.path.exists(opensslcnf) :
					raise Exception('Could not find openssl conf file, exiting')
				
			os.environ["OPENSSL_CONF"] = opensslcnf.replace('/','\\')
		else :
			if not isPackageInstalled('openssl',installation):
				installPackage('openssl')
			openssl = 'openssl'
			
		
		
		if not os.path.exists(installation.privatekey_file) :
			cmd = [openssl,'genrsa','-out',installation.privatekey_file ,'2048']
			shellExecExceptOnError(cmd)

		if not os.path.exists(installation.certificate_file) :
			(handle,tmp_file ) = tempfile.mkstemp()
			os.close(handle)
			
			# CN is limited to 64 char, on some server fqdn might be longer, so use /SAN instead of /CN
			cmd = [openssl,'req','-new','-key',installation.privatekey_file,'-out',tmp_file,'-subj','/SAN=%s/O=infinite/C=FR' % socket.getfqdn()]
			(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
			if errorCode != 0 :
				os.remove(tmp_file)
				print(stdout)
				print(stderr)
				raise Exception('Error creating certificate request command was "%s"' % '" "'.join(cmd))


			cmd = [openssl,'x509','-req','-days','3650','-sha256','-in',tmp_file,'-signkey',installation.privatekey_file,'-out',installation.certificate_file]
			(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
			os.remove(tmp_file)
			if errorCode != 0 :
				raise Exception("Error creating certificate")
	
	if sys.platform != 'win32' :
		lItems = [
			installation.privatekey_file,
			installation.certificate_file]
		# docker deployment do not need to add pg ssl certificate in a dedicated folder
		if not installation.isDockerInstall():
			lItems = lItems + [
					installation._privatekey_file_pg,
					installation._privatekey_file_pg,
					installation._ssl_folder_pg]
			if os.path.exists(installation._privatekey_file_pg):
				os.remove(installation._privatekey_file_pg)
			if os.path.exists(installation._certificate_file_pg):
				os.remove(installation._certificate_file_pg)
			shutil.copy(installation.privatekey_file,installation._privatekey_file_pg)
			shutil.copy(installation.certificate_file,installation._certificate_file_pg)
		
		for item in [installation.ssl_folder] :
			setFileOrFolderRights(installation.owner_user,installation.owner_grp,'550',True,item)
		
		for item in lItems:
			setFileOrFolderRights(installation.owner_user,installation.owner_grp,'440',False,item)
			
		
			
## installs postgres silently
def installPostgres(installation) :
	
	if sys.platform == 'win32':
		if not installation._install_postgres :
			print("Skipping postgresql installation")
			return
		pgFolder = findInstalledProgram('Postgresql')
		if pgFolder != '':
			pgServiceName = getInstalledServiceName('postgresql-x64-')
			stopService(pgServiceName)
			# uninstall previous plugins if installed
			installedProgram = findInstalledProgram('3DJuump.*Postgres Plugins')
			if installedProgram != '' :
				uninstallProgram('3DJuump.*Postgres Plugins',['/SILENT'])
			uninstallProgram('Postgresql',['--mode','unattended'])
		
		# some times it remain some garbage in the destination folder which will prevent postgres to install
		if os.path.exists(installation.postgres_folder):
			shutil.rmtree(installation.postgres_folder,ignore_errors=True)
		
		print("Installing postgresql")
		cmd = [os.path.normpath(os.path.join(installation._Djuump_installers,'third-party',installation._postgresl_installer)),'--mode','unattended','--superaccount',installation.postgres.postgres_login,'--superpassword',installation.postgres.postgres_password, '--serverport',str(installation.postgres.postgres_port),'--prefix',os.path.normpath(installation.postgres_folder),
			'--datadir',os.path.normpath(installation.postgres_data_folder),'--locale','English']
		shellExecExceptOnError(cmd)
		
	else :
		if installation._install_postgres :
			installPackage('postgresql-common')
			configurePostgresqlCommons()
		# upgrade if necessary
		packages = ['postgresql-%s' % (installation._postgresl_version)]
		installPackages(packages)
		
def installApache(installation) :
	
	if sys.platform == 'win32' :
		if not installation._install_apache :
			print("Skipping apache installation")
			return
			
	
		print("Installing apache")
		
		filepattern = os.path.join(installation._Djuump_installers,'third-party','3DJuump Infinite Apache Lounge-setup') + "*"
		allFiles = glob.glob(filepattern)
		if len(allFiles) != 1:
			print("Could not find file %s" % filepattern)
			sys.exit(1)
		
		cmd = [os.path.normpath(allFiles[0]),'/DIR=%s' % (installation.apache_folder.replace('/','\\')),'/SILENT']
		shellExecExceptOnError(cmd)
		
	else :
		# upgrade if necessary
		installPackages(['apache2'])

def getRegistryKeyValue(inputKey, keyStr) :
	value = ''
	if sys.platform == 'win32':
		from winreg import OpenKey, QueryValueEx, CloseKey, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_64KEY, EnumKey, QueryValue
		try :
			key = OpenKey(HKEY_LOCAL_MACHINE, inputKey,0,KEY_READ | KEY_WOW64_64KEY)
			(value,type) = QueryValueEx(key, keyStr)
		except Exception:
			pass
	return value

def duplicateRegistryKey(inputPath, outputPath, keyName) :
	from winreg import OpenKey, QueryValueEx, CloseKey, HKEY_LOCAL_MACHINE,KEY_WRITE, KEY_READ, KEY_WOW64_64KEY, EnumKey, QueryValue, QueryInfoKey, CreateKey, SetValueEx, SetValue
	try:
		lKey = OpenKey(HKEY_LOCAL_MACHINE, inputPath,0,KEY_READ | KEY_WOW64_64KEY)
		(lValue,lType) = QueryValueEx(lKey, keyName)
	except:
		raise Exception('Fail to duplicate reg key %s' % inputPath)

	try:
		lNewKey = OpenKey(HKEY_LOCAL_MACHINE, outputPath,0,KEY_WRITE | KEY_WOW64_64KEY)
	except FileNotFoundError:
		lNewKey = CreateKey(HKEY_LOCAL_MACHINE,outputPath)
	except:
		raise Exception('Fail to open/create new key')
	SetValueEx(lNewKey,keyName,0,lType,lValue)

def askYesNoInput(str,pDefault):
	if '-f' in sys.argv:
		return pDefault
	lVal = input(str + ' ' + ('[Y/n]' if pDefault else '[y/N]')).upper()
	return pDefault if lVal == '' else lVal[0] == 'Y'

def _checkInstallationSoftwares(installation, tokens) :
	
	installedStr = ''
	installed = 0
	for token,func in tokens:
		if func(installation) : 
			if installed > 0:
				installedStr = installedStr + ', ' + token
			else :
				installedStr = token
			installed = installed + 1
	
	if installed > 0 :
		if installed > 1 :
			strInput = '%s are already installed, do you want to override their configurations ?'
		else :
			strInput = '%s is already installed, do you want to override its configuration ?'
		strInput = strInput % installedStr
		if not askYesNoInput(strInput,True):
			print('User cancelled, aborting')
			sys.exit(1)

	if sys.platform != 'win32':
		installation._run_apt_upgrade = askYesNoInput('Do you want to run an apt_upgrade ?',True)
		
def checkPreviousInstallationSoftwares(installation) :
	tokens = []
	
	tokens = tokens + [('apache',checkApacheInstalled),('postgres',checkPostgresInstalled)]
	if installation.isProxyInstall():
		tokens = tokens + [('elastic search',checkElasticInstalled)]
	if sys.platform != 'win32' :
		tokens = tokens + [('previous installation',checkPreviousInstallation)]
	
	_checkInstallationSoftwares(installation,tokens)

def isPackageInstalled(package, installation) :
	
	if installation.linux_type == 'debianbase':
		from subprocess import Popen, PIPE
		isInstalled = False
		command = ['dpkg','-l',package]
		p = Popen(command, stdin=None, stdout=PIPE, stderr=PIPE)
		output, err = p.communicate()
		rc = p.returncode
		if(rc != 0)	:
			return False
		output = output.decode('utf-8')
		content = output.splitlines()
		for inval in content :
			parsedContent = inval.split()
			if len(parsedContent) > 1:
				if (parsedContent[0] == 'ii') and (parsedContent[1] == package) :
					isInstalled = True
					break
		return isInstalled
	elif installation.linux_type == 'redhat':
		command = ['dnf','list','--installed','openssl']
		(stdout,_,errorCode) = shellExecExceptOnError(command)
		if errorCode != 0:
			raise Exception('Fail to check if openssl is installed')
		return 'openssl' in stdout
	raise Exception()

def aptcmd(packages, interactive, word, commands) :
	
	print('%s %s' % (word, ','.join(packages)))
	
	lastfrontend = None
	if 'DEBIAN_FRONTEND' in os.environ:
		lastfrontend=os.environ['DEBIAN_FRONTEND']
	
	if not interactive :
		os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
	
	cmd = ['apt-get','-yq','-o','Dpkg::Options::=--force-confdef'] + commands + packages
	if not interactive :
		shellExecExceptOnError(cmd)
		#subprocess.call(cmd)
	else :
		errorCode = shellExecReturn(cmd)
		if errorCode != 0 :
			sys.exit(1)
	
	if not interactive :
		if lastfrontend is None :
			del os.environ['DEBIAN_FRONTEND']
		else :
			os.environ['DEBIAN_FRONTEND']= lastfrontend

def installPackages(packages, interactive=False,hold=False) :
	aptcmd(packages,interactive,'Installing', ['install'])
	if hold :
		for item in packages :
			cmd = ['apt-mark','hold', item]
			shellExecExceptOnError(cmd)

def uninstallPackages(packages, interactive=False) :
	aptcmd(packages,interactive,'Uninstalling', ['remove','--purge','--allow-change-held-packages'])
			
def installPackage(package, interactive=False, hold=False) :
	installPackages([package],interactive,hold)

def uninstallPackage(package, interactive=False) :
	uninstallPackages([package],interactive)

def searchForExistingApacheCusto(installation):
	print("Checking for apache conf custo (Include directives)")

	lInfiniteConfFile = None
	lHttpdConfFile = None
	if sys.platform == 'win32':
		# for windows look for includes in both infinite conf file and base httpd.conf
		# for windows we also need to look for an older version 
		if not os.path.exists(installation.apache_folder):
			return
		
		lApacheFolders = ['Apache'+os.environ['SV_VERSION_APACHE_LOUNGE']] + sorted(os.listdir(installation.apache_folder),reverse=True)
		for f in lApacheFolders:
			lTmp1 = os.path.join(installation.apache_folder,f,'conf','httpd.conf')
			lTmp2 = os.path.join(installation.apache_folder,f,'conf','3djuump-infinite-apache-gate.conf')	
			if os.path.exists(lTmp1) and os.path.exists(lTmp2):
				lInfiniteConfFile = lTmp2
				lHttpdConfFile = lTmp1
				break
	else:
		# for linux look only for includes in infinite conf file
		lInfiniteConfFile = os.path.join('/etc/apache2/sites-available','3djuump-infinite-apache-gate.conf')

	lIncludeRe = re.compile(r'^\s*Include\s+(.*)$',re.MULTILINE)
	if not lHttpdConfFile is None:
		lOriginalConfFile = ''
		with open(lHttpdConfFile,encoding='utf-8') as f:
			lOriginalConfFile = f.read()
		for i in lIncludeRe.findall(lOriginalConfFile):
			if 'conf/extra/proxy-html.conf' in i:
				continue
			elif 'conf/3djuump-infinite-apache-gate.conf' in i:
				continue
			installation._httpd_conf_custom_includes.append(i)
	
	lHttpsIncludes = []
	if not lInfiniteConfFile is None and os.path.isfile(lInfiniteConfFile):
		lOriginalConfFile = ''
		with open(lInfiniteConfFile,encoding='utf-8') as f:
			lOriginalConfFile = f.read()
		# extract https vhost
		lHttpsVHostRe = re.compile('<VirtualHost _default_:%s>.*?<\\/VirtualHost>' % (installation.apache_https_port),re.DOTALL)
		lMatch = lHttpsVHostRe.search(lOriginalConfFile)
		if not lMatch is None:
			for i in lIncludeRe.findall(lMatch.group(0)):
				lHttpsIncludes.append('Include ' + i)
	
	if len(installation._httpd_conf_custom_includes) > 0:
		print('Found Include to restore in httpd.conf ' + str(installation._httpd_conf_custom_includes))
	if len(lHttpsIncludes) > 0:
		installation._httpd_infinite_conf_custom_includes = '\n\t'.join(lHttpsIncludes)
		print('Found Include to restore in 3djuump-infinite-apache-gate.conf ' + str(lHttpsIncludes))

def checkApacheInstalled(installation) : 
	searchForExistingApacheCusto(installation)
	
	print("Checking apache installation")
	
	if sys.platform == 'win32':
		installerPath = getRegistryKeyValue(r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{E42D260D-FDE9-459A-A20A-19B64A0917DC}_is1', 'InstallLocation')
		installation._install_apache = True
		if installerPath != '' :
			binExe = os.path.join(installerPath,'Apache'+os.environ['SV_VERSION_APACHE_LOUNGE'],'bin','httpd.exe')
			installation._install_apache = not os.path.exists(binExe)
		return not installation._install_apache
	else:
		installation._install_apache = (not isPackageInstalled('apache2',installation))
		return not installation._install_apache

def checkPreviousInstallation(installation) :
	print('checking previous installation')
	if sys.platform == 'win32':
		return False
	
	# common folders
	versions = os.environ['SV_VERSION_INFINITE'].split('.')
	curversion = versions[0]+'.'+versions[1]
	dst = os.path.normpath(os.path.join(installation.general.install_basepath,curversion))
	link = os.path.join(installation.general.install_basepath)
	if os.path.exists(link) :
		if os.path.islink(link) :
			actualDst = os.readlink(link)
			if actualDst == dst :
				#this is already the correct path
				return False
		return True
	return False


def getImagePathServiceLocation(serviceName) :
	value = ''
	if sys.platform == 'win32':
		from winreg import OpenKey, QueryValueEx, CloseKey, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_64KEY, EnumKey, QueryValue
		try :
			servicestr = r'SYSTEM\CurrentControlSet\Services' + '\\' + serviceName
			curkey = OpenKey(HKEY_LOCAL_MACHINE, servicestr,0,KEY_READ | KEY_WOW64_64KEY)
			(command,type) = QueryValueEx(curkey, 'ImagePath')
			if len(command) > 0:
				value = command
		except Exception:
			pass
	return value

def getWindowsPostgresqlDataClusterLocation(serviceName) :
	if sys.platform != 'win32':
		return ''
	lFullLocation = getImagePathServiceLocation(serviceName)
	regObj = re.compile(r'-D\s+(?:(?:"([^"]+)")|(\S+))')
	match = regObj.search(lFullLocation)
	if not match is None:
		value = match.group(1)
	if len(value) == 0:
		raise Exception('Cannot find the location of the installed postgresql cluster')
	return value
	
def getInstalledServiceLocation(serviceName) :
	value = ''
	if sys.platform == 'win32':
		command = getImagePathServiceLocation(serviceName)
		if len(command) == 0:
			return value
		if command[0] == '\"':
			regObj = re.compile(r'^"([^"]+)"')
		else :
			regObj = re.compile(r'^([^\s]+)(\s|$)')
		match = regObj.search(command)
		if not match is None:
			value = match.group(1)
			value = os.path.split(value)[0]
	return value

def getPreviousPostgresWinMainVersion() :
	if sys.platform != 'win32':
		return ''
	serviceName = getInstalledServiceName('postgresql-x64-')
	if serviceName == '' :
		raise Exception("Internal error, cannot find main postgres version")
	reg = re.compile("postgresql-x64-(.*)\\s*")
	regObj = reg.match(serviceName)
	if regObj is None :
		raise Exception("Internal error, cannot find main postgres version")
	return regObj.group(1)

def getInstalledServiceName(pattern) :
	value = ''
	if sys.platform == 'win32':
		
		from winreg import OpenKey, QueryValueEx, CloseKey, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_64KEY, EnumKey, QueryValue
		try :
			regObj = re.compile(pattern,re.IGNORECASE)
			servicestr = r'SYSTEM\CurrentControlSet\Services'
			curkey = OpenKey(HKEY_LOCAL_MACHINE, servicestr,0,KEY_READ | KEY_WOW64_64KEY)
			count = 0
			try :
				while 1:
					name = EnumKey(curkey, count)
					if not regObj.search(name) is None :
						if value != '' :
							print(value)
							print("Multiple installations found !")
							return ''
						value = name
					count = count + 1
			except WindowsError:
				pass
		except Exception:
			pass
	return value	

def getVersionVal (pString) :
	fullVersion = 0
	pString = pString.replace('_','.')
	all_vers = pString.split('.')
	multiplicator = 1000
	for i in range(0,len(all_vers)) :
		fullVersion += int(all_vers[i]) * multiplicator
		multiplicator = multiplicator / 1000
	return fullVersion

def getPreviousPostgresVersion ():
	if sys.platform == 'win32' :
		installLocation = findInstalledProgram('Postgresql')
		if installLocation == '' :
			return ''
		binExe = os.path.join(installLocation,'bin','pg_ctl.exe')
		if not os.path.exists(binExe) :
			return ''
		cmd = [binExe,'--version']
	else :
		cmd = ['pg_config','--version']
	
	(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
	if errorCode == 0:
		versionfull = stdout
		regObj = re.compile(r'[^0-9]*([0-9\.]+)(?:[^0-9\.].*)?$')
		regVal = regObj.match(versionfull)
		if not regVal is None:
			return regVal.group(1)
			# we assume 
	#print('cannot get pg version')
	return ''
	
def getInstalledProgramProperty(pattern,key) :
	lMatches = []
	if sys.platform == 'win32':
		
		from winreg import OpenKey, QueryValueEx, CloseKey, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_64KEY, EnumKey, QueryValue
		try :
			regObj = re.compile(pattern,re.IGNORECASE)
			uninstallstr = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
			curkey = OpenKey(HKEY_LOCAL_MACHINE, uninstallstr,0,KEY_READ | KEY_WOW64_64KEY)
			count = 0
			try :
				while 1:
					name = EnumKey(curkey, count)
					program = uninstallstr + '\\' + name
					try :
						programkey = OpenKey(HKEY_LOCAL_MACHINE, program,0,KEY_READ | KEY_WOW64_64KEY)
						(displayname,type) = QueryValueEx(programkey, 'DisplayName')
						if not regObj.search(displayname) is None :
							(value,type) = QueryValueEx(programkey, key)
							lMatches.append((value,displayname))
					except Exception:
						pass
					count = count + 1
			except WindowsError:
				pass
		except Exception:
			pass
	if len(lMatches) == 0:
		return ''
	if len(lMatches) > 1:
		raise Exception('Found multiple version of %s : %s' % (pattern,lMatches))
	return lMatches[0][0]

	
def findInstalledProgram(pattern) :
	installLocation = getInstalledProgramProperty(pattern,'InstallLocation')
	return installLocation.strip('"')
	
def uninstallProgram(pattern, args = []) :
	import shlex
	uninstallStr = getInstalledProgramProperty(pattern,'UninstallString')
	if uninstallStr == '':
		raise Exception("cannot find uninstall procedure for program %s"% pattern)
	# shellex is used to split args, and '\' is not handled properly => replace \ by / (fortunatly, \ will
	# only be present on the first arg
	uninstallStr = uninstallStr.replace('\\','/')
	cmd = shlex.split(uninstallStr) + args
	print("Uninstalling %s" % pattern)
	shellExecExceptOnError(cmd)

def getElasticSearchVersions () :
	if sys.platform != 'win32' :
		return (0,0)
		
	# find the command line for the elastic service, we will get the bin folder
	installLocation = getInstalledServiceLocation('elasticsearch-service-x64')
	javaversion = 0
	elasticversion = 0
	if installLocation == '' :
		# bail out, not found
		return (javaversion,elasticversion)
	
	# get elastic installation folder with JAVA_HOME
	serviceFile = os.path.join(installLocation,'service.bat')
	with open(serviceFile,"r",encoding='UTF-8') as handle:
		content = handle.read()
	reg = re.compile("set\\s+ES_JAVA_HOME\\s*=(.*)\n")
	match = reg.search(content)
	if not match is None :
		javaFolder = match.group(1).strip()
		# set the java_home in env
		os.environ['ES_JAVA_HOME'] = javaFolder
	
	# execute elasticsearch --version
	cmd = [os.path.join(installLocation,'elasticsearch.bat'),'--version']
	(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
	if errorCode != 0 :
		# something is broken in the realm
		return (javaversion,elasticversion)
	stdout = stdout.strip()
	
	# parse output to get java and elastic versions
	reg = re.compile("Version\\s*:\\s*([0-9\\.]+)[^0-9\\.]")
	match = reg.search(stdout)
	if not match is None :
		elasticversion = getVersionVal(match.group(1))
	reg = re.compile("JVM\\s*:\\s*([0-9\\._]+)\\s*$")
	match = reg.search(stdout)
	if not match is None :
		javaversion = getVersionVal(match.group(1))
	return (javaversion,elasticversion)	
	
def getElasticSearchFolders () :
	if sys.platform != 'win32' :
		return ('','')
		
	# find the command line for the elastic service, we will get the bin folder
	installLocation = getInstalledServiceLocation('elasticsearch-service-x64')
	javalocation = ''
	elasticlocation = ''
	if installLocation == '' :
		# bail out, not found
		return (javalocation,elasticlocation)
	
	elasticlocation = os.path.split(installLocation)[0]
	# get elastic installation folder with ES_JAVA_HOME
	serviceFile = os.path.join(installLocation,'service.bat')
	with open(serviceFile,"r",encoding='UTF-8') as handle:
		content = handle.read()
	reg = re.compile("set\\s+ES_JAVA_HOME\\s*=(.*)\n")
	match = reg.search(content)
	if not match is None :
		javalocation = match.group(1).strip()
	return (javalocation,elasticlocation)
	
def checkElasticInstalled(installation) :
	print("Checking elastic search installation")
	if sys.platform == 'win32':
		expectedJava = os.environ['SV_VERSION_JRE']
		expectedJavaVersion = getVersionVal(expectedJava)
		expectedElasticVersion = getVersionVal(os.environ['SV_VERSION_ES_FULL'])
		(javaversion,elasticversion) = getElasticSearchVersions()
		installation._install_elastic = ((expectedElasticVersion > elasticversion) or (expectedJavaVersion > javaversion))
	else :
		installation._install_elastic = (not isPackageInstalled('elasticsearch',installation))
		if installation.proxy.linux_oracle_java == True:
			cmd=['java','-version']
			(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
			if errorCode != 0 :
				print("Java is not installed on your system, please download the oracle java version and retry the installation")
				sys.exit(1)
			
			reg = re.compile(r"Java\(TM\)")
			match = reg.search(stdout+"\n"+stderr)
			if match is None :
				print("Java is installed on your system, but not the Oracle one, please download the oracle java version and retry the installation")
				sys.exit(1)
	return not installation._install_elastic

def checkPostgresInstalled(installation) :
	
	print("Checking postgres installation")
	
	postgresversion = getPreviousPostgresVersion()
	
	if postgresversion == '' :
		# postgres is not installed
		#print("postgresql is not installed")
		installation._install_postgres = True
	else :
		if sys.platform == 'win32' :
			installation._previous_win_postgres_folder = findInstalledProgram('Postgresql')
			installation._previous_postgres_version = getPreviousPostgresWinMainVersion()
			installation._previous_win_postgres_data_folder = getWindowsPostgresqlDataClusterLocation(getPreviousPostgresqlServiceName(installation))
			
			expectedpostgresversion = getVersionVal(os.environ['SV_VERSION_PG_FULL'])
			installation._install_postgres = (expectedpostgresversion > getVersionVal(postgresversion))
			if not installation._install_postgres:
				installation.postgres_folder =	installation._previous_win_postgres_folder
				installation.postgres_data_folder = installation._previous_win_postgres_data_folder
		else:
			old_versions = getInstalledClusterVersions(installation._postgresl_cluster_name)
			if len(old_versions) > 1 :
				raise Exception("Multiple postgresql versions found (more than one), this script cannot handle this case")
			if len(old_versions) == 1 :
				installation._previous_postgres_version = old_versions[0]
			installation._install_postgres = (not isPackageInstalled('postgresql-%s'%(installation._postgresl_version),installation))
	
		checkHasBuilds(installation)
	
	return not installation._install_postgres
	
def installPostgresPlugins(installation):
	print("Installing postgres plugins")
	
	stopService(getPostgresqlServiceName(installation));
	
	if sys.platform == 'win32':

		exe = os.path.join(installation._Djuump_installers,'dist','win',installation._plugin_installer)
		allFiles = glob.glob(exe+"*")
		if len(allFiles) != 1 :
			print("Error finding postgres plugins")
			sys.exit(1)
		exe = allFiles[0]
		
		cmd = [os.path.normpath(exe),'/DIR=%s' % (installation.infinite_postgres_plugins_folder.replace('/','\\')),'/SILENT']
		shellExecExceptOnError(cmd)
	else:
		packages = [
			'3djuump-infinite-pgplugin-3d-%s-%s' % (os.environ['SV_VERSION_PG_SMALL'],os.environ['SV_PG_3D_OP_PLUGIN_VERSION_MAJOR'].replace('.','-')),
			'3djuump-infinite-pgplugin-fdw-%s-%s' % (os.environ['SV_VERSION_PG_SMALL'],os.environ['SV_PG_FDW_PLUGIN_VERSION_MAJOR'].replace('.','-')),
			'3djuump-infinite-pgplugin-range-operators-%s-%s' % (os.environ['SV_VERSION_PG_SMALL'],os.environ['SV_PG_RANGE_OP_PLUGIN_VERSION_MAJOR'].replace('.','-')),
			]
		installPackages(packages)
	
	restartService(getPostgresqlServiceName(installation),altName='postgresql');

def configureString(installation,content) :
	inAttr = getAttributes(installation)
	lRe = re.compile(r'.*\${([A-Za-z0-9-_\\.]+)}.*',re.MULTILINE | re.DOTALL)
	while True:
		lMatchRes = lRe.match(content)
		if lMatchRes is None:
			break
		lKeyName = lMatchRes.group(1)
		if '.' in lKeyName:
			v = installation
			for e in lKeyName.split('.'):
				if not hasattr(v,e):
					raise Exception('Invalid key name %s' % (lMatchRes.group(1)))
				v = getattr(v,e)
			content = content.replace('${'+lKeyName+'}',str(v))
		elif hasattr(installation,lKeyName):
			content = content.replace('${'+lKeyName+'}',str(getattr(installation,lKeyName)))
		elif lKeyName in os.environ:
			content = content.replace('${'+lKeyName+'}',os.environ[lKeyName])
		else:
			raise Exception('Missing value %s' % (lMatchRes.group(1)))
		
	return content

def copyAndConfigure(installation,src,dst, replaceRegEx) :
	
	bakupFile = os.path.join(os.getcwd(),os.path.split(dst)[1]+".bak")
	if os.path.exists(dst) and not os.path.exists(bakupFile):
		shutil.copy(dst,bakupFile)
		
	with open(src,"r",encoding='UTF-8') as handle:
		lTplContent = handle.read()
	
	# first apply regex as they may be used to remove some content before configuration step
	content = lTplContent
	lMatchedRe = set()
	for (reg,replacement,_) in replaceRegEx :
		matchObj = reg.search(content)
		if matchObj is None:
			continue
		content = content.replace(matchObj.group(0),replacement)
		lMatchedRe.add(reg.pattern)
	
	for (reg,replacement,shouldbematched) in replaceRegEx :
		if reg.pattern in lMatchedRe:
			continue
		if shouldbematched:
			raise Exception('Fail to configure %s, fail to match %s' % (src,reg.pattern))
		content = content + replacement + '\n'
	
	# then configure
	content = configureString(installation,content)
	
	
	if os.path.exists(dst) :
		with open(dst,"r",encoding='UTF-8') as handle:
			oldContent = handle.read()
			
		if oldContent == content :
			return
	
	with open(dst,"w",encoding='UTF-8') as handle:
		handle.write(content)
	return

def isProcessRunning(name) :
	cmd =['ps','cax']
	(stdout,stderr,errorCode) = shellExecExceptOnError(cmd)
	
	alllines=stdout.splitlines()
	for line in alllines:
		if name in line:
			return True
	return False

def configureApacheImpl (installation, modules) :
	
	print("Configuring Apache")
	regs = []
	if not installation.isDirectoryInstall():
		regs.append( (re.compile('\\${apache_has_directory_define.*?<\\/IfDefine>',re.DOTALL),"",True) )
	if not installation.isProxyInstall():
		regs.append( (re.compile('\\${apache_has_proxy_define.*?<\\/IfDefine>',re.DOTALL),"",True) )
	
	
	if installation.isDockerInstall():
		lApacheConfDst = os.path.join(installation.general.install_basepath,'3djuump-infinite-apache-gate-example.conf')
		
		copyAndConfigure(installation,
				os.path.join(installation._Djuump_installers,'install form','config','3djuump-infinite-apache-gate.conf'),
				lApacheConfDst,regs)
		print('Apache gate conf example available in : ' + lApacheConfDst)
	else:
	
		lModulesToDisable = [('access_compat','modules/mod_access_compat.so')]
		if sys.platform == 'win32':
			
			src = os.path.join(installation.apache_folder,'Apache'+os.environ['SV_VERSION_APACHE_LOUNGE'],'conf','httpd.conf')
			dst = src
			
			srcTemplate = os.path.join(installation._Djuump_installers,'install form','config','httpd_win.conf')
		
			bakupFile = os.path.join(os.getcwd(),os.path.split(dst)[1]+".bak")
			if not os.path.exists(bakupFile):
				shutil.copy(dst,bakupFile)
		
			with open(srcTemplate,"r",encoding='UTF-8') as handle:
				content = handle.read()
			
			content = configureString(installation,content)
			enableregs = []
			for (moduleName,moduleSo) in modules :
				enableregs.append(re.compile('^#(LoadModule\\s+%s_module\\s+%s.*)' % (moduleName,moduleSo)))
			
			disableregs = []
			for (moduleName,moduleSo) in lModulesToDisable :
				disableregs.append(re.compile('^\\s*(LoadModule\\s+%s_module\\s+%s.*)' % (moduleName,moduleSo)))
		
			allLines = content.split('\n')
			newContent = ''
			for line in allLines :
				if line != '' :
					foundContent = False
					for reg in enableregs :
						matchObj = reg.match(line)
						if matchObj :
							foundContent = True
							newContent = newContent + matchObj.group(1) + '\n'
							break
					for reg in disableregs :
						matchObj = reg.match(line)
						if matchObj :
							foundContent = True
							newContent = newContent + '#' + matchObj.group(1) + '\n'
							break
					if not foundContent:
						newContent = newContent + line + '\n'
					
			newContent = newContent + ('Include "conf/3djuump-infinite-apache-gate.conf"\n' )
			for i in installation._httpd_conf_custom_includes:
				newContent = newContent + ('Include %s\n' % i)
			with open(dst,"w",encoding='UTF-8') as handle:
				handle.write(newContent)
			
			src = os.path.join(installation._Djuump_installers,'install form','config','3djuump-infinite-apache-gate.conf')
			dst = os.path.join(installation.apache_folder,'Apache'+os.environ['SV_VERSION_APACHE_LOUNGE'],'conf','3djuump-infinite-apache-gate.conf')
			copyAndConfigure(installation,src,dst,regs)
			
			# customize apache run user
			# apply this at configuration time to ensure that it will be applied even if Apache does not need to be reinstalled
			lApacheWin = getattr(installation,'apache_win',None)
			# by default use LocalSystem account
			lUserLocalSystemAccount = True
			if not lApacheWin is None:
				lApacheUser = getattr(lApacheWin,'user',None)
				lApachePwd = getattr(lApacheWin,'password',None)
				if (not lApacheUser is None) and (not lApachePwd is None):
					print('Use "%s" account to run Apache service' % lApacheUser)
					cmd = ['sc', 'config', 'Apachehttp3djuumpInfinite', 'obj=', lApacheUser, 'password=', lApachePwd]
					lUserLocalSystemAccount = False
					shellExecExceptOnError(cmd)
			if lUserLocalSystemAccount:
				print('Use "LocalSystem" account to run Apache service')
				cmd = ['sc', 'config', 'Apachehttp3djuumpInfinite', 'obj=', 'LocalSystem']
				shellExecExceptOnError(cmd)
			

					
			restartService("Apachehttp3djuumpInfinite",altName='apache2')
		else :
			restartApache = False
			
			if len(modules) > 0:
				cmd = ['a2enmod']
				for module in modules :
				# mod_version is statically linked in Linux, not external module
				# do not try to enable it
					if module[0] != 'version' :
						cmd.append(module[0])
				shellExecExceptOnError(cmd)
			
			# disable some modules
			cmd = ['a2dismod']+[module[0] for module in lModulesToDisable]
			shellExecExceptOnError(cmd)
			
			src = '/etc/apache2/envvars'
			
			with open(src,"r",encoding='UTF-8') as handle:
				content = handle.read()
			
			envvarsregs = []	
			envvarsregs.append((re.compile('^\\s*export\\s+APACHE_RUN_USER.*'),'export APACHE_RUN_USER=%s' % (installation.owner_user)))
			envvarsregs.append((re.compile('^\\s*export\\s+APACHE_RUN_GROUP.*'),'export APACHE_RUN_GROUP=%s' % (installation.owner_grp)))
			allLines = content.splitlines()
			
			newContent = ''
			count = 0
			for line in allLines :
				if line != '' :
					foundContent = False
					for (reg,replacement) in envvarsregs :
						matchObj = reg.match(line)
						if matchObj :
							count = count + 1
							foundContent = True
							newContent = newContent + replacement + '\n'
							break
					if not foundContent:
						newContent = newContent + line + '\n'
			
			if count != len(envvarsregs) :
				print("Error configuring apache envvars")
				sys.exit(1)
			
			if newContent != content :
				restartApache = True
				with open(src,"w",encoding='UTF-8') as handle:
					handle.write(newContent)
				
			content = ''
			content = content + 'Listen %s\n' % installation.apache_https_port
			
			src = '/etc/apache2/ports.conf'
			
			oldContent = ''
			if os.path.exists(src) :
				with open(src,"r",encoding='UTF-8') as handle:
					oldContent = handle.read()
				
			if oldContent != content :
				restartApache = True
				with open(src,"w",encoding='UTF-8') as handle:
					handle.write(content)
				
			src = os.path.join(installation._Djuump_installers,'install form','config','3djuump-infinite-apache-gate.conf')
			dst = os.path.join('/etc/apache2/sites-available','3djuump-infinite-apache-gate.conf')
			if copyAndConfigure(installation,src,dst,regs):
				restartApache = True
			
			cmd = ['a2ensite','3djuump-infinite-apache-gate.conf']
			shellExecExceptOnError(cmd)
			
			if not restartApache :
				restartApache = not isProcessRunning('apache2')
				
			if restartApache :
				restartService('apache2')

def configureApache (installation) :
	
	modules = [
				('proxy','modules/mod_proxy.so'),
				('proxy_http','modules/mod_proxy_http.so'),
				('socache_shmcb','modules/mod_socache_shmcb.so'),
				('mime','modules/mod_mime.so'),
				('setenvif','modules/mod_setenvif.so'),
				('ssl','modules/mod_ssl.so'),
				('headers','modules/mod_headers.so'),
				('authz_core','modules/mod_authz_core.so'),
				('authz_host','modules/mod_authz_host.so'),
				('version','modules/mod_version.so'),
				]
	if sys.platform == 'win32':
		modules.append(('logio','modules/mod_logio.so'))
		
	configureApacheImpl(installation,modules)

def configureCliAndRegisterDirectoryCredentials(installation):

	versions = os.environ['SV_VERSION_INFINITE'].split('.')
	
	if sys.platform == 'win32':
		lConfFile = os.path.join(getAppData(),'3djuump-infinite-cli','conf_%s_%s.json' % (versions[0],versions[1]))
	else:
		lConfFile = os.path.join(os.environ['HOME'],'.3djuump-infinite-cli','conf_%s_%s.json' % (versions[0],versions[1]))
	
	lCliExe = None
	if sys.platform == 'win32':
		if installation.isDirectoryInstall():
			lCliExe = os.path.join(installation.directory_binary_folder,'3dJuumpInfiniteCli.exe')
		else:
			lCliExe = os.path.join(installation.proxy_binary_folder,'3dJuumpInfiniteCli.exe')
	else:
		lCliExe = os.path.join('/usr/lib/3djuump-infinite-cli/bin','3dJuumpInfiniteCli')
	
	# init configuration
	# configure cli at user scope (root) to avoid exposing registered directory to std user
	shellExecExceptOnError([lCliExe, 'cli', 'conf', 'init','--location','userscope'])

	# if installation.isDirectoryInstall() :
		# if hasattr(installation,'directory_api_key' and not installation.directory_api_key is None:
			# shellExecExceptOnError([lCliExe, 'cli', 'conf', 'directory','registerapikey','this',installation.directory_public_url,installation.directory_api_key,'--location','userscope'])
		# else:
			# raise Exception('need to handle m2m here')

		#load existing configuration to configure mTLS and ssl check
		# lConf = loadJson(lConfFile)
		
		# lConf['directory_collection'].setdefault('this',{})
		# lDirectoryConf = lConf['directory_collection']['this']
		# lDirectoryConf.setdefault('http_client_configuration',{})
		# lDirectoryConf['http_client_configuration']['verify_ssl_peer'] = installation.verify_ssl_peer
		# if hasattr(installation,'provided_mTLS_root_ca') and not installation.provided_mTLS_root_ca is None :
			# lDirectoryConf['http_client_configuration']['client_certificate'] = {'crt':installation.certificate_file,'key':installation.privatekey_file}
		# else:
			# lDirectoryConf['http_client_configuration']['client_certificate'] = False

		# saveJson(lConf,lConfFile)
	
	# consolidate configuration
	shellExecExceptOnError([lCliExe, 'cli', 'conf', 'consolidate','--location','userscope'] )


def _hasConfEntry(pConf : dict, pPath: str):
	lDst = pConf
	lKeys = pPath.split('/')
	for e in lKeys[:-1]:
		if not e in lDst or lDst[e] is None:
			return False
		lDst = lDst[e]
	return lKeys[-1] in lDst

def _editConfEntry(pConf : dict, pPath: str, pDefault):
	lDst = pConf
	lKeys = pPath.split('/')
	for e in lKeys[:-1]:
		if not e in lDst or lDst[e] is None:
			lDst[e]={}
		lDst = lDst[e]
	if not lKeys[-1] in lDst:
		lDst[lKeys[-1]] = pDefault
	return lDst[lKeys[-1]]

def _setConfValue(pConf : dict,pPath : str, pVal):
	lDst = pConf
	lKeys = pPath.split('/')
	for e in lKeys[:-1]:
		if not e in lDst or lDst[e] is None:
			lDst[e]={}
		lDst = lDst[e]
	lDst[lKeys[-1]] = pVal

def _delConfValue(pConf : dict,pPath : str):
	lDst = pConf
	lKeys = pPath.split('/')
	for e in lKeys[:-1]:
		if not e in lDst or lDst[e] is None:
			return
		lDst = lDst[e]
	if lKeys[-1] in lDst:
		del lDst[lKeys[-1]]

def configureInfiniteService(installation):
	import urllib
	print('Generate/Update infinite configuration files')
	lInfiniteServices = []
	if installation.isDirectoryInstall():
		lInfiniteServices.append(('3djuump-infinite-directory','directory'))
	if installation.isProxyInstall():
		lInfiniteServices.append(('3djuump-infinite-proxy','proxy'))
	if installation.isDirectoryInstall() and installation.isDockerInstall():
		lInfiniteServices.append(('3djuump-infinite-cli','cli'))
		
	for (servicename,type) in lInfiniteServices:
		versions = os.environ['SV_VERSION_INFINITE'].split('.')
		
		if installation.isDockerInstall():
			lConfFile = os.path.join(installation.general.install_basepath,servicename + '_conf.json')
		elif sys.platform == 'win32':
			lConfFile = os.path.join(getAppData(),servicename,'conf_%s_%s.json' % (versions[0],versions[1]))
		else:
			lConfFile = os.path.join('/etc',servicename,'conf_%s_%s.json' % (versions[0],versions[1]))
		
		
		#load existing configuration
		lConf = loadJson(lConfFile)
		
		_setConfValue(lConf,'http_client_configuration/verify_ssl_peer',installation.general.verify_ssl_peer)
		
		if hasattr(installation.general,'provided_mTLS_root_ca'):
			_setConfValue(lConf,'http_client_configuration/client_certificate',{'crt':installation.certificate_file,'key':installation.privatekey_file})
		else:
			_setConfValue(lConf,'http_client_configuration/client_certificate',False)

		if hasattr(installation.general,'loki_post_url'):
			lLokiPostUrl = urllib.parse.urlparse(installation.general.loki_post_url)
			if not lLokiPostUrl.username is None:
				_setConfValue(lConf,'log/loki/login',lLokiPostUrl.username)
				_setConfValue(lConf,'log/loki/password',lLokiPostUrl.password)
			else:
				_delConfValue(lConf,'log/loki/login')
				_delConfValue(lConf,'log/loki/password')
				
			# remove credentials from url
			lLokiPostUrl = lLokiPostUrl._replace(netloc=lLokiPostUrl.hostname)
			_setConfValue(lConf,'log/loki/posturl',urllib.parse.urlunparse(lLokiPostUrl))
			_setConfValue(lConf,'log/loki/label/host', socket.gethostname())
			_setConfValue(lConf,'log/loki/http_client_configuration/verify_ssl_peer',installation.general.verify_ssl_peer)
			_setConfValue(lConf,'log/loki/http_client_configuration/http_proxy',False)
		else:
			_delConfValue(lConf,'log/loki')

		_setConfValue(lConf,'log/log2console',False)

		lDirectoryBackendUrl = None
		if installation.isDirectoryInstall() and installation.isDockerInstall():
			lDirectoryBackendUrl = 'http://directory:80/directory'
		elif installation.isProxyInstall() and hasattr(installation.proxy,'directory_backend_url'):
			lDirectoryBackendUrl = installation.proxy.directory_backend_url + '/directory'
		elif installation.isDirectoryInstall():
			if hasattr(installation.general,'backend_url'):
				lDirectoryBackendUrl = installation.general.backend_url + '/directory'
			else:
				lDirectoryBackendUrl = installation.general.public_url + '/directory'
		else:
			raise Exception('Missing directory_backend_url')
		
		
		if type == 'directory':
			if installation.isDockerInstall():
				_setConfValue(lConf,'log/folder', '/infinite_logs')
			else:
				_setConfValue(lConf,'log/folder', os.path.join(installation.directory_log_folder).replace('\\','/'))
			
			if installation.isDockerInstall() and installation.general.docker_use_minio:
				_delConfValue(lConf,'filerstorage')
				_setConfValue(lConf,'filerstorage/type', 's3bucket')
				_setConfValue(lConf,'filerstorage/region', 'us-east-1')
				_setConfValue(lConf,'filerstorage/access_key', installation.minio_login)
				_setConfValue(lConf,'filerstorage/secret_key', installation.minio_password)
				_setConfValue(lConf,'filerstorage/url', 'http://minio:9000/infinite-bucket')
			else:
				_setConfValue(lConf,'filerstorage/type', 'filesystem')
				if installation.isDockerInstall():
					_setConfValue(lConf,'filerstorage/folder', '/infinite_directory_data')
				else:
					_setConfValue(lConf,'filerstorage/folder', installation.directory_filer_folder)

			_setConfValue(lConf,'postgres/database','InfiniteDirectory')
			
			if installation.isDockerInstall():
				_setConfValue(lConf,'directoryapi/bindport',80)
				_setConfValue(lConf,'directoryapi/publicbind',True)
			else:
				_setConfValue(lConf,'directoryapi/bindport',installation.directory.directory_api_local_port)
				_setConfValue(lConf,'directoryapi/publicbind',False)
			# check if it already registered in the conf
			lBackendVHosts = _editConfEntry(lConf,'directoryapi/backendvhosts',[])
			lFoundUrls = set()
			for v in lBackendVHosts:
				lFoundUrls.add(v['url'])
			lBackendVHostsToRegister = []
			if installation.isDockerInstall():
				lBackendVHostsToRegister.append(('http://directory:80/directory',True))
			if hasattr(installation.general,'backend_url'):
				lBackendVHostsToRegister.append((installation.general.backend_url + '/directory',False))
			if len(lBackendVHostsToRegister) > 0:
				for u, infinitebackendonly in lBackendVHostsToRegister:
					if not u in lFoundUrls:
						lBackendVHosts.append({"url":u,"securityschemes": None, "securityflows": ['back.infinite'] if infinitebackendonly else None,"endpoints":{}})
			else:
				_setConfValue(lConf,'directoryapi/backendvhosts',[])
			_setConfValue(lConf,'directoryapi/publicvhost/url',installation.general.public_url + '/directory')
			if 'all' in installation.directory.public_url_allowed_trafic:
				_setConfValue(lConf,'directoryapi/publicvhost/securityschemes',None)
				_setConfValue(lConf,'directoryapi/publicvhost/securityflows',None)
			else:
				lSecuritySchemes = []
				lSecurityFlows = []
				
				# need to accept client trafic
				lSecurityFlows.append('app.client')
				lSecuritySchemes.append('infinitebearer.directory_session')
				lSecuritySchemes.append('infinitebearer.data_session')
				lSecuritySchemes.append('infiniteprivate')
				lSecuritySchemes.append('infinitebearer.directory_session_extended')
				lSecuritySchemes.append('infinitebearer.data_session_extended')
				lSecuritySchemes.append('infinitebearer.directory_session_download_token')
				lSecuritySchemes.append('infinitebearer.data_session_download_token')
				
				for r in installation.directory.public_url_allowed_trafic:

					if r == 'admin_portal':
						lSecurityFlows.append('app.admin')
					elif r == 'connector':
						lSecurityFlows.append('back.connector')
						lSecuritySchemes.append('http.directory_key')
						lSecuritySchemes.append('http.m2m_bearer')
					elif r == 'all':
						pass
					else:
						raise Exception()
				_setConfValue(lConf,'directoryapi/publicvhost/securityschemes',lSecuritySchemes)
				_setConfValue(lConf,'directoryapi/publicvhost/securityflows',lSecurityFlows)
			if hasattr(installation.directory,'directory_api_key'):
				_setConfValue(lConf,'directoryapi/apikey',installation.directory.directory_api_key)
			else:
				_setConfValue(lConf,'directoryapi/apikey',None)
				if installation.openidconnect['m2m_auth'] is None:
					raise Exception('Neet at least an api_key or m2m')

			_setConfValue(lConf,'openidconnect', installation.openidconnect)
		elif type == 'proxy':
			if installation.isDockerInstall():
				_setConfValue(lConf,'log/folder', '/infinite_logs')
			else:
				_setConfValue(lConf,'log/folder',os.path.join(installation.proxy_log_folder).replace('\\','/'))
			_setConfValue(lConf,'openidconnect/common',installation.openidconnect['common'])
			
			# load proxy validation schema to found which oidc keys should be kept for proxy configuration
			lUserAuthKeys = None
			lM2MKeys = None
			with open( os.path.join(os.path.dirname( os.path.realpath( __file__ ) ),'validation_schema/schema_form_install_info.schema.json'),encoding='UTF-8') as fv:
				lProxyValidationSchema = json.load(fv)
				lUserAuthKeys = lProxyValidationSchema['properties']['openidconnect']['oneOf'][1]['properties']['user_auth']['properties'].keys()
				lM2MAuthKeys = lProxyValidationSchema['properties']['openidconnect']['oneOf'][1]['properties']['m2m_auth']['oneOf'][1]['properties'].keys()
			
			for k in lUserAuthKeys:
				if k in installation.openidconnect['user_auth']:
					_setConfValue(lConf,'openidconnect/user_auth/' + k, installation.openidconnect['user_auth'][k])
			
			if installation.openidconnect['m2m_auth'] is None:
				_setConfValue(lConf,'openidconnect/m2m_auth',None)
			else:
				for k in lM2MAuthKeys:
					if k in installation.openidconnect['m2m_auth']:
						_setConfValue(lConf,'openidconnect/m2m_auth/'+k,installation.openidconnect['m2m_auth'][k])

			_setConfValue(lConf,'postgres/database','InfiniteProxy')
			
			_setConfValue(lConf,'directoryapi/url', lDirectoryBackendUrl )
			if hasattr(installation,'directory') and hasattr(installation.directory,'directory_api_key'):
				_setConfValue(lConf,'directoryapi/apikey',installation.directory.directory_api_key)
			else:
				_setConfValue(lConf,'directoryapi/apikey',None)
				if installation.openidconnect['m2m_auth'] is None:
					raise Exception('Neet at least an directory_api_key or m2m for directoryapi')
			
			if installation.isDockerInstall():
				_setConfValue(lConf,'proxyapi/bindport',80)
				_setConfValue(lConf,'proxyapi/publicbind',True)
				_setConfValue(lConf,'elasticsearch/url','http://elasticsearch:9200')
				_setConfValue(lConf,'elasticsearch/login','elastic')
				_setConfValue(lConf,'elasticsearch/password',installation.elastic_password)
			else:
				_setConfValue(lConf,'proxyapi/bindport',installation.proxy.proxy_api_local_port)
				_setConfValue(lConf,'proxyapi/publicbind',False)
				_setConfValue(lConf,'elasticsearch/url','http://127.0.0.1:9200')
				_setConfValue(lConf,'elasticsearch/login','')
				_setConfValue(lConf,'elasticsearch/password','')
			
			if installation.isDirectoryInstall() and installation.isDockerInstall():
				_setConfValue(lConf,'proxyapi/backendvhost/url','http://proxy:80/proxy')
			elif hasattr(installation.general,'backend_url'):
				_setConfValue(lConf,'proxyapi/backendvhost/url',installation.general.backend_url + '/proxy')
			else:
				_delConfValue(lConf,'proxyapi/backendvhost')
			if _hasConfEntry(lConf,'proxyapi/backendvhost') and not _hasConfEntry(lConf,'proxyapi/backendvhost/securityschemes'):
				_setConfValue(lConf,'proxyapi/backendvhost/securityschemes',None)
			if _hasConfEntry(lConf,'proxyapi/backendvhost') and not _hasConfEntry(lConf,'proxyapi/backendvhost/securityflows'):
				_setConfValue(lConf,'proxyapi/backendvhost/securityflows',None)
			
			_setConfValue(lConf,'proxyapi/publicvhost/url',installation.general.public_url + "/proxy")
			if hasattr(installation.proxy,'proxy_api_key') :
				_setConfValue(lConf,'proxyapi/apikey',installation.proxy.proxy_api_key)
			else:
				_setConfValue(lConf,'proxyapi/apikey',None)
				if installation.openidconnect['m2m_auth'] is None:
					raise Exception('Neet at least an api_key or m2m')

			if installation.isDockerInstall():
				_setConfValue(lConf,'workingfolder', '/infinite_proxy_data')
			else:
				_setConfValue(lConf,'workingfolder', installation.proxy_data_folder)
				
			if installation.isDirectoryInstall() and installation.isDockerInstall():
				_setConfValue(lConf,'asyncjobsolver/enable', False)
			else:
				_setConfValue(lConf,'asyncjobsolver/enable', True)
				_setConfValue(lConf,'asyncjobsolver/maxcpu', installation.asyncjobsolver.max_cpu)
				_setConfValue(lConf,'asyncjobsolver/maxmemorymb', installation.asyncjobsolver.max_memory_MB)

				lOpenGLProvider = None
				if sys.platform == 'win32':

					if hasattr(installation.proxy,'win32openglprovider'):
						lOpenGLProvider = installation.proxy.win32openglprovider

					if lOpenGLProvider == 'auto' or lOpenGLProvider is None:
						# on windows depending on available GPU we try to select the best openglprovider
						(stdout,stderr,errorCode) = shellExecExceptOnError(['wmic','path','win32_videocontroller','get', 'Name'])

						# here are some well known adapters
						# system openglprovider
						#	- NVIDIA Quadro RTX 3000
						#	- Intel(R) UHD Graphics 630
						#	- AMD Radeon(TM) Graphics
						#	- VMware SVGA 3D => use system because currently our mesa conflict with the one from VMware
						# mesa openglprovider
						#	- Matrox G200eW3 (Nuvoton) WDDM 2.0
						#	- Microsoft Remote Display Adapter
						#	- Microsoft Hyper-V Video
						#	- Citrix Display Only Adapter
						#	- Citrix Indirect Display Adapter

						lOpenGLProvider = 'mesa'
						stdout = stdout.lower()
						for e in ['NVIDIA','Intel','VMware SVGA 3D','AMD']:
							if e.lower() in stdout:
								lOpenGLProvider = 'system'
								break
				else:
					lOpenGLProvider = 'system'
				
				_setConfValue(lConf,'asyncjobsolver/openglprovider', lOpenGLProvider)
				

		elif type == 'cli':
			assert(installation.isDockerInstall())
			_setConfValue(lConf,'log/folder', '/infinite_logs')
			_delConfValue(lConf,'directory_collection/thedirectory')
			_setConfValue(lConf,'directory_collection/thedirectory/http_client_configuration/verify_ssl_peer',installation.general.verify_ssl_peer)
			_setConfValue(lConf,'directory_collection/thedirectory/url',lDirectoryBackendUrl)
			# favor use of m2m_auth
			if not installation.openidconnect['m2m_auth'] is None:
				_setConfValue(lConf,'directory_collection/thedirectory/openidconnect/common/configuration_endpoint',installation.openidconnect['common']['configuration_endpoint'])
				_setConfValue(lConf,'directory_collection/thedirectory/openidconnect/common/http_client_configuration',installation.openidconnect['common']['http_client_configuration'])
				_setConfValue(lConf,'directory_collection/thedirectory/openidconnect/m2m_auth/client_id',installation.openidconnect['m2m_auth']['client_id'])
				_setConfValue(lConf,'directory_collection/thedirectory/openidconnect/m2m_auth/client_secret',installation.openidconnect['m2m_auth']['client_secret'])
			else:
				if not hasattr(installation,'directory') or not hasattr(installation.directory,'directory_api_key'):
					raise Exception('Neet at least an directory_api_key or m2m for directoryapi')
				_setConfValue(lConf,'directory_collection/thedirectory/apiKey',installation.directory.directory_api_key)
				
		if type in ['directory','proxy']:
			if installation.isDockerInstall():
				_setConfValue(lConf,'postgres/hosts', [{'host':'postgres','port':5432}])
			else:
				_setConfValue(lConf,'postgres/hosts', [{'host':'127.0.0.1','port':installation.postgres.postgres_port}])
			_setConfValue(lConf,'postgres/ssl/enable', False)
			_setConfValue(lConf,'postgres/login',installation.postgres.postgres_login)
			_setConfValue(lConf,'postgres/password',installation.postgres.postgres_password)
			
		
		saveJson(lConf,lConfFile)
		
		if not installation.isDockerInstall():
			lCliExe = None
			if sys.platform == 'win32':
				if installation.isDirectoryInstall():
					lCliExe = os.path.join(installation.directory_binary_folder,'3dJuumpInfiniteCli.exe')
				else:
					lCliExe = os.path.join(installation.proxy_binary_folder,'3dJuumpInfiniteCli.exe')
			else:
				lCliExe = os.path.join('/usr/lib/3djuump-infinite-cli/bin','3dJuumpInfiniteCli')
		
			# consolidate configuration
			shellExecExceptOnError([lCliExe, type, 'conf', 'consolidate'])
			
			# create db
			shellExecExceptOnError([lCliExe, type, 'init'])
	
		if sys.platform != 'win32' and installation.isDockerInstall():
			setFileOrFolderRights(installation.owner_user,installation.owner_grp,'060',True,lConfFile)


def isClusterExists(installation):
	if sys.platform == 'win32' :
		return True
	cmd = ['pg_lsclusters']
	(stdout,stderr,errorCode) = shellExecExceptOnError(cmd)
	
	targetVersion = installation._postgresl_version
	
	lines = stdout.splitlines()
	for line in lines :
		allContent = line.split()
		if len(allContent) > 2 :
			if (allContent [0] == targetVersion) and (allContent [1] == installation._postgresl_cluster_name) :
				return True
	return False

def getInstalledClusterVersions (installation_cluster_name) :
	if sys.platform == 'win32' :
		return []
	cmd = ['pg_lsclusters']
	(stdout,stderr,errorCode) = shellExecExceptOnError(cmd)
	
	versions = []
	lines = stdout.splitlines()
	for line in lines :
		allContent = line.split()
		if len(allContent) > 2 :
			if allContent [1] == installation_cluster_name :
				versions.append(allContent[0])
	return versions

def createClusterDebian (installation) :
	if sys.platform == 'win32' :
		return
	
	cmd = ['usermod','-a','-G',installation.owner_grp,'postgres']
	shellExecExceptOnError(cmd)

	
	items = [installation._privatekey_file_pg, installation._privatekey_file_pg,installation._ssl_folder_pg]
	for item in items :
		setFileOrFolderRights('postgres','postgres','400',True,item)
		
	cmd = ['chmod','u+x',installation._ssl_folder_pg]
	shellExecExceptOnError(cmd)
	
	cmd = ['chown','-R','postgres:postgres', installation.postgres_data_folder]
	shellExecExceptOnError(cmd)
		
	cmd = ['chmod','-R','700', installation.postgres_data_folder]
	shellExecExceptOnError(cmd)
	
	if not isClusterExists(installation) :
		cmd = ['pg_createcluster','-d',installation.postgres_data_folder,'--port',str(installation.postgres.postgres_port),'-E','UTF8','--locale=en_US.UTF8','-u','postgres','%s' % (installation._postgresl_version),installation._postgresl_cluster_name]
		shellExecExceptOnError(cmd)

def updatePostgresConfFile(confFile,installation,certificate,privateKey) :
	print('Generate/Update postgres configuration files')
	src = confFile
	dst = src
	
	bakupFile = os.path.join(os.getcwd(),os.path.split(dst)[1]+".bak")
	if not os.path.exists(bakupFile) and os.path.exists(dst):
		shutil.copy(dst,bakupFile)
	
	regContent = '^#{0,1}\\s*%s\\s*=.*$'
	
	lPgSharedMemory = max(1024,installation.postgres.postgres_memory_MB * 0.8)
	lPgWorkMemory = min(max(16,installation.postgres.postgres_memory_MB / 100),128)
	lPgMaintenanceWorkMemory = min(max(512,installation.postgres.postgres_memory_MB / 3),2048)

	regs = [
				(re.compile(regContent % 'shared_buffers'),"shared_buffers = %dMB" % lPgSharedMemory),
				(re.compile(regContent % 'work_mem'),"work_mem = %dMB" % lPgWorkMemory),
				(re.compile(regContent % 'maintenance_work_mem'),"maintenance_work_mem = %dMB" % lPgMaintenanceWorkMemory),
				
				#(re.compile(regContent % 'checkpoint_segments'),"checkpoint_segments = 30"),
				(re.compile(regContent % 'client_min_messages'),"client_min_messages = warning"),
				(re.compile(regContent % 'logging_collector'),"logging_collector = on"),
				(re.compile(regContent % 'max_connections'),"max_connections = 1000"),
				(re.compile(regContent % 'password_encryption'),"password_encryption = scram-sha-256"),
				(re.compile(regContent % 'max_wal_size'),"max_wal_size = 2GB"),
				(re.compile(regContent % 'min_wal_size'),"min_wal_size = 80MB"),
				(re.compile(regContent % 'log_timezone'),"log_timezone = 'Etc/UTC'"),
				(re.compile(regContent % 'timezone'),"timezone = 'Etc/UTC'"),
			]
	if installation.isDockerInstall():
		regs = regs + [
			(re.compile(regContent % 'port'), "port = 5432"),
			(re.compile(regContent % 'listen_addresses'),"listen_addresses = '*'"),
		]
	else:
		regs = regs + [
			(re.compile(regContent % 'port'), "port = %s" % installation.postgres.postgres_port),
			(re.compile(regContent % 'listen_addresses'),"listen_addresses = 'localhost'"),
			(re.compile(regContent % 'ssl_cert_file'),"ssl_cert_file = '%s'" % certificate),
			(re.compile(regContent % 'ssl_key_file'),"ssl_key_file = '%s'" % privateKey),
			(re.compile(regContent % 'ssl'), "ssl = on"),
			(re.compile(regContent % 'ssl_ciphers'),"ssl_ciphers = 'DEFAULT:!LOW:!EXP:!MD5:@STRENGTH'"),
			#(re.compile(regContent % 'ssl_renegotiation_limit'),"#ssl_renegotiation_limit = 512MB"),
		]
		
	oldContent = ''
	if os.path.exists(src):
		with open(src,"r",encoding='UTF-8') as handle:
			oldContent = handle.read()
	
	allLines = oldContent.split('\n')
	newContent = ''
	lMatchedRe = set()
	for line in allLines :
		if line != '':
			foundContent = False
			for (reg,replacement) in regs :
				if reg.match(line) :
					lMatchedRe.add(reg)
					foundContent = True
					newContent = newContent + replacement + '\n'
					break
			if not foundContent:
				newContent = newContent + line + '\n'
	for (reg,replacement) in regs:
		if not reg in lMatchedRe:
			newContent = newContent + replacement + '\n'
		
	if oldContent != newContent :
		with open(dst,"w",encoding='UTF-8') as handle:
			handle.write(newContent)
	if sys.platform != 'win32':
		if installation.isDockerInstall():
			setFileOrFolderRights(installation.owner_user,installation.owner_grp,'644',False,dst)
		else:
			setFileOrFolderRights('postgres','postgres','440',False,dst)

def getPreviousPostgresConfFile(installation) :
	if sys.platform == 'win32' :
		return os.path.join(installation._previous_win_postgres_data_folder,'postgresql.conf')	
	else :
		return '/etc/postgresql/%s/%s/postgresql.conf' % (installation._previous_postgres_version,installation._postgresl_cluster_name)
		
def getPostgresPortFromConfFile(confFile) :
	
	reg = re.compile('^\\s*%s\\s*=\\s*([0-9]+)[^0-9]?.*$' % 'port')
				
	with open(confFile,"r",encoding='UTF-8') as handle:
		content = handle.read()
	
	allLines = content.split('\n')
	found = 0
	port = 5432
	for line in allLines :
		if line != '':
			regObj = reg.match(line)
			if not regObj is None :
				found = found + 1
				port = int(regObj.group(1))
				#print("found port %d" % port)
	if found > 1 :
		raise Exception("Mulitple port definition found for postgres")
	#print("final port %d" % port)
	return port
		
def restoreHbaFile(src,hbaFile) :
	
	dst = hbaFile
	
	bakupFile = os.path.join(os.getcwd(),os.path.split(dst)[1]+".bak")
	if not os.path.exists(bakupFile):
		shutil.copy(dst,bakupFile)
	
	with open(src,"r",encoding='UTF-8') as handle:
		content = handle.read()
	
	oldContent = ''
	if os.path.exists(dst) :
		with open(dst,"r",encoding='UTF-8') as handle:
			oldContent = handle.read()
		
	if oldContent != content:
		with open(dst,"w",encoding='UTF-8') as handle:
			handle.write(content)
		
def changePostgresPassword(psql,hbaFile,pgport,pgdatabaseowner,pglogin,pgpasswd,serviceName):
		
	os.environ['PGPASSWORD'] = pgpasswd
	pg_cmd = "SELECT * FROM pg_stat_database;"
	cmd = [psql,'-w','-U',pglogin,'-d','postgres','-h','127.0.0.1','-p',str(pgport),'-c',pg_cmd]
	(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
	del os.environ['PGPASSWORD'] 

	if errorCode == 0 :
		return
	
	src = hbaFile
	
	(handle,bakupFile) = tempfile.mkstemp(suffix='.tmp')
	os.close(handle)
	shutil.copy(src,bakupFile)
		
	content = 'host all all 127.0.0.1/32 trust'
	with open(src,"w",encoding='UTF-8') as handle:
		handle.write(content)
	
	restartService(serviceName,altName='postgresql')
	
	if pgdatabaseowner != pglogin :
		pg_cmd = "DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '%s') THEN CREATE ROLE \"%s\";END IF;END $$;" % (pglogin,pglogin)
		cmd = [psql,'-w','-U',pgdatabaseowner,'-d','postgres','-h','127.0.0.1','-p',str(pgport),'-c',pg_cmd]
		shellExecExceptOnError(cmd)
		pg_cmd =  "ALTER USER \"%s\" WITH SUPERUSER CREATEDB LOGIN CREATEROLE INHERIT;" % pglogin
		cmd = [psql,'-w','-U',pgdatabaseowner,'-d','postgres','-h','127.0.0.1','-p',str(pgport),'-c',pg_cmd]
		shellExecExceptOnError(cmd)
	
	pg_cmd = "ALTER USER \"%s\" WITH PASSWORD '%s';" % (pglogin,pgpasswd)
	cmd = [psql,'-w','-U',pgdatabaseowner,'-d','postgres','-h','127.0.0.1','-p',str(pgport),'-c',pg_cmd]
	shellExecExceptOnError(cmd)
	
	# and restore original hba
	with open(bakupFile,"r",encoding='UTF-8') as handle:
		content = handle.read()
	os.remove(bakupFile)
	
	with open(src,"w",encoding='UTF-8') as handle:
		handle.write(content)
	
	restartService(serviceName,altName='postgresql')

def getPsql (installation):
	if sys.platform == 'win32':
		return os.path.join(installation.postgres_folder,'bin','psql.exe')
	else :
		return 'psql'

def getPreviousPsql (installation):
	if sys.platform == 'win32':
		return os.path.join(installation._previous_win_postgres_folder,'bin','psql.exe')
	else :
		return 'psql'

def getPreviousPostgresqlServiceName(installation) :
	if sys.platform == 'win32':
		return 'postgresql-x64-%s'%(installation._previous_postgres_version)
		
	else :
		return 'postgresql'

def getPostgresqlServiceName(installation) :
	if sys.platform == 'win32':
		return 'postgresql-x64-%s'%(installation._postgresl_version)
		
	else :
		return 'postgresql'

def getPostgresqlHbaFile(installation) :
	if sys.platform == 'win32':
		return os.path.join(installation.postgres_data_folder,'pg_hba.conf')
	else :
		return '/etc/postgresql/%s/%s/pg_hba.conf' % (installation._postgresl_version,installation._postgresl_cluster_name)

def getPreviousPostgresqlHbaFile(installation) :
	if sys.platform == 'win32':
		return os.path.join(installation._previous_win_postgres_data_folder,'pg_hba.conf')
	else :
		return '/etc/postgresql/%s/%s/pg_hba.conf' % (installation._previous_postgres_version,installation._postgresl_cluster_name)


def checkHasBuilds(installation) :
	
	print("Checking if previous installation has some builds")
	
	src = getPreviousPostgresqlHbaFile(installation)
	if not os.path.exists(src) :
		return
	
	old_port = getPostgresPortFromConfFile(getPreviousPostgresConfFile(installation))
	
	(handle,bakupFile) = tempfile.mkstemp(suffix='.tmp')
	os.close(handle)
	shutil.copy(src,bakupFile)

	content = 'host all all 127.0.0.1/32 trust'
	with open(src,"w",encoding='UTF-8') as handle:
		handle.write(content)
	
	restartService(getPreviousPostgresqlServiceName(installation),altName='postgresql')
	
	psql = getPreviousPsql(installation)

	hasBuildPattern = '####3djuump-infinite Has Builds####'
	hasNoBuildPattern = '####3djuump-infinite Has No Builds####'
	
	build_version = os.environ['SV_BUILD_VERSION']
	build_versions = build_version.split('.')
	
	pg_cmd	=	"DROP TABLE IF EXISTS infinite_temp; "
	pg_cmd +=	"DROP FUNCTION IF EXISTS testHasBuilds(); "
	pg_cmd +=	"CREATE OR REPLACE FUNCTION testHasBuilds() "
	pg_cmd +=	"RETURNS TABLE(build_content TEXT) "
	pg_cmd +=	"LANGUAGE plpgsql "
	pg_cmd +=	"AS $$ "
	pg_cmd +=	"DECLARE "
	pg_cmd +=	"BEGIN "
	pg_cmd +=		"build_content = '%s'; " % hasNoBuildPattern
	pg_cmd +=		"IF EXISTS (SELECT FROM information_schema.tables WHERE table_schema='DirectoryPrivate' AND table_name='ProxyBuilds') THEN "
	pg_cmd +=			"IF EXISTS ( SELECT FROM \"DirectoryPrivate\".\"ProxyBuilds\" WHERE string_to_array(buildversion, '.')::int[] != ARRAY[%s,%s]) THEN " % (build_versions[0],build_versions[1]) 
	pg_cmd +=				"build_content = '%s'; " % hasBuildPattern
	pg_cmd +=			"END IF; "
	pg_cmd +=		"END IF;	 "
	pg_cmd +=		"IF EXISTS (SELECT FROM information_schema.tables WHERE table_schema='Directory' AND table_name='ProxyBuilds') THEN "
	pg_cmd +=			"IF EXISTS (SELECT FROM \"Directory\".\"ProxyBuilds\" WHERE string_to_array(builddbcommentlight->>'buildversion', '.')::int[] != ARRAY[%s,%s]) THEN " % (build_versions[0],build_versions[1])
	pg_cmd +=				"build_content = '%s'; " % hasBuildPattern
	pg_cmd +=			"END IF; "
	pg_cmd +=		"END IF; "
	pg_cmd +=		"IF (build_content = '####3djuump-infinite Has No Builds####') THEN "
	pg_cmd +=			"IF EXISTS (SELECT FROM information_schema.tables WHERE table_schema='ProxyAdministration' AND table_name='Builds') THEN "
	pg_cmd +=				"IF EXISTS (SELECT FROM information_schema.columns WHERE table_name='Builds' and column_name='buildproperties') THEN "
	pg_cmd +=					"IF EXISTS (SELECT FROM \"ProxyAdministration\".\"Builds\" WHERE string_to_array(buildproperties->>'buildversion', '.')::int[] != ARRAY[%s,%s]) THEN " % (build_versions[0],build_versions[1])
	pg_cmd +=						"build_content = '%s'; " % hasBuildPattern
	pg_cmd +=					"END IF; "
	pg_cmd +=				"ELSE "
	pg_cmd +=					"IF EXISTS (SELECT dbnameorbuildid FROM \"ProxyAdministration\".\"Builds\") THEN "
	pg_cmd +=						"build_content = '%s'; " % hasBuildPattern
	pg_cmd +=					"END IF; "
	pg_cmd +=				"END IF; "
	pg_cmd +=			"END IF; "
	pg_cmd +=		"END IF; "
	pg_cmd +=		"RETURN NEXT; "
	pg_cmd +=	"END; "
	pg_cmd +=	"$$; "

	pg_cmd +=	"CREATE TEMPORARY TABLE infinite_temp AS (SELECT * FROM testHasBuilds()); "
	pg_cmd +=	"DROP FUNCTION IF EXISTS testHasBuilds(); "
	pg_cmd +=	"SELECT * FROM infinite_temp; "
	
	databases = ['InfiniteDirectory', 'InfiniteProxy']
	
	lHasErrors = False
	for cur_database in databases :
		cmd = [psql,'-w','-U',installation.postgres.postgres_login,'-d',cur_database,'-h','127.0.0.1','-p','%d' % old_port,'-c',pg_cmd]
		(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
	
		#print(stdout)
		#print('####')
		#print(stderr)
		
		if errorCode == 0:
			if stdout.find(hasBuildPattern) >= 0 :
				lHasErrors = True
	
	# and restore original hba
	with open(bakupFile,"r",encoding='UTF-8') as handle:
		content = handle.read()
	
	os.remove(bakupFile)
	
	with open(src,"w",encoding='UTF-8') as handle:
		handle.write(content)
	
	restartService(getPreviousPostgresqlServiceName(installation),altName='postgresql')
	
	if lHasErrors:
		print("The 3djuump infinite cluster has still some builds, please remove all builds (and backup as evojuumps) and projets (and backup their project descriptors) before proceeding")
		sys.exit(1)
		
## configure postgres : hba.conf and postgresql.conf
def configurePostgres(installation) :
	
	createClusterDebian(installation)
	
	print("Configuring postgresql")
	
	psql = getPsql(installation)
	serviceName = getPostgresqlServiceName(installation)
	hbaFile = getPostgresqlHbaFile(installation)
	if sys.platform == 'win32':
		confFile = os.path.join(installation.postgres_data_folder,'postgresql.conf')	
		pgdatabaseowner = installation.postgres.postgres_login
		certificate = installation.certificate_file.replace('\\','/')
		privateKey = installation.privatekey_file.replace('\\','/')
	else :
		confFile = '/etc/postgresql/%s/%s/postgresql.conf' % (installation._postgresl_version,installation._postgresl_cluster_name)
		pgdatabaseowner = 'postgres'
		certificate = installation._certificate_file_pg
		privateKey = installation._privatekey_file_pg
	
	# configure and set the required port and certificate
	updatePostgresConfFile(confFile,installation,certificate,privateKey)
	
	# restore hba
	restoreHbaFile(os.path.join(installation._Djuump_installers,'install form','config','pg_hba.conf'),hbaFile)
	
	# restart service to reflect new conf
	restartService(serviceName,altName='postgresql')	
	
	# reset pwd
	changePostgresPassword(psql,hbaFile,installation.postgres.postgres_port,pgdatabaseowner, installation.postgres.postgres_login,installation.postgres.postgres_password,serviceName)


def updateElasticSearchConfFile(confFile,installation) :
	if not installation.isProxyInstall():
		return
	
	if sys.platform != 'win32':
		# ensure that max_map_count is big enough
		(stdout,_,_) = shellExecExceptOnError(['sysctl','vm.max_map_count'],False)
		if int(stdout.split('=')[1]) < 262144:
			raise Exception('Elasticsearch needs vm.max_map_count >= 262144, edit your /etc/sysctl.conf file then reload it "sysctl -p"')
		
		(stdout,_,_) = shellExecExceptOnError(['swapon','--show=SIZE','--noheading','--raw','--bytes'],False)
		lSwapSize = 0
		for r in stdout.split('\n'):
			r = r.strip().lower()
			if r == '':
				continue
			try:
				lSwapSize = lSwapSize + int(r)
			except:
				pass
		if lSwapSize < (2*1024*1024):
			raise Exception('A minimum of 2GB of swap is needed to avoid OOM killing elasticsearch, allocating more swap is advised.')



	print('Generate/Update elasticsearch configuration files')
	src = os.path.join(installation._Djuump_installers,'install form','config','elasticsearch.yml')

	regs = [
		(re.compile('thread_pool\\.write\\.size:.*'),"thread_pool.write.size: %d" % (min(max(1,os.cpu_count() - 2),12)),True),
	]

	if installation.isDockerInstall():
		regs.append((re.compile('xpack\\.security\\.enabled:.*'),"xpack.security.enabled: true",True))
	if installation.isDockerInstall():
		regs = regs + [
			(re.compile('network.host:.*'),"network.host: _site_",True),
			(re.compile('path.data:.*'),"path.data: /usr/share/elasticsearch/data",True),
			(re.compile('path.logs:.*'),"",True),
		]
	else:
		regs = regs + [
			(re.compile('network.host:.*'),"network.host: 127.0.0.1",True),
		]

	copyAndConfigure(installation,src,confFile,regs)
	if sys.platform != 'win32':
		setFileOrFolderRights(installation.owner_user,installation.owner_grp,'440',True,confFile)

# executes a program but allows user input
def shellExecReturn (cmd):
	# make sure we have a list
	if type(cmd) is str :
		import shlex
		cmd = shlex.split(cmd)
	
	errorCode = 0
	try :
		errorCode = subprocess.call(cmd)
	except :
		print("Error executing %s "% cmd[0])
		errorCode = 1
		
	return errorCode

def stopService(serviceName) :
	if sys.platform == 'win32':
		cmd = ['net','stop',serviceName,'/y']
	else :
		cmd = ['systemctl','stop',serviceName]
	
	# ignore error, service might be missing
	shellExecExceptOnError(cmd,True)

# restart the given service
def restartService(serviceName, pIgnoreError = False, altName=None) :
	
	printName = serviceName
	if not altName is None :
		printName = altName
	print('Restarting service %s'% printName)
	if sys.platform == 'win32':
		stopService(serviceName)
		cmd = ['net','start',serviceName]
		(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
		if errorCode != 0 and not pIgnoreError:
			raise Exception('Error restarting %s, return %d %s %s' % (printName,errorCode,stdout,stderr))
	else :
		cmd = ['systemctl','restart',serviceName]
		(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
		if errorCode != 0 and not pIgnoreError:
			raise Exception('Error restarting %s, return %d\nstdout:\n%s\nstderr\n%s' % (printName,errorCode,stdout,stderr))
			
# restart the given service
def uninstallService(serviceName) :
	if sys.platform == 'win32':
		cmd = ['sc','delete',serviceName]
		(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
		if errorCode != 0 :
			raise Exception('Error unininstalling %s, return %d %s %s' % (serviceName,errorCode,stdout,stderr))

def stopAllServices(installation) :
	stopService('3djuump-infinite-proxy')
	stopService('3djuump-infinite-directory')
	stopService(getPostgresqlServiceName(installation))
	if sys.platform != 'win32':
		stopService('apache2')
		stopService('elasticsearch')
	else:
		stopService('Apachehttp3djuumpInfinite')
		stopService('elasticsearch-service-x64')

def startInfiniteServices(installation):
	if installation.isDirectoryInstall():
		restartService('3djuump-infinite-directory')
	if installation.isProxyInstall():
		restartService('3djuump-infinite-proxy')

def getCurrentAttribute(item,attrname) :
	if hasattr(installation,attrname) :
		return getattr(item,attrname)
	return ''
		
def getAttributes (item) :
	allNames = dir(item)
	allAttr = []
	for name in allNames :
		if not name.startswith('_') and not callable(getattr(item, name)):
			allAttr.append(name)
	return allAttr

def printAttrs (item) :
	allAttr = getAttributes(item)
	for name in allAttr :
		print(name+" : "+str(getattr(item, name)))


def toUrl (inputPath) :	
	import pathlib
	return pathlib.Path(inputPath).as_uri()

def getAppData () :
	
	import ctypes
	
	CSIDL_COMMON_APPDATA = 35
	_SHGetFolderPath = ctypes.windll.shell32.SHGetFolderPathW
	_SHGetFolderPath.argtypes = [ctypes.wintypes.HWND,
						ctypes.c_int,
						ctypes.wintypes.HANDLE,
						ctypes.wintypes.DWORD, ctypes.wintypes.LPCWSTR]

	path_buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
	result = _SHGetFolderPath(0, CSIDL_COMMON_APPDATA, 0, 0, path_buf)
	homedir = path_buf.value
	return homedir


def loadJson(src) :
	if not os.path.isfile(src):
		return {}
	with open(src,'r',encoding='utf-8') as f:
		return json.load(f)
	raise Exception('fail to read %s' % (src))
	
def saveJson(input,dst):
	with open(dst,'w',encoding='utf-8') as f:
		return json.dump(input,f, sort_keys=True, indent=4)
	raise Exception('fail to write %s' % (dst))

def parseIni(src) :
	parser = MyParser()
	parser.read(src)
	d = parser.as_dict()
	return d

def saveIni(input,dst) :
	content = ''
	
	if 'General' in input :
		content = content + ('[General]\n')
		for item,itemVal in input['General'].items() :
			content = content + ('%s=%s\n' % (item,itemVal))
		content = content+"\n"
		input.pop("General", None)
			
	for key,val in input.items() :
		content = content + ('[%s]\n' % key)
		for item,itemVal in val.items() :
			content = content + ('%s=%s\n' % (item,itemVal))
		content = content+"\n"
		
	with open(dst,"w",encoding='UTF-8') as handle:
		handle.write(content)
	
def saveConf (input,packageName,dst) :
	content = ''
			
	for key,val in input.items() :
		content = content + ('%s\t%s\t%s\t%s\n' % (packageName, key,val[0],val[1]))
		
	with open(dst,"w",encoding='UTF-8') as handle:
		handle.write(content)
	
def copyFiles(inputFolder,outFolder) :
	files = glob.glob(os.path.join(inputFolder,'*'))
	for curFile in files :
		dst = os.path.join(outFolder,os.path.split(curFile)[1])
		if not os.path.exists(dst) :
			shutil.copy(curFile,dst)

def _installInfiniteBinary(installation,exename,serviceName, values, unholdPackages, checkApt = True) :
	
	if sys.platform == 'win32' :
		print("Installing %s" % serviceName)
		
		filepattern = os.path.join(installation._Djuump_installers,'dist','win',exename) + "*"
		allFiles = glob.glob(filepattern)
		if len(allFiles) != 1:
			raise Exception("Could not find file %s" % exename)
			
		(handle,tmp_file) = tempfile.mkstemp(suffix='.ini')
		os.close(handle)
		saveIni(values,tmp_file)
		
		cmd = [os.path.normpath(allFiles[0]),'/SILENT','/LOADINF=%s' % tmp_file]
		
		cmd = cmd + ['/LOG=./' + exename + '.log']
	
		(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
		os.remove(tmp_file)
		if errorCode != 0 :
			raise Exception("Error installing %s %d %s %s" % (cmdToString(cmd),errorCode,stdout,stderr))
		
		
	else:
		for item in unholdPackages :
			cmd = cmd = ['apt-mark','unhold', item]
			shellExecExceptOnError(cmd)
		uninstallPackage(exename)
		
		if installation._first_linux_binary_install :
			installation._first_linux_binary_install = False
			uninstallPackage("lib3djuump-infinite-cli")
		
		(handle,tmp_file) = tempfile.mkstemp(suffix='.conf')
		os.close(handle)
		saveConf(values,serviceName, tmp_file)
		cmd = ['debconf-set-selections',tmp_file]
		(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
		os.remove(tmp_file)
		if errorCode != 0 :
			raise Exception("Error registering selections")
		installPackage(exename,False,True)
		
		
		if checkApt and installation._run_apt_upgrade :
			installation._run_apt_upgrade = False
			lastfrontend = None
			if 'DEBIAN_FRONTEND' in os.environ:
				lastfrontend=os.environ['DEBIAN_FRONTEND']
			os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
			print('Running apt-get upgrade')
			cmd = ['apt-get','-yq','-o','Dpkg::Options::=--force-confdef','upgrade']
			shellExecExceptOnError(cmd)
			if lastfrontend is None :
				del os.environ['DEBIAN_FRONTEND']
			else :
				os.environ['DEBIAN_FRONTEND']= lastfrontend

def installInfiniteBinary (installation) :
	if installation.isDirectoryInstall():
		if sys.platform == 'win32':	
			paramsDirectory = {
				'Setup' : {
					'Lang' : 'en',
					'Dir' : installation.directory_binary_folder,
					}
				}
		else :
			paramsDirectory = {
				'lib3djuump-infinite-cli/eula' : ('string', 'yes'),
			}
		_installInfiniteBinary(installation,installation.directory_binary_installer,'3djuump-infinite-directory', paramsDirectory, [installation.directory_binary_installer,installation.proxy_binary_installer])
	if installation.isProxyInstall():
		if sys.platform == 'win32':	
			paramsProxy = {
				'Setup' : {
					'Lang' : 'en',
					'Dir' : installation.proxy_binary_folder,
					}
				}
		else :
			paramsProxy = {
				'lib3djuump-infinite-cli/eula' : ('string', 'yes'),
			}
		_installInfiniteBinary(installation,installation.proxy_binary_installer,'3djuump-infinite-proxy', paramsProxy, [])

def createDockerImages(installation):
	lVersionFull = os.environ['SV_VERSION_INFINITE_FULL']
	lVersionShort = '.'.join(lVersionFull.split('.')[:2])

	lImageList = [ 'infinite.postgres','infinite.cli']
	if installation.isDirectoryInstall():
		lImageList.append('infinite.directory')
	if installation.isProxyInstall():
		lImageList.append('infinite.proxy')

	# list existing images
	(lOut,_,_) = shellExecExceptOnError(['docker','image','list','--format','json'])
	lAllInfiniteImages = {}
	for r in lOut.replace('\r','\n').split('\n'):
		r = r.strip()
		if r == '':
			continue
		lImageInfo = json.loads(r)
		if not 'Repository' in lImageInfo or not 'Tag' in lImageInfo:
			continue
		lImageName = lImageInfo['Repository']
		lImageTag = lImageInfo['Tag']
		if not 'infinite.' in lImageName or not lImageTag.startswith('%s.'%lVersionShort):
			continue
		# RedHat if using podman will prepend repository to image name in our case localhost
		if lImageName.startswith('localhost/'):
			lImageName = lImageName[10:]
		lAllInfiniteImages.setdefault(lImageName,set()).add(lImageTag)
	lExistingImages = set()
	for i in lAllInfiniteImages:
		if lVersionFull in lAllInfiniteImages[i]:
			lExistingImages.add(i)
	
	lForceRebuildDockerImages = False
	if len(lExistingImages) > 0:
		lInfoStr = 'Following docker images already exists :'
		for e in lExistingImages:
			lInfoStr = lInfoStr + '\n\t- %s:%s' % (e,lVersionFull)
		lInfoStr = lInfoStr + '\nWould you rebuild them ?'
		lForceRebuildDockerImages = askYesNoInput(lInfoStr,False)
	
	if len(lExistingImages) > 0 and lForceRebuildDockerImages:
		# we need to stop stack to rebuild images
		stopDockerStack(installation)
	
	for imagename in lImageList:
		if imagename in lExistingImages and not lForceRebuildDockerImages:
			print('Reuse existing Docker image %s:%s' % (imagename,lVersionFull))
			continue

		print('Build Docker image %s:%s' % (imagename,lVersionFull))
		os.chdir('..')
		lCmd = ['docker','build','-f','./install form/docker/%s.Dockerfile' % imagename,'-t','%s:%s' % (imagename,lVersionFull)]
		for e in ['http_proxy','https_proxy']:
			if e in os.environ:
				lCmd = lCmd + ['--build-arg','%s=%s' % (e,os.environ[e])]
		lCmd.append('.')
		# build image and tag with full version name
		(stdout,stderr,_) = shellExecExceptOnError(lCmd)
		with open('../docker_build_log_%s_std_out.log' % imagename,'w',encoding='utf-8') as logfd:
			logfd.write(stdout)
			logfd.flush()
		with open('../docker_build_log_%s_std_err.log' % imagename,'w',encoding='utf-8') as logfd:
			logfd.write(stderr)
			logfd.flush()
		# then add an extra 'latest' tag
		#shellExecExceptOnError(['docker','tag','%s:%s' % (imagename,lVersionFull),'%s:%s.x' % (imagename,lVersionShort)])
		os.chdir('install form')

	# update short version tag
	print('Update images tags')
	for imagename in lImageList:
		lVersions = [lVersionFull]
		if imagename in lAllInfiniteImages:
			lVersions = lVersions + list(lAllInfiniteImages[imagename])
		lVersions = sorted(lVersions)
		lCmd = ['docker','image','tag','%s:%s' % (imagename,lVersions[-1]),'%s:%s' % (imagename,lVersionShort)]
		shellExecExceptOnError(lCmd)
	
	lNeededComposeFiles = [
		'compose.postgres.yaml'
		]

	if installation.isDirectoryInstall():
		lNeededComposeFiles.append('compose.directory.yaml')
		if installation.general.docker_use_minio:
			lNeededComposeFiles.append('compose.minio.yaml')

	if installation.isProxyInstall():
		lNeededComposeFiles.append('compose.proxy.yaml')

	lComposeFileContent = {
		'networks':{

		},
		'include':[
		]
	}
	os.makedirs(installation.general.install_basepath,exist_ok=True)
	for lNeededComposeFileName in lNeededComposeFiles:
		with open('./docker/%s' % lNeededComposeFileName,'r',encoding='UTF-8') as f:
			lSubCompose = configureString(installation,f.read())
			lSubComposeContent = yaml.safe_load(lSubCompose)

		if lNeededComposeFileName == 'compose.directory.yaml' and installation.general.docker_use_minio:
			for d in ['directory_init','directory','directory_garbage_collector']:
				lSubComposeContent['services'][d]['networks'].append('net_directory_minio')
				lSubComposeContent['services'][d]['depends_on']['minio_init'] = {'condition':'service_completed_successfully'}

		with open(os.path.join(installation.general.install_basepath,lNeededComposeFileName),'w',encoding='UTF-8') as f:
			yaml.safe_dump(lSubComposeContent,f)
			
		lComposeFileContent['include'].append(lNeededComposeFileName)
		for s in lSubComposeContent['services']:
			if not 'networks' in lSubComposeContent['services'][s]:
				continue
			for n in lSubComposeContent['services'][s]['networks']:
				if n in lComposeFileContent['networks']:
					continue
				lComposeFileContent['networks'][n] = {'driver':'bridge','external': False}

	
	with open(os.path.join(installation.general.install_basepath,'compose.yaml'),'w',encoding='UTF-8') as f:
		yaml.safe_dump(lComposeFileContent,f)
	
	with open(os.path.join(installation.general.install_basepath,'.env'),'w',encoding='UTF-8') as f:
		f.write('# compose name\n')
		f.write('COMPOSE_PROJECT_NAME=%s\n' % installation.docker_project_name)
		f.write('IMG_VERSION_INFINITE_FULL=%s\n' % os.environ['SV_VERSION_INFINITE_FULL'])
		f.write('# postgres credentials\n')
		f.write('POSTGRES_LOGIN=%s\n' % (installation.postgres.postgres_login))
		f.write('POSTGRES_PASSWORD=%s\n' % (installation.postgres.postgres_password))
		if installation.isDirectoryInstall() and installation.general.docker_use_minio:
			f.write('# random password for minio local cluster\n')
			f.write('MINIO_VERSION=2023.12.23\n')
			f.write('MINIO_LOGIN=%s\n' % (installation.minio_login))
			f.write('MINIO_PASSWORD=%s\n' % (installation.minio_password))
		if installation.isProxyInstall():
			f.write('# random password for elasticsearch local cluster\n')
			f.write('ELASTIC_PASSWORD=%s\n' % (installation.elastic_password))
		if sys.platform == 'win32':
			f.write('# on windows default to 1000\n')
			f.write('GID=1000\n')
		else:
			(stdout,_,_) = shellExecExceptOnError(['getent','group',installation.owner_grp],False)
			lValues = stdout.split(':')
			if len(lValues) < 3:
				raise Exception('Unexpected output for getent')
			f.write('# group id of "%s"\n' % installation.owner_grp)
			f.write('GID=%s\n' % lValues[2])
		

def checkDockerInstall(installation):
	print('Check docker install')
	shellExecExceptOnError(['docker','version'],False)
	shellExecExceptOnError(['docker','compose','version'],False)
	
def stopDockerStack(installation):
	if installation.general.docker_config_only:
		return
	print('Stop docker stack')
	lPreviousWD = os.getcwd()
	os.chdir(installation.general.install_basepath)
	if os.path.isfile('compose.yaml'):
		shellExecExceptOnError(['docker','compose','down'],False)
	os.chdir(lPreviousWD)

def startDockerStack(installation):
	if installation.general.docker_config_only:
		return
	print('Start docker stack')
	lPreviousWD = os.getcwd()
	os.chdir(installation.general.install_basepath)
	shellExecExceptOnError(['docker','compose','up','--detach','--remove-orphans'],False)
	os.chdir(lPreviousWD)

	print('Wait for containers')
	lContainerToWaitFor = []
	if installation.isDirectoryInstall():
		lContainerToWaitFor.append('%s-directory-1' % installation.docker_project_name)
	if installation.isProxyInstall():
		lContainerToWaitFor.append('%s-proxy-1' % installation.docker_project_name)
	lContainerReady = set()
	for i in range(15,-1,-1):
		for c in lContainerToWaitFor:
			if c in lContainerReady:
				continue
			(stdout,_,_) = shellExecExceptOnError(['docker','container','inspect',c],False)
			lJson = json.loads(stdout)
			if lJson is None:
				continue
			if len(lJson) != 1:
				raise Exception('Unexpected inspect response')
			lStatus = lJson[0]['State']['Status']
			if lStatus == 'starting':
				continue
			elif lStatus != 'running':
				raise Exception('Invalid container status %s' % c)
			lContainerReady.add(c)
		if len(lContainerReady) == len(lContainerToWaitFor):
			break
		if i == 0:
			raise Exception('Timeout while waiting for container(s) %s, abort' % ', '.join(lContainerToWaitFor))
		else:
			print('Waiting for container(s) %s' % ', '.join(lContainerToWaitFor))
			time.sleep(10.)

def __installRedists(installation):
	print("Installing redistributables")
	for (exename,option) in installation._redists :
		cmd = [os.path.normpath(os.path.join(installation._Djuump_installers,'third-party','vcresdists',exename)),option]
		(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
		if errorCode != 0 :
			print("Error installing %s" % exename)
	

def prepareSystem(installation):
	if sys.platform == 'win32' :
		__installRedists(installation)
	else :
		if installation.linux_type == 'debianbase' and (installation.linux_distribution == 'bookworm' or installation.linux_distribution == 'trixie'):
			if installation.linux_distribution == 'bookworm':
				print('Debian 12 (bookworm) support is now deprecated, consider upgrading your system to Debian 13 (trixie).')
				if not askYesNoInput('Continue deployment on Debian 12 (bookworm)',True):
					sys.exit(1)
			__configureDebianSystem(installation)
		else:
			raise Exception('Non docker install is only supported on this system')


class commonInstallationInfos :

	def __init__(self) :
		
		self._postgresl_cluster_name = 'main'
		self._postgresl_installer = 'postgresql-' + os.environ['SV_VERSION_PG_WIN_INSTALLER'] + '-windows-x64.exe'
		self._postgresl_version = os.environ['SV_VERSION_PG_SMALL']
		self._plugin_installer = '3DJuump Infinite Postgres Plugins x64-setup-'
		self._java_installer = 'openjdk-' + os.environ['SV_VERSION_JRE'] + '-windows-x64.zip'
		self._elasticsearch_installer = 'elasticsearch-' + os.environ['SV_VERSION_ES_FULL'] + '-windows-x86_64.zip'
		self._eseditor_installer = '3djuumpInfiniteEsEditorHtml_*.zip'
		self._redists = [('vcredist_x64_v140','/quiet')]
		
		# where install folder is located
		self._Djuump_installers = ''
		self._install_apache = True
		self._install_postgres = True
		
		self._install_elastic = True
		self._run_apt_upgrade = False
		self._ssl_folder_pg = ''
		self._certificate_file_pg = ''
		self._privatekey_file_pg = ''
		
		self._previous_win_postgres_folder = ''
		self._previous_postgres_version = ''
		self._previous_win_postgres_data_folder = ''
		self._first_linux_binary_install = True
		
		self._httpd_conf_custom_includes = []
		self._httpd_infinite_conf_custom_includes = ''
		
		if sys.platform != "win32" :
			self.getLinuxDistribution()
		self._Djuump_installers = os.path.normpath(os.path.abspath('..')).replace('\\','/')
	
		print('Options :\n\t--accept-eula : to skip eula consent\n\t-f to force other consents')
	
	def isDockerInstall(self):
		return self.general.docker_install != False
	
	def isDirectoryInstall(self):
		return hasattr(self,'directory') and hasattr(self.directory,'directory_api_local_port')

	def isProxyInstall(self):
		return hasattr(self,'proxy') and hasattr(self.proxy,'proxy_api_local_port')
	
	def getLinuxDistribution (self) :
		# try for debian/ubuntu
		cmd = ['lsb_release','-a']
		(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
		reg =r'.*Codename:\s*([^\s]+).*'
		reg = re.compile(reg)
		result = reg.search(stdout)
		if not result is None and errorCode == 0:
			self.linux_distribution = result.group(1)
			self.linux_type = 'debianbase'
			return
			
		# try for redhat
		cmd = ['cat','/etc/redhat-release']
		(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
		if errorCode == 0:
			self.linux_distribution = stdout
			self.linux_type = 'redhat'
			return
		
		print("Cannot get distribution codename, is lsb-release installed ? Or os compatible ?")
		print("exiting")
		sys.exit(1)

	def installElasticSearch(self):
		if sys.platform == 'win32' :
			if not self._install_elastic :
				print("Skipping elastic search installation")
			else :
				# remove old version if required
				(javafolder,elasticfolder) = getElasticSearchFolders()
				if elasticfolder != '':
					print("Stoping ElasticSearch service")
					stopService('elasticsearch-service-x64')
					time.sleep(2)
					print("Removing ElasticSearch")
					uninstallService('elasticsearch-service-x64')
					time.sleep(2)
					goon = True
					while goon :
						try :
							if os.path.exists(elasticfolder):
								shutil.rmtree(elasticfolder)
							goon = False
						except:
							time.sleep(1)
					
				if javafolder != '':
					print("Removing Java")
					shutil.rmtree(javafolder)
			
				print("Extracting java")
				zfile = zipfile.ZipFile(os.path.join(self._Djuump_installers,'third-party',self._java_installer))
				zfile.extractall(self.java_binary_folder)
				
				print("Extracting elasticsearch")
				zfile = zipfile.ZipFile(os.path.join(self._Djuump_installers,'third-party',self._elasticsearch_installer))
				for zinfo in zfile.infolist():
					dst = self.elastic_binary_folder + zinfo.filename.replace('elasticsearch-' + os.environ['SV_VERSION_ES_FULL'],'',1)
					if zinfo.filename.endswith('/'):
						os.makedirs(dst,exist_ok=True)
					else :
						dstFolder = os.path.split(dst)[0]
						os.makedirs(dstFolder,exist_ok=True)
						content = zfile.read(zinfo)
						with open(dst,"wb") as handle:
							handle.write(content)
				zfile.close()
		else :
			if self.proxy.linux_oracle_java != True:
				java_package = 'default-jre'
				# upgrade if necessary
				installPackage(java_package)
			# upgrade if necessary
			installPackage('elasticsearch')
			# register to launch on startup
			cmd=['update-rc.d','elasticsearch','defaults','95','10']
			shellExecExceptOnError(cmd,True)
			cmd=['systemctl','enable','elasticsearch']
			shellExecExceptOnError(cmd,True)
		
		# patch jvm.options here, before registering the service
		if sys.platform == 'win32' :
			jvmoptfile = os.path.join(self.elastic_binary_folder,'config','jvm.options')
		else :
			jvmoptfile = os.path.join('/etc/elasticsearch/jvm.options')
		bakupFile = os.path.join(os.getcwd(),os.path.split(jvmoptfile)[1]+".bak")
		if not os.path.exists(bakupFile):
			shutil.copy(jvmoptfile,bakupFile)
		
		newcontent = ''
		with open(jvmoptfile,"r",encoding='UTF-8') as handle:
			content = handle.read()
		lOriginalContent = content
		
		
		content = content.replace('-Xms1g','-Xms%dm' % self.proxy.elastic_memory_MB)
		content = content.replace('-Xmx1g','-Xmx%dm' % self.proxy.elastic_memory_MB)
		
		content = content.replace('8:-XX:+PrintGCDetails','8:-XX:-PrintGCDetails')
		content = content.replace('8:-XX:+PrintGCApplicationStoppedTime','8:-XX:-PrintGCApplicationStoppedTime')
		
		if content != lOriginalContent:
			with open(jvmoptfile,"w",encoding='UTF-8') as handle:
				handle.write(content)
			
		os.makedirs(jvmoptfile + '.d', exist_ok=True)
		
		with open(jvmoptfile + '.d/heap.options',"w",encoding='UTF-8') as handle:
			handle.write('-Xms%dm\n-Xmx%dm' % (self.proxy.elastic_memory_MB,self.proxy.elastic_memory_MB))
		
		if sys.platform != 'win32':
			cmds = [
				['chown','root:elasticsearch',jvmoptfile + '.d'],
				['chmod','770',jvmoptfile + '.d'],
				['chown','root:elasticsearch',jvmoptfile + '.d/heap.options'],
				['chmod','660',jvmoptfile + '.d/heap.options'],
				]
			for cmd in cmds :
				shellExecExceptOnError(cmd)
		
		
	def patchElasticSearch(self):
		print("Patching elasticsearch")
		
		if sys.platform == 'win32':
			src = os.path.join(self.elastic_binary_folder,'bin','elasticsearch-service.bat')
			dst = os.path.join(self.elastic_binary_folder,'bin','service.bat')
			
			java_home_folder = os.path.join(self.java_binary_folder,'jdk-' + os.environ['SV_VERSION_JRE']);
			
			content = '@echo off\n'
			content = content + 'set ES_JAVA_HOME=' + java_home_folder + '\n'
			with open(src,"r",encoding='UTF-8') as handle:
				content = content + handle.read()
			with open(dst,"w",encoding='UTF-8') as handle:
				handle.write(content)
			
			
			stopService('elasticsearch-service-x64')
			
			cmd = ['sc','delete','elasticsearch-service-x64','/Q']
			shellExecExceptOnError(cmd,True)
			
			cmd = ['sc','query','elasticsearch-service-x64']
			while True:
				(stdout,stderr,errorCode) = shellExecExceptOnError(cmd,True)
				if errorCode != 0:
					break;
				time.sleep(1)
				
			
			print('Install elasticsearch-service-x64')
			cmd = [os.path.join(self.elastic_binary_folder,'bin','service.bat'),'install']
			shellExecExceptOnError(cmd)
			
			# service.bat keep %ES_JAVA_HOME% in settings 
			# we don't want to set it system wide, so we tweak reg entry were it is stored to replace it
			import winreg as reg
			
			lKeyName = r'SOFTWARE\Wow6432Node\Apache Software Foundation\Procrun 2.0\elasticsearch-service-x64\Parameters\Java\\'

			lRegKey = reg.OpenKey(reg.HKEY_LOCAL_MACHINE,lKeyName,0,reg.KEY_WRITE)
			lJvmDllPath = os.path.join(java_home_folder,r'bin\server\jvm.dll')
			reg.SetValueEx(lRegKey,'Jvm',0,reg.REG_SZ,lJvmDllPath)
			reg.CloseKey(lRegKey)
			
			cmd = ['sc','config','elasticsearch-service-x64','start=','auto' ]
			shellExecExceptOnError(cmd)
			
	def configureElasticSearch(self):
		import requests
		
		print("Configuring elasticsearch")
		
		if sys.platform == 'win32' :
			src = os.path.join(self.elastic_binary_folder,'config','log4j2.properties')
			serviceName = 'elasticsearch-service-x64'
		else :
			cmd= ['usermod','-a','-G',self.owner_grp,'elasticsearch']
			shellExecExceptOnError(cmd)
			
			src = os.path.join('/etc/elasticsearch/log4j2.properties')
			serviceName = 'elasticsearch'
			
		dst = src
		
		copyAndConfigure(self,src,dst,[
				(re.compile('\\s*rootLogger\\.level\\s*=.*'),'rootLogger.level = error',True)
			])
		
		
		
		if sys.platform == 'win32' :
			dst = os.path.join(self.elastic_binary_folder,'config','elasticsearch.yml')
		else :
			dst = '/etc/elasticsearch/elasticsearch.yml'
		
		updateElasticSearchConfFile(dst,self)
	
		# don't now why sometimes restart will fail ...
		for i in range(5,-1,-1):
			try:
				restartService(serviceName, pIgnoreError = (i > 0),altName='elasticsearch' )
				break;
			except:
				if i == 0:
					raise Exception('Fail to restart elasticsearch, abort')
				else:
					print('Failed to restart elasticsearch, will retry in a few moment')
					time.sleep(10.)
					
		# wait for elastic search service
		if 'http_proxy' in os.environ :
			http_proxy=os.environ['http_proxy']
			del os.environ['http_proxy']
		else:
			http_proxy=None
		
		if 'https_proxy' in os.environ :
			https_proxy=os.environ['https_proxy']
			del os.environ['https_proxy']
		else:
			https_proxy=None

		for i in range(10,-1,-1):
			lUrl = 'http://127.0.0.1:' + str(self.proxy.elastic_port)
			lResponse = None
			try:
				lResponse = requests.get(lUrl)
			except:
				lResponse = None
			if lResponse is None or lResponse.status_code != 200:
				if i == 0:
					if lResponse is None:
						raise Exception('elasticsearch is not responding, abort')
					else:
						raise Exception('elasticsearch return %s : %s' % (lResponse.status_code, lResponse.text))
				else:
					print('elasticsearch is not responding, will retry in a few moment')
					time.sleep(10.)
		if not http_proxy is None :
			os.environ['http_proxy']=http_proxy
		
		if not https_proxy is None :
			os.environ['https_proxy']=https_proxy
		print('Elasticsearch is ready')
	
	def createUpdateScript(self, pSourceForm):
		lFolder = os.path.dirname( os.path.realpath( __file__ ) )
		with open(os.path.join(lFolder,'scripts','update.py.tpl'),'r',encoding='utf8') as fd:
			lTplContent = fd.read()
		lTplContent = lTplContent.replace('%%PYTHON_PATH%%', '!./python-embeded-win/python.exe' if (sys.platform == 'win32') else '!/usr/bin/env python3')
		lTplContent = lTplContent.replace('%%VERSION%%',os.environ['SV_VERSION_INFINITE_FULL'])
		lTplContent = lTplContent.replace('%%INSTALL_FORM_PATH%%',os.path.realpath(pSourceForm))
		lTplContent = lTplContent.replace('%%INSTALL_PACKAGE_FOLDER%%',os.path.realpath(os.path.join(lFolder,'../..')))
		lScriptPath = os.path.realpath(os.path.join(getattr(self.general,'install_basepath'),'update.py'))
		
		with open(lScriptPath,'w',encoding='utf8') as fd:
			fd.write(lTplContent)
			
		if sys.platform != 'win32':
			cmd = ['chmod','+x',lScriptPath]
			shellExecExceptOnError(cmd)
		
	
	def fromYaml(installation, src,srcschema):
		import urllib, uuid
		try:
			import yaml
			import jsonschema
			import requests
		except:
			raise Exception('need following modules : PyYaml jsonschema requests')
			
		if not os.path.exists(src) :
			raise Exception("File %s does not exist, exiting" % src)
		
		# load configuration
		lConf = None
		with open(src,encoding='utf8') as f:
			lConf = yaml.safe_load(f)
		lFormFolderPath = os.path.split(os.path.abspath(src))[0]

		# convert into json (convert all keys into string)
		lConf = json.loads(json.dumps(lConf))
		
		# load validation schema
		lSchema = None
		with open(srcschema,encoding='utf8') as f:
			lSchema = yaml.safe_load(f)
		
		# validate configuration
		try:
			jsonschema.validate(instance=lConf,schema=lSchema)
		except jsonschema.exceptions.ValidationError as err:
			print('in section : ''%s''' % ('/'.join(err.path)))
			print('\t %s' % (err.message))
			lConf = None
		except:
			raise
		if lConf is None:
			raise Exception('Invalid install configuration')
		
		# parse configuration
		for section in lConf:
			
			lSubConf = lConf[section]
			
			if section == 'openidconnect':
				installation.openidconnect = lSubConf
				continue
			
			lDst = type('', (), {})()
			for key in lSubConf:
				lValue = lSubConf[key]
				if lValue is None:
					continue
				# if field is a file name, replace it with absolute path because installers 
				# might use a different execution folder
				if isinstance(lValue, str) and (os.path.isfile(lValue) or os.path.isdir(lValue)):
					lValue = os.path.abspath(lValue)
				if hasattr(installation,key):
					raise Exception('Got attribute conflict while parsing section %s : %s' % (section,key))
				setattr(lDst, key, lValue)
			setattr(installation,section,lDst)
		
		# resolve relative path
		for e in ['install_basepath','binary_basepath','provided_certificate_file','provided_privatekey_file']:
			if not hasattr(installation.general,e):
				continue
			lValue = getattr(installation.general,e)
			if lValue is None or lValue == '':
				continue
			if os.path.isabs(lValue):
				continue
			lResolvedPath = os.path.join(lFormFolderPath,lValue)
			setattr(installation.general,e,lResolvedPath)
			print('Resolve relative path value of %s from "%s" to "%s"' % (e,lValue,lResolvedPath))
		
		# create destination folders and if they exists ensure that path matches (case issue on windows)
		for e in ['install_basepath','binary_basepath']:
			if not hasattr(installation.general,e):
				continue
			lValue = getattr(installation.general,e)
			if lValue is None or lValue == '':
				continue
			os.makedirs(lValue,exist_ok=True)
			lRealPath = os.path.normpath(os.path.realpath(lValue))
			lSpecifiedPath = os.path.normpath(lValue)
			if lRealPath != lSpecifiedPath :
				raise Exception('%s path (%s) is not strictly equal to real path (%s), check for case issue' % (e,lSpecifiedPath,lRealPath))
		
		if installation.general.public_url.endswith('/directory') or installation.general.public_url.endswith('/proxy'):
			raise Exception('public_url should not ends with /directory or /proxy')
		if hasattr(installation.general,'backend_url') and (installation.general.backend_url.endswith('/directory') or installation.general.backend_url.endswith('/proxy')):
			raise Exception('backend_url should not ends with /directory or /proxy')
		
		# extract user and group name
		if sys.platform != 'win32' :
			(usr,grp) = installation.general.user.split(':')
			if not installation.isDockerInstall():
				setattr(installation,'owner_user',usr)
			else:
				setattr(installation,'owner_user',None)
			setattr(installation,'owner_grp',grp)
		
		# compute publichostname
		lPublicUrl = urllib.parse.urlparse(installation.general.public_url)
		installation.publichostname = lPublicUrl.hostname
		if installation.isDockerInstall():
			installation.docker_project_name = re.sub('[^a-z0-9_-]','-',installation.general.public_url).lower()
			# generate a random password for elasticsearch
			installation.elastic_password = str(uuid.uuid4())
			if installation.general.docker_use_minio:
				installation.minio_login = str(uuid.uuid4())
				installation.minio_password = str(uuid.uuid4())
		if installation.isProxyInstall():
			setattr(installation,'_elastic_memory_MB_plus_delta',installation.proxy.elastic_memory_MB+1024)
			setattr(installation,'_elastic_cpu_limit',os.cpu_count()*0.7)

		# check for empty attributes
		allItems = getAttributes(installation)
		for curAttr in allItems :
			if str(getattr(installation, curAttr)) == '':
				should_exit = True
				if should_exit:
					print("attribute %s cannot be empty, exiting" % curAttr)
					sys.exit(1)
		
		# apply proxy to current env for http calls made from this script
		if hasattr(installation.general,'http_proxy') and installation.general.http_proxy != None:
			os.environ['http_proxy'] = installation.general.http_proxy
		if hasattr(installation.general,'https_proxy') and installation.general.https_proxy != None:
			os.environ['https_proxy'] = installation.general.https_proxy
		
		print('http_proxy : %s' % (os.environ['http_proxy'] if 'http_proxy' in os.environ else 'n/a'))
		print('https_proxy : %s' % (os.environ['https_proxy'] if 'https_proxy' in os.environ else 'n/a'))
		
		if hasattr(installation.general,'binary_basepath') and (installation.general.binary_basepath != '' or installation.general.binary_basepath != None):
			if sys.platform != 'win32' or installation.isDockerInstall():
				raise Exception(' ''binary_basepath'' should be empty for linux/docker deployments')
		else:
			if sys.platform == 'win32' and not installation.isDockerInstall():
				raise Exception(' ''binary_basepath'' could not be empty')
		
		# ensure that some parameters do not contains special characters that cause issues later
		# $ => will cause issues in docker compose files and .env
		for p in ['postgres_login','postgres_password']:
			lValue = getattr(installation.postgres,p)
			if '$' in lValue:
				raise Exception('%s should not contain "$" character' % p)
		
		# some system specific checks
		if sys.platform != 'win32' and not installation.isDockerInstall():
			installation.general.binary_basepath = '/'
			if not hasattr(installation.general,'juump_apt') or installation.general.juump_apt == '':
				raise Exception(' ''juump_apt'' could not be empty')
			lReRes = re.match(r'.*\/(.*)\/infinite\/[0-9]+.[0-9]+\/?',installation.general.juump_apt)
			if lReRes is None:
				raise Exception('Fail to analyze apt url')
			lPackagesUrl = installation.general.juump_apt
			if lPackagesUrl[-1] != '/':
				lPackagesUrl = lPackagesUrl + '/'
			lPackagesUrl = lPackagesUrl + 'dists/' + lReRes.group(1) + '/main/binary-amd64/Packages'
			
			packagelist = getFile(lPackagesUrl)
			
			lReRes = re.match(r'.*Package: lib3djuump-infinite-cli.*?Version: ([0-9]+.[0-9]+.[0-9]+.[0-9]+).*',packagelist.decode('utf-8'),re.MULTILINE | re.DOTALL)
			if lReRes is None:
				raise Exception('Fail to retrieve version number from apt server')
			
			if not lReRes.group(1).startswith(os.environ['SV_VERSION_INFINITE']):
				raise Exception('This install package version (%s) is different from the package version on apt (%s)' % (os.environ['SV_VERSION_INFINITE'],lReRes.group(1)))

		
		# generate apache configuration based on public url
		setattr(installation,'apache_https_port',lPublicUrl.port)
		setattr(installation,'apache_path_prefix',lPublicUrl.path)
		setattr(installation,'apache_has_proxy_define','Define' if installation.isProxyInstall() else 'UnDefine')
		setattr(installation,'apache_has_directory_define','Define' if installation.isDirectoryInstall() else 'UnDefine')
		setattr(installation,'apache_rotate_log', './bin/rotatelogs.exe' if sys.platform == 'win32' else '/usr/bin/rotatelogs')
		lApacheServerNameAndAlias = 'ServerName %s\n' % (urllib.parse.urlparse(installation.general.public_url).hostname)
		if hasattr(installation.general,'backend_url'):
			lApacheServerNameAndAlias = lApacheServerNameAndAlias + '\tServerAlias %s\n' % (urllib.parse.urlparse(installation.general.backend_url).hostname)
		setattr(installation,'apache_server_and_alias',lApacheServerNameAndAlias)
		  
		if installation.apache_path_prefix == '/':
			installation.apache_path_prefix=''
		installation.apache_path_prefix_regexp = installation.apache_path_prefix.replace('/','\\/')
		setattr(installation,'getgenericerror_base_url',urllib.parse.urlparse(installation.general.public_url + ('/directory' if not hasattr(installation,'proxy')  else '/proxy')).path)
		

		# define attributes based on what service we are installing
		if installation.isDirectoryInstall():
			if sys.platform == 'win32' :
				installation.directory_exe_name = '3dJuumpInfiniteDirectoryService.exe'
				installation.directory_binary_installer = '3DJuump Infinite Directory x64-setup-'
			else:
				installation.directory_exe_name = '3dJuumpInfiniteDirectoryService'
				installation.directory_binary_installer = '3djuump-infinite-directory'
		
		lDefaultProxyBinaryFolder = None
		if installation.isProxyInstall():
			lDefaultProxyBinaryFolder = '3DJuumpInfiniteProxyx64'
			if sys.platform == 'win32' :
				installation.proxy_exe_name = '3dJuumpInfiniteProxyService.exe'
				installation.proxy_binary_installer = '3DJuump Infinite Proxy x64-setup-'
			else:
				installation.proxy_exe_name = '3dJuumpInfiniteProxyService'
				installation.proxy_binary_installer = '3djuump-infinite-proxy'
		
		# build inferred path
		lInferredPath = [
			('ssl_folder', 'install_basepath' ,'ssl'),
			('apache_log_folder', 'install_basepath' ,'www/logs'),
			('apache_empty_root_folder', 'install_basepath' ,'www/empty/public'),
			
		]

		if not installation.isDockerInstall():
			lInferredPath = lInferredPath + [
				('apache_folder','binary_basepath','Apache'),
				('postgres_data_folder', 'install_basepath' ,'postgresql'),
			]

			if sys.platform == 'win32' :
				lInferredPath = lInferredPath + [
					('postgres_folder','binary_basepath','PostgreSQL'),
				]

		if installation.isDirectoryInstall():
			if installation.isDockerInstall():
				lInferredPath = lInferredPath + [
					('directory_log_folder', 'install_basepath' ,'logs'),
				]
			else:
				lInferredPath = lInferredPath + [
					('directory_log_folder', 'install_basepath' ,'service/logs'),
					('directory_filer_folder', 'install_basepath' ,'service/filer'),
				]
				if sys.platform == 'win32':
					lInferredPath = lInferredPath + [
						('directory_binary_folder','binary_basepath','3DJuumpInfiniteDirectoryx64'),
					]

		if installation.isProxyInstall():
			if installation.isDockerInstall():
				lInferredPath = lInferredPath + [
					('proxy_log_folder', 'install_basepath' ,'logs'),
				]
			else:
				lInferredPath = lInferredPath + [
					('proxy_log_folder', 'install_basepath' ,'service/logs'),
					('elastic_data_folder', 'install_basepath' ,'elasticsearch/data'),
					('elastic_log_folder', 'install_basepath' ,'elasticsearch/logs'),
					('proxy_data_folder', 'install_basepath' ,'service/proxy'),
				]
				if sys.platform == 'win32':
					lInferredPath = lInferredPath + [
						('elastic_binary_folder','binary_basepath','ElasticSearch'),
						('proxy_binary_folder','binary_basepath',lDefaultProxyBinaryFolder),
						('infinite_postgres_plugins_folder','binary_basepath','3DJuumpInfinitePostgresPluginsx64'),
						('java_binary_folder','binary_basepath','Java'),
					]

		setattr(installation,'inferred_path',[])
		for (attrName,base,suffix) in lInferredPath :
			setattr(installation,attrName,os.path.join(getattr(installation.general,base),suffix).replace('\\','/'))
			installation.inferred_path.append(getattr(installation,attrName))
			
		if hasattr(installation.general,'provided_certificate_file') != hasattr(installation.general,'provided_privatekey_file'):
			raise Exception('inconsistent provided_certificate_file/provided_privatekey_file')
		
		if hasattr(installation.general,'provided_certificate_file'):
			if not os.path.exists(installation.general.provided_privatekey_file):
				raise Exception('file missing : %s' % (installation.general.provided_privatekey_file))
			if not os.path.exists(installation.general.provided_certificate_file):
				raise Exception('file missing : %s' % (installation.general.provided_certificate_file))
		installation.certificate_file = os.path.join(installation.ssl_folder,'server.crt')
		installation.privatekey_file = os.path.join(installation.ssl_folder,'private.key')
		
		setattr(installation,'mTLS_define','UnDefine')
		setattr(installation,'mTLS_root_crt','n/a')
		installation.mTLS_root_ca=None
		if hasattr(installation.general,'provided_mTLS_root_ca'):
			if not hasattr(installation.general,'provided_certificate_file'):
				raise Exception('to enable mTLS a certificate should be provided')
			if not os.path.exists(installation.general.provided_mTLS_root_ca):
				raise Exception('file missing : %s' % (installation.general.provided_mTLS_root_ca))
			setattr(installation,'mTLS_define','Define')
			installation.mTLS_root_ca = os.path.join(installation.ssl_folder,'mTLSRootCa.crt')
			setattr(installation,'mTLS_root_crt',installation.mTLS_root_ca)
		
		
