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:" def receive_file(self, sMessage): aMessageParts = sMessage.split(":") iBytesToReceive = int(aMessageParts[2]) 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") #check whether the file with this exact size already exists if os.path.isfile(Fullpath+Filename) and os.stat(Fullpath+Filename).st_size==iBytesToReceive: 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()