Select Git revision
VirtualAcousticsStarterServer.py
VirtualAcousticsStarterServer.py 17.37 KiB
import sys, socket, subprocess, time, os, json
from enum import Enum
from configparser import ConfigParser
class ErrorCodes(Enum):
NO_ERROR = 0
ERROR_NO_CONFIG_FOUND = 1
ERROR_INCOMPLETE_CONFIG = 2
ERROR_BINDING_SOCKET = 3
ERROR_CONNECTING_SOCKET = 4
ERROR_MISSING_VA_INI = 5
ERROR_INVALID_REPRODUCTION_ID = 6
ERROR_INCOMPLETE_VA_INI = 7
ERROR_UNDEFINED_LAUNCHER_STATE = 8
class ReproductionInput(Enum):
NOT_SPECIFIED = 0
BINAURAL = 1
AMBISONICS = 2
CUSTOM = 3
# Class representing the VA-Launcher config (.json) file
class LauncherConfig:
def __init__(conf, sConfigFile):
try:
with open( sConfigFile ) as json_file:
json_config = json.load(json_file)
except Exception as e:
print( "ERROR reading the json config:" + os.linesep + str(e) )
sys.exit( ErrorCodes.ERROR_INCOMPLETE_CONFIG )
try:
conf.dVirtualAcousticDirectories = json_config["dVAServerDirectories"]
conf.sLocalIP = json_config["sLocalIP"]
conf.nLauncherPort = json_config["nLauncherPort"]
conf.nVAServerPort = json_config["nVAServerPort"]
conf.nWaitForVAServerStart = json_config["nWaitForVAServerStart"]
except KeyError as e:
print( "ERROR reading the json config. Missing " + str(e.args[0]) )
sys.exit( ErrorCodes.ERROR_INCOMPLETE_CONFIG )
try:
conf.sVASetupIni = json_config["sVASetupIni"]
except KeyError:
conf.sVASetupIni = None
try:
conf.lsBinauralReproductionModules = json_config["lsBinauralReproductionModules"]
except KeyError:
conf.lsBinauralReproductionModules = None
try:
conf.lsAmbisonicsReproductionModules = json_config["lsAmbisonicsReproductionModules"]
except KeyError:
conf.lsAmbisonicsReproductionModules = None
try:
conf.lsCustomReproductionModules = json_config["lsCustomReproductionModules"]
except KeyError:
conf.lsCustomReproductionModules = None
class VAComposedIniParser:
def __init__(self, oLauncherConf : LauncherConfig):
self.sVASetupIni = oLauncherConf.sVASetupIni
self.lsBinauralReproductionModules = oLauncherConf.lsBinauralReproductionModules
self.lsAmbisonicsReproductionModules = oLauncherConf.lsAmbisonicsReproductionModules
self.lsCustomReproductionModules = oLauncherConf.lsCustomReproductionModules
self.eReproductionInput = ReproductionInput.NOT_SPECIFIED
self.sRendererIniPath = None
self.sConfFolder = "../conf/"
def _create_parser(self):
parser = ConfigParser()
parser.optionxform = str
return parser
def get_main_inifile(self):
return self.sConfFolder + "VACore.Main.ini"
def prepare_inis(self):
print("Parsing VA ini-files")
self.link_setup_and_renderer_ini()
return self.prepare_reproduction_ini()
def link_setup_and_renderer_ini(self):
if not self.sRendererIniPath:
print("No renderer ini-file sent by client, using default one: 'VARenderer.Default.ini'")
self.sRendererIniPath = "VARenderer.Default.ini"
if not self.sVASetupIni:
print("No setup ini-file specified, using default one: 'VASetup.Launcher.ini'")
self.sVASetupIni = "VASetup.Launcher.ini"
sMainInifile = self.get_main_inifile()
if not os.path.isfile(sMainInifile):
print("ERROR: Could not find 'VACore.Main.ini' in 'conf' folder. Did you delete it?")
sys.exit( ErrorCodes.ERROR_MISSING_VA_INI )
mainIni = self._create_parser()
try:
mainIni.read( sMainInifile )
mainIni["Files"]["VARendererIni"] = self.sRendererIniPath
mainIni["Files"]["VASetupIni"] = self.sVASetupIni
except Exception as e:
print( "ERROR: " + str(e) )
sys.exit( ErrorCodes.ERROR_INCOMPLETE_VA_INI )
with open( sMainInifile, 'w' ) as inifile:
mainIni.write(inifile)
def prepare_reproduction_ini(self):
sFileToRead = self.sConfFolder + "VAReproduction.Prototype.ini"
sFileToWrite = self.sConfFolder + "VAReproduction.ini"
if not os.path.isfile(sFileToRead):
print("ERROR: Could not find 'VAReproduction.Prototype.ini' in 'conf' folder. Did you delete it?")
sys.exit( ErrorCodes.ERROR_MISSING_VA_INI )
reproductionIni = self._create_parser()
try:
reproductionIni.read( sFileToRead )
except Exception as e:
print( "ERROR: " + str(e) )
sys.exit( ErrorCodes.ERROR_INCOMPLETE_VA_INI )
if self.eReproductionInput == ReproductionInput.BINAURAL:
if self.lsBinauralReproductionModules:
lsReproductionModuleIDs = self.lsBinauralReproductionModules
else:
print("ERROR: Requested BINAURAL reproduction modules are not specified")
return False
elif self.eReproductionInput == ReproductionInput.AMBISONICS:
if self.lsAmbisonicsReproductionModules:
lsReproductionModuleIDs = self.lsAmbisonicsReproductionModules
else:
print("ERROR: Requested AMBISONICS reproduction modules are not specified")
return False
elif self.eReproductionInput == ReproductionInput.CUSTOM:
if self.lsCustomReproductionModules:
lsReproductionModuleIDs = self.lsCustomReproductionModules
else:
print("ERROR: Requested CUSTOM reproduction modules are not specified")
return False
for sReproductionID in lsReproductionModuleIDs:
sSection = "Reproduction:" + sReproductionID
if not reproductionIni.has_section(sSection):
print( "ERROR: Reproduction module with ID: '" + sReproductionID + "' not available in respective ini file '" + sFileToRead + "'")
#sys.exit( ErrorCodes.ERROR_INVALID_REPRODUCTION_ID )
return False
reproductionIni[sSection]["Enabled"] = "True"
with open( sFileToWrite, 'w' ) as inifile:
reproductionIni.write(inifile)
return True
#Main class representing the VA Launcher app
class VirtualAcousticsLauncher:
def __init__(self):
print("init")
self.oConfig = None
self.vaIniParser = None
self.oVAProcess = None
self.oLauncherServerSocket = None
self.oLauncherConnection = None
self.sCurrentScriptsDirectory = os.path.dirname( os.path.realpath( sys.argv[0] ) )
self.sVAServerID = None
self.sVAServerDir = None
self.start()
#Start the launcher
def start(self):
print( "VirtualAcoustics Starter script - press ctrl+c to quit" )
self.read_config()
self.open_server_socket()
self.main_loop()
#Reads the launcher config and initializes all respective class parameters
def read_config(self):
sHostConfigurationFile = "VirtualAcousticStarterConfig." + socket.gethostname() + ".json"
sGeneralConfigurationFile = "VirtualAcousticStarterConfig.json"
#check which config file exists and load it
sUsedConfigFile = ""
if os.path.isfile( sHostConfigurationFile ):
sUsedConfigFile = sHostConfigurationFile
elif os.path.isfile( self.sCurrentScriptsDirectory + "/" + sHostConfigurationFile ):
sUsedConfigFile = self.sCurrentScriptsDirectory + "/" + sHostConfigurationFile
elif os.path.isfile( sGeneralConfigurationFile ):
sUsedConfigFile = sGeneralConfigurationFile
elif os.path.isfile( self.sCurrentScriptsDirectory + "/" + sGeneralConfigurationFile ):
sUsedConfigFile = self.sCurrentScriptsDirectory + "/" + sGeneralConfigurationFile
else:
print( "ERROR: No configuration file found - please create " + sHostConfigurationFile + " or " + sGeneralConfigurationFile )
sys.exit( ErrorCodes.ERROR_NO_CONFIG_FOUND )
print("Using config: " + sUsedConfigFile)
self.oConfig = LauncherConfig( sUsedConfigFile )
self.vaIniParser = VAComposedIniParser(self.oConfig)
#Open network socket used for the communication
def open_server_socket(self):
print( "Creating server socket at " + self.oConfig.sLocalIP + ":" + str( self.oConfig.nLauncherPort ) )
self.oLauncherServerSocket = socket.socket()
if self.oConfig.sLocalIP == "":
self.oConfig.sLocalIP = socket.gethostname()
try:
self.oLauncherServerSocket.bind( ( self.oConfig.sLocalIP, self.oConfig.nLauncherPort ) )
self.oLauncherServerSocket.listen( 3 )
except socket.error:
print( "Error on binding socket" )
sys.exit( ErrorCodes.ERROR_BINDING_SOCKET )
self.oLauncherServerSocket.settimeout( 1.0 )
def _reset_connection(self):
if self.oLauncherConnection:
self.oLauncherConnection.close()
self.oLauncherConnection = None
self.sVAServerDir = None
self.sVAServerID = None
self.vaIniParser.sRendererIniPath = None
def _close_va_and_reset_connection(self):
if self.oVAProcess:
print( "Closing VA instance" )
self.oVAProcess.terminate()
time.sleep( 1 )
self.oVAProcess.kill()
self.oVAProcess = None
self._reset_connection()
def main_loop(self):
try:
while True:
if not self.oLauncherConnection:
self.wait_for_connection()
elif self.sVAServerDir and not self.oVAProcess:
self.start_va_server()
elif self.oVAProcess:
self.listen_for_requests()
else:
print("ERROR: Undefined state launcher state leading to infinite loop")
sys.exit( ErrorCodes.ERROR_UNDEFINED_LAUNCHER_STATE )
except KeyboardInterrupt:
print( "Caught keyboard interrupt, quitting" )
self._reset_connection()
def wait_for_connection(self):
print( "Waiting for launcher connection..." )
while not self.oLauncherConnection:
try:
self.oLauncherConnection, sAddress = self.oLauncherServerSocket.accept()
except socket.timeout:
self.oLauncherConnection = None
except socket.error:
print( "Error while listening for launcher connection" )
sys.exit( ErrorCodes.ERROR_CONNECTING_SOCKET )
except (KeyboardInterrupt, SystemExit):
raise #re-raising the received exception
print( "Connection received from " + sAddress[0] )
if self.oVAProcess:
print( "Closing current VA instance" )
self.oVAProcess.kill()
self.oVAProcess = None
self.receive_va_start_info()
if not self.vaIniParser.prepare_inis():
print( "Resetting launcher connection" )
self._reset_connection()
#Checks for a message containing the ID of the VAServer instance to be started and returns the respective VAServer directory
def receive_va_start_info(self):
try:
sMessage = self.oLauncherConnection.recv( 512 )
if type( sMessage ) is bytes:
sMessage = sMessage.decode( 'utf-8' )
if ":" not in sMessage: #VAServer ID, should be received last
self.sVAServerID = sMessage
elif sMessage.startswith("reproduction_input_type:"): #ReproductionInput type (Binaural / Ambisonics), optional
self.receive_reproduction_input(sMessage)
return self.receive_va_start_info()
elif sMessage.startswith("file:"): #VARenderer.ini file, optional
self.vaIniParser.sRendererIniPath = self.receive_file(sMessage)
return self.receive_va_start_info()
else:
lMessageParts = sMessage.split(":")
print("ERROR: Invalid message keyword '" + lMessageParts[0] + "' while receiving VA start info")
return False
print( "Received launch request for VAServer ID: " + self.sVAServerID )
except socket.error:
print( "ERROR: Socket error while reading VAServer ID" )
self._reset_connection()
return False
else:
try:
self.sVAServerDir = self.oConfig.dVirtualAcousticDirectories[self.sVAServerID]
except KeyError:
self.sVAServerDir = None
if not self.sVAServerDir:
print( 'Requested VA Instance "' + self.sVAServerID + '" not available' )
self.oLauncherConnection.send( b'f' ) #answer 'requested version not available
self._reset_connection()
return False
return True
def receive_reproduction_input(self, sMessage):
try:
lsMessageParts = sMessage.split(":")
sReproductionInput = lsMessageParts[1].upper()
self.vaIniParser.eReproductionInput = ReproductionInput[sReproductionInput]
except IndexError:
print("ERROR: Message for receiving reproduction input type was empty")
self.oLauncherConnection.send( b'fail' )
except ValueError:
print("ERROR: Invalid ID (case-insensitive) for reproduction input: '" + sReproductionInput + "'")
self.oLauncherConnection.send( b'fail' )
else: #send acceptance
self.oLauncherConnection.send( b'ack' )
#Starts the VAServer from given directory
def start_va_server(self):
# Check for VAServer.exe
sVAExecutableFile = "bin/VAServer.exe"
try:
if not os.path.isfile( sVAExecutableFile ):
if self.sVAServerDir and os.path.isfile( self.sVAServerDir + "/" + sVAExecutableFile ):
sVAExecutableFile = self.sVAServerDir + "/" + sVAExecutableFile
else:
print( "ERROR: Invalid config for " + self.sVAServerID + " -- file " + sVAExecutableFile + " does not exist" )
self.oLauncherConnection.send( b'n' ) #answer 'binary file cannot be found or invalid'
return
except KeyError:
sVAExecutableFile = None
print( "ERROR: config for " + self.sVAServerID + " has no valid \"file\" entry" )
self.oLauncherConnection.send( b'i' ) #answer 'invalid file entry in the config'
self._reset_connection()
return
if not sVAExecutableFile:
return
# Create start command
sConnectionParam = self.oConfig.sLocalIP + ":" + str( self.oConfig.nVAServerPort )
sVACoreIniParam = self.vaIniParser.get_main_inifile()
sParams = sConnectionParam + " " + sVACoreIniParam
sCommand = sVAExecutableFile + " " + sParams
# start instance
print( 'executing "' + sCommand + '"' )
self.oVAProcess = subprocess.Popen( sCommand, cwd = self.sVAServerDir, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP )
# wait for requested duration before sending the go signal
time.sleep( self.oConfig.nWaitForVAServerStart )
if self.oVAProcess.poll() != None:
print( "VA Process died - sending abort token" )
self.oLauncherConnection.send( b'a' ) #answer 'VAServer was aborted
self._reset_connection()
return
else:
print( "sending go token" )
self.oLauncherConnection.send( b'g' )#answer 'go, VAServer is correctly started'
#Listens for requests while the VAServer is running
def listen_for_requests(self):
while True:
try:
sResult = self.oLauncherConnection.recv( 512 )
if sResult != '':
#check whether we are about to reveive a file
sMessage = sResult.decode("utf-8")
if sMessage.startswith("file"):
self.receive_file(sMessage)
else: #NOT sMessage.startswith("file")
print( "Received quit event: " + sMessage )
self._close_va_and_reset_connection()
break
except socket.timeout:
# timeouts are okay
print( "Launcher Socket timeout, keep running anyways" )
except socket.error:
print( "Launcher Connection terminated unexpectedly" )
break
except (KeyboardInterrupt, SystemExit):
raise #re-raise for higher instance to catch
# Receives a file from a client, copies it to a tmp folder and returns the respective fullpath
# Input is a string message starting with "file:"
# it has the content file:[RelativePathToFile]:[FileLengthInBytes]:[ModificationTimeInSecondsSinceEPOCH]
def receive_file(self, sMessage):
aMessageParts = sMessage.split(":")
iBytesToReceive = int(aMessageParts[2])
iLastModificationTime = int(aMessageParts[3])
Path, Filename = os.path.split(aMessageParts[1])
ProjectName = aMessageParts[3]
Fullpath = os.path.join(self.sCurrentScriptsDirectory, "..", "tmp", ProjectName, Path, "")
print("Should receive file: "+Filename+" in path "+Fullpath+ " with "+str(iBytesToReceive)+" bytes")
iLocalFileLastModification = os.path.getmtime(Fullpath+Filename)
#check whether the file with this exact size (which is not older than the file to send) already exists
if os.path.isfile(Fullpath+Filename) and os.stat(Fullpath+Filename).st_size==iBytesToReceive and iLocalFileLastModification<iLastModificationTime:
self.oLauncherConnection.send( b'exists' )
print("File already exists with this size, so no need for resending")
else: #file need to be received
#create dir if it does not exist
if not os.path.exists(Fullpath):
os.makedirs(Fullpath)
#send acceptance
self.oLauncherConnection.send( b'ack' )
#receive file
iBytesReceived = 0
with open(Fullpath+Filename, "wb") as f:
bReceivingFile = True
while bReceivingFile:
# read 1024 bytes from the socket (receive)
bytes_read = self.oLauncherConnection.recv(1024)
if not bytes_read:
# nothing is received
# file transmitting is done
bReceivingFile = False
else:
# write to the file the bytes we just received
f.write(bytes_read)
iBytesReceived += len(bytes_read)
if iBytesReceived == iBytesToReceive:
bReceivingFile = False
f.close()
#check whether received file seems ok
if iBytesReceived == iBytesToReceive:
self.oLauncherConnection.send( b'ack' ) #send acceptance
print("File received successfully")
else:
self.oLauncherConnection.send( b'fail' ) #send failure
print("File receive failed")
return Fullpath+Filename
#create an instance of the class
oLauncher = VirtualAcousticsLauncher()