package INSTALL::caspeng; use INSTALL::os; use INSTALL::common; use INSTALL::global; use INSTALL::parameter; use INSTALL::services; use INSTALL::substitute; use INSTALL::query; use INSTALL::reboot; use INSTALL::ini; use POSIX; my $pGlobals = \%INSTALL::global::variables; my $pParameters = \%INSTALL::parameter::variables; my $pOS = \%INSTALL::os::Details; my $CONST_StartPriority = 86; my $CONST_StopPriority = 14; # For 3.6.1, multiprocess is to be disabled for all, as we do not support # multiprocess for any platform. # if ($pOS->{os} =~ /Linux/i) { # $disable_multiprocess=1; # } # else { # $disable_multiprocess=0; # } my $disable_multiprocess=1; #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # External functions #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #------------------------------------------------------------------------------ # This method returns the list of all known installed engines. #============================================================================== sub list_engines { my $pIni = INSTALL::ini::open($pGlobals->{installed_db}); if (!$pIni) { return(error("Could not open $pGlobals->{installed_db}.")); } return(INSTALL::ini::getSectionNames($pIni)); } #------------------------------------------------------------------------------ # This method is similar to list_engines except that instead of returning a # list of all of the engine directories, each engine's information (as depicted # in .installed_db) is returned via a list of hash references. Each hash # contains the key / value pairs as depicted in the .installed_db #============================================================================== sub load_engines { my $pIni = INSTALL::ini::open($pGlobals->{installed_db}); if (not $pIni) { return(error("Could not open $pGlobals->{installed_db}.")); } my @section_list = INSTALL::ini::getSectionNames($pIni); my @result; foreach(@section_list) { my $pSection = INSTALL::ini::openSection($pIni,$_); my %server_details = %{$pSection}; if (keys %server_details) { push @result,\%server_details; } } return(@result); } #------------------------------------------------------------------------------ # This method takes the key and value pair combination as input and looks for # these combinations in our installation database. If a suitable entry is found # in the database the corresponding ini reference is returned to the caller. # Once this routine is completed, it can be moved down to common.pm file #============================================================================== sub get_pSection_Installeddb { my ($key,$value) = (@_); my $pIniSection = undef; unless ($key && $value) { error(" a valid key and value pair comination is required to do the "); error(" name lookup from the ini database. "); return (undef); } my $pIni = INSTALL::ini::open($pGlobals->{installed_db}); if (!$pIni) { error("Could not open $pGlobals->{installed_db}."); return (undef); } my @section_names = INSTALL::ini::getSectionNames($pIni); foreach (@section_names) { $pIniSection = INSTALL::ini::openSection($pIni,$_); if (! $pIniSection) { error ("Unable to retrieve a valid section reference from $pGlobals->{installed_db}."); error ("It seems that your installation database file is corrupted."); return (undef); } elsif ( (exists $pIniSection->{$key}) && ($pIniSection->{$key} =~ /$value/) ) { # FIX ME: # before returning this section, we also need to validate # some of the required parameters to be found in the # installation database. return ($pIniSection); } } return (undef); } #------------------------------------------------------------------------------ # This method takes the hash reference to the ini section of the .installed_db # and uses this reference to locate the engine_dir and other engine related # parameters to find out whether the engine is running or stopped. # returns 0 #============================================================================== sub status_engine { (@_ == 1) || die "Invalid number of arguments."; my $pEngine = shift @_; unless (ref($pEngine) eq "HASH") { error ("This routine requires a hash reference to .installed_db section."); error ("start_engine called with an invalid parameter."); return (undef); } unless (-d "$pEngine->{engine_dir}") { error ("This routine requires a valid engine location."); return (undef); } my $engine_status_script; if ( (exists $pEngine->{engine_status_script}) && (-x "$pEngine->{engine_status_script}") ) { $engine_status_script = $pEngine->{engine_status_script}; } elsif ( -x "$pEngine->{engine_dir}/caspctrl" ) { $engine_status_script = "$pEngine->{engine_dir}/caspctrl -status"; } if (not $engine_status_script) { error_log("Unable to locate ASP Engine status scripts."); error("An error occurred while trying to retrieve ASP engine status."); return(undef); } if (system_log("$engine_status_script")) { # engine is NOT running currently. $result = 0; } else { # engine is running currently. $result = 1; } return ($result); } #------------------------------------------------------------------------------ # This method takes the hash reference to the ini section of the .installed_db # and uses this reference to locate the engine_dir and other engine related # parameters to perform the start/stop/restart jobs. # returns undef on error. #============================================================================== sub start_engine { (@_ == 1) || die "Invalid number of arguments."; my $pEngine = shift @_; unless (ref($pEngine) eq "HASH") { error ("This routine requires a hash reference to .installed_db section."); error ("start_engine called with an invalid parameter."); return (undef); } unless (-d "$pEngine->{engine_dir}") { error ("This routine requires a valid engine location."); return (undef); } # check for start scripts. my $engine_start_script; if ( (exists $pEngine->{engine_start_script}) && (-x "$pEngine->{engine_start_script}") ) { $engine_start_script = $pEngine->{engine_start_script}; } elsif ( -x "$pEngine->{engine_dir}/caspctrl" ) { $engine_start_script = "$pEngine->{engine_dir}/caspctrl -startall"; } if (! $engine_start_script) { error_log("Unable to locate ASP Engine start scripts."); error("An error occurred while starting $pEngine->{engine_name}."); return (undef); } # Before starting the engine, make sure it is stopped. my $running=status_engine($pEngine); if ($running) { # It seems that ASP Engine is running already. return 1; } if (system_log("$engine_start_script")) { error_log("An error occurred while starting $pEngine->{engine_name}."); note("You will need to manually restart $pEngine->{engine_name}."); return (undef); } else { return(1); } } #------------------------------------------------------------------------------ # This method takes the hash reference to the ini section of the .installed_db # and uses this reference to locate the engine_dir and other engine related # parameters to perform the start/stop/restart jobs. # returns undef on error. #============================================================================== sub stop_engine { (@_ == 1) || die "Invalid number of arguments."; my $pEngine = shift @_; unless (ref($pEngine) eq "HASH") { error ("This routine requires a hash reference to .installed_db section."); error ("start_engine called with an invalid parameter."); return (undef); } unless (-d "$pEngine->{engine_dir}") { error ("This routine requires a valid engine location."); return (undef); } # check for stop scripts. my $engine_stop_script; if ( (exists $pEngine->{engine_stop_script}) && (-x "$pEngine->{engine_stop_script}") ) { $engine_stop_script = $pEngine->{engine_stop_script}; } elsif ( -x "$pEngine->{engine_dir}/caspctrl" ) { $engine_stop_script = "$pEngine->{engine_dir}/caspctrl -stopall"; } if (! $engine_stop_script) { error_log("Unable to locate ASP Engine stop scripts."); error("An error occurred while stopping '$pEngine->{engine_name}'"); return (undef); } # Before stopping the engine, make sure it is stopped. my $running=status_engine($pEngine); if (! $running) { # It seems that ASP Engine is already stopped. return 1; } if (system_log("$engine_stop_script")) { error_log(" An error occurred while stopping $pEngine->{engine_name}."); note("You will need to manually stop $pEning->{engine_name}."); return (undef); } else { return(1); } } #------------------------------------------------------------------------------ # This method takes the hash reference to the ini section of the .installed_db # and uses this reference to locate the engine_dir and other engine related # parameters to perform the start/stop/restart jobs. # returns undef on error. #============================================================================== sub restart_engine { my ($pEngine) = (@_); unless (ref($pEngine) eq "HASH") { error ("This routine requires a hash reference to .installed_db section."); error ("start_engine called with an invalid parameter."); return (undef); } unless (-d "$pEngine->{engine_dir}") { error ("This routine requires a valid engine location."); return (undef); } # dont care about the engine stop status here. $result = stop_engine($pEngine); $result = start_engine($pEngine); return ($result); } #------------------------------------------------------------------------------ # This method takes the hash reference to the ini section of the .installed_db # and uses this reference to generate a start script for the associated # caspeng instance. #------------------------------------------------------------------------------ sub install_boot_script { my $pServerStats = shift @_; return(INSTALL::reboot::install_script ($pServerStats->{engine_name}, "$pServerStats->{engine_dir}/caspctrl -startall", "$pServerStats->{engine_dir}/caspctrl -stopall", "$pServerStats->{engine_dir}/caspctrl -status", $CONST_StartPriority, $CONST_StopPriority)); } #------------------------------------------------------------------------------ # This method takes the hash reference to the ini section of the .installed_db # and uses this reference to generate a start script for the associated # caspeng instance. #------------------------------------------------------------------------------ sub uninstall_boot_script { my $pServerStats = shift @_; return(INSTALL::reboot::uninstall_script ($pServerStats->{engine_name}, $CONST_StartPriority, $CONST_StopPriority)); } #------------------------------------------------------------------------------ # The caspeng::configure method is responsible for querying / identifying the # wants of the user and incorporating those wants into the generated engine. #============================================================================== # Implementors note: # (1) The data stored into installed_db will be keyed off of the server name # (ie. the directory name of the generated server) and will contain a # naming scheme such that all webserver variables (ie. the values pulled # from the webserver cache) will be preceeded by webserver_ and those # values associated with the engine(s) will be preceeded by engine_. #------------------------------------------------------------------------------ sub configure { my $custom_install = boolean_value($pParameters->{custom_install}); sub enabled { ($#_ == 0) || die "Invalid number of arguments."; my ($option) = @_; return($option ? "Enabled." : "Disabled."); } sub yes { ($#_ == 0) || die "Invalid number of arguments."; my ($option) = @_; return($option ? "Yes." : "No."); } sub exit_parent { ($#_ == 0) || die "Invalid number of arguments."; my ($pid) = @_; if (interactive) { INSTALL::common::signal_spinner($pid); } else { waitpid($pid,0); } return(undef); } (@_ == 1) || die "Invalid number of arguments."; my $pServerStats = shift @_; (ref($pServerStats)) || die "Invalid argument passed."; # Multi-threaded? my @models = ("multi-threaded", "multi-process"); # Task count is now OS dependent. my %task_counts = ( LINUX => 5, SUNOS => 10, ); my $task_count = 5; # Default if (exists $task_counts{uc($pOS->{os})}) { $task_count = $task_counts{uc($pOS->{os})}; } my @default_ports = (($task_count * 5) + 1,($task_count * 5) + 1); my $model = 0; my %subst; if ((not $disable_multiprocess) && (interactive && $custom_install)) { REQUERY_THREADING_MODEL: $model = INSTALL::query::user_menu ("Sun Chili!Soft ASP - Threading Model Selection", "Sun Chili!Soft ASP can run in one of two modes: multi-process or multi-threaded. The ". "appropriate choice depends on a number of factors. For more information ". "about choosing a mode, see 'More Information' and the online product documentation.", "Select a mode", "terminal_mark=.", "Run ASP in Multi-threaded Mode", "Run ASP in Multi-process Mode", "More Information", "default=1"); if ($model == 3) { INSTALL::query::browser ( "Sun Chili!Soft ASP - Threading Model Description", "Multi-threaded Mode:\n". "-------------------\n". "Multi-threaded mode has the benefit of being the fastest of the execution modes. ". "It provides similar robustness to the Multi-process engine and is conceptually, ". "similar in the way it functions. Its only drawback is that it provides a ". "limited security model. It may be run as only one user (ie. it may not change ". "users upon request).\n". " \n". "Multi-process Mode:\n". "------------------\n". "In contrast, multi-process is slightly slower and provides the ability to change ". "users per request. It does, however, harbor the problem that it is Application ". "variable unfriendly. Usage of Application variables can hurt stability and / or ". "reduce the speed of the server.\n". " \n". "Recommendations:\n". "---------------\n". "It is recommended that any site needing a fast response time and heavy usage of ". "Application variables, use the Multi-threaded engine. This describes the more ". "common situation since clients, such as FrontPage, make heavy use of Application ". "variables. If the usage of Application variables are not an issue or you must ". "tailor each request to run as a specific user, you would probably be advised to choose ". "the multi-process model." ); goto REQUERY_THREADING_MODEL; } $model--; } else { # By Default we will be enabling the Multi-Threaded Mode $model = 0; } if (interactive) { INSTALL::query::user_menu ("Sun Chili!Soft ASP - Additional Server Options", "The following is the configuration file and port number for the ". "Web server with which Sun Chili!Soft ASP Server will run:\n". " \n". " Configuration file: $pServerStats->{webserver_conf}\n". " Port: $pServerStats->{webserver_port}\n" ); } ########## Install samples? my $install_samples = 1; if (exists $pParameters->{install_samples}) { $install_samples = boolean_value($pParameters->{install_samples}); } if (interactive && $custom_install) { $install_samples = INSTALL::query::boolean_default ("Enable samples on this Web server",$install_samples); } ########## Install docs? $install_docs = 1; if (exists $pParameters->{install_docs}) { $install_docs = boolean_value($pParameters->{install_docs}); } if (interactive && $custom_install) { $install_docs = INSTALL::query::boolean_default ("Enable documentation on this Web server",$install_docs); } ########## Automatic start on system boot (if available)? my $start_asp_onboot = 1; my $support_reboot = INSTALL::reboot::supported; if (exists $pParameters->{start_asp_onboot}) { $start_asp_onboot = boolean_value($pParameters->{start_asp_onboot}); } if (interactive && $custom_install && $support_reboot) { $start_asp_onboot = INSTALL::query::boolean_default ("Start the ASP server on system boot", $start_asp_onboot); } if ($start_asp_onboot) { $subst{caspeng::asp_on_boot} = "1"; } else { $subst{caspeng::asp_on_boot} = "0"; } ########## Start ASP now? my $start_asp = 1; if (exists $pParameters->{start_asp}) { $start_asp = boolean_value($pParameters->{start_asp}); } if (interactive && $custom_install) { $start_asp = INSTALL::query::boolean_default ("Would you like to start this ASP server",$start_asp); } ########## Do the actual configuration. if (interactive) { print STDERR ("Configuring the Sun Chili!Soft ASP server ... "); } my $pid = fork; if ($pid) { ########## Parent if (exists $pParameters->{asp_port}) { if (($pParameters->{asp_port} > 1024) && ($pParameters->{asp_port} < (32*1024))) { if (not INSTALL::services::validate_free_ports($pParameters->{asp_port}, $default_ports[$model])) { note("The specified caspd / caspeng base port $pParameters->{asp_port} was", "already in use. However, due to the options specified, this port", "has been allocated (perhaps inappropriately) for usage. This may", "cause the caspd / caspeng located in this port range to fail to", "start. Refer to the /etc/services file to find the conflict."); } $subst{caspeng::asp_port} = INSTALL::services::allocate_ports ($pParameters->{asp_port},$default_ports[$model], "Sun Chili!Soft ASP"); } else { $subst{caspeng::asp_port} = INSTALL::services::allocate_free_ports (3000,$default_ports[$model], "Sun Chili!Soft ASP"); note("The specified asp_port=$pParameters->{asp_port} option was invalid.", "Valid user port numbers range from 1024 to 32767. As a result, the", "following base port number was used: $subst{caspeng::asp_port}."); } } else { $subst{caspeng::asp_port} = INSTALL::services::allocate_free_ports (3000,$default_ports[$model], "Sun Chili!Soft ASP"); } if ($models[$model] eq "multi-threaded") { $subst{caspeng::threaded} = 1; } else { $subst{caspeng::threaded} = 0; } $subst{caspeng::task_count} = $task_count; ######################### Server type in directory # $subst{caspeng::server_dir} = # "$pGlobals->{asphome}/asp-$pServerStats->{webserver_type}-$subst{caspeng::asp_port}"; ######################### Server type in directory $subst{caspeng::server_dir} = "$pGlobals->{asphome}/asp-server-$subst{caspeng::asp_port}"; my @server_path_components = split(m@/@,$subst{caspeng::server_dir}); $subst{caspeng::server_name} = $server_path_components[$#server_path_components]; unless (mkdir($subst{caspeng::server_dir},0755)) { error("Unable to create server directory: $subst{caspeng::server_dir}"); # Run cleanup code. return(exit_parent($pid)); } unless (shallow_copy("$pGlobals->{bin_dir}/caspd", $subst{caspeng::server_dir}) && shallow_copy("$pGlobals->{bin_dir}/caspeng", $subst{caspeng::server_dir}) && shallow_copy("$pGlobals->{bin_dir}/caspctrlb", $subst{caspeng::server_dir}) && copy("$pGlobals->{asphome}/global_odbc.ini", "$subst{caspeng::server_dir}/odbc.ini")) { INSTALL::services::deallocate_ports($subst{caspeng::asp_port},$default_ports[$model]); recursive_rmdir($subst{caspeng::server_dir}); error("Unable to copy the required server binaries into:", " $subst{caspeng::server_dir}"); # Run cleanup code. return(exit_parent($pid)); } # odbc.ini assumes permission 0600 for security sake. chmod(0600,"$subst{caspeng::server_dir}/odbc.ini"); ########## Template substitutions. if (not $install_samples) { $subst{caspeng::disable_samples} = "#"; # Comments out the samples lines. } else { $subst{caspeng::disable_samples} = ""; # Leave the samples lines. } if (not $install_docs) { $subst{caspeng::disable_docs} = "#"; # Comments out the docs line. } else { $subst{caspeng::disable_docs} = ""; # Leave the docs line. } INSTALL::substitute::template("stopcaspd",$subst{caspeng::server_dir},%subst); INSTALL::substitute::template("startcaspd",$subst{caspeng::server_dir},%subst); INSTALL::substitute::template("caspctrl",$subst{caspeng::server_dir},%subst); INSTALL::substitute::template("odbc.sh",$subst{caspeng::server_dir},%subst); INSTALL::substitute::template("casp.cnfg",$subst{caspeng::server_dir},%subst); INSTALL::substitute::template("setsqlnk",$subst{caspeng::server_dir},%subst); INSTALL::substitute::template("testadocon",$subst{caspeng::server_dir},%subst); INSTALL::substitute::template("bean.policy",$subst{caspeng::server_dir},%subst); my @binary_files = ("stopcaspd", "startcaspd", "caspctrl", "setsqlnk", "testadocon" ); chmod(0755,map { "$subst{caspeng::server_dir}/$_" } @binary_files); $pServerStats->{engine_name} = $subst{caspeng::server_name}; $pServerStats->{engine_dir} = "$pGlobals->{asphome}/$subst{caspeng::server_name}"; $pServerStats->{engine_port} = $subst{caspeng::asp_port}; $pServerStats->{engine_portcount} = $default_ports[$model]; $pServerStats->{engine_asponboot} = $subst{caspeng::asp_on_boot}; ########## Install reboot scripts, if supported / enabled. if ($start_asp_onboot && $support_reboot) { install_boot_script($pServerStats); } my @summary_log = ("Server installed ($subst{caspeng::server_name}):", " Associated Web server conf file: $pServerStats->{webserver_conf}", " Associated Web server port: $pServerStats->{webserver_port}", " Location: $subst{caspeng::server_dir}", " Port: $subst{caspeng::asp_port}", " Samples: ".enabled($install_samples), " Documentation: ".enabled($install_docs), " Automatic ASP start on system boot: ".enabled($start_asp_onboot && $support_reboot), " ASP started: ".yes($start_asp), " ASP start script: $subst{caspeng::server_dir}/startcaspd", " ASP stop script: $subst{caspeng::server_dir}/stopcaspd", " ASP general control script: $subst{caspeng::server_dir}/caspctrl" ); if ($install_samples) { my $hostname; if (not exists $pServerStats->{webserver_realhost}) { use Sys::Hostname; use Socket; $hostname = hostname(); my $address = gethostbyname($hostname); $hostname = gethostbyaddr($address,AF_INET); } else { $hostname = $pServerStats->{webserver_realhost}; } push @summary_log, " Samples URL: http://$hostname:$pServerStats->{webserver_port}/caspsamp"; } summary_log(@summary_log); if (-r "$pGlobals->{bin_dir}/hashobj") { ## Support MainWin hashobj multi-process requirement. ## We need the HashObj to be here. shallow_copy("$pGlobals->{bin_dir}/hashobj","$pServerStats->{engine_dir}"); shallow_copy("$pGlobals->{bin_dir}/hashobj.rsb","$pServerStats->{engine_dir}"); shallow_copy("$pGlobals->{bin_dir}/hashobj.tlb","$pServerStats->{engine_dir}"); my $setup = "$pGlobals->{asphome}/chsetup.sh"; my $register_log = "Registering hashobj for $pServerStats->{engine_dir}: "; if (not system(". $setup;$pServerStats->{engine_dir}/hashobj -regserver >> $pGlobals->{installer_output} 2>&1")) { $register_log .= "Success."; } else { error("Unable to register hashobj. Skipping the server configuration"); $register_log .= "Failed."; # do not complete the installation. INSTALL::caspeng::uninstall($pServerStats); } register_log($register_log); } if ($start_asp) { unless(start_engine($pServerStats)) { note("The ASP engine failed to start."); } } exit_parent($pid); } else { ########## Child if (interactive) { INSTALL::common::spinner_wait_signal; } exit(0); } return(1); } #------------------------------------------------------------------------------ # #============================================================================== # Implementors note: # (1) This method is guaranteed to receive not only the listing of all of the # cached webserver information, but also a single element of the hash # named 'engine_dir'. This refers to the directories (ie. server_name), # of the Sun Chili!Soft ASP engine being uninstalled. It should be noted # that truncated version of engine_dir, which contains only its name, is # also passed via 'engine_name'. # (2) The naming convention in %server_stats is different from that used during # the install. As dictated in caspeng::configure, webserver_ is prepended # to all contained webserver information. #------------------------------------------------------------------------------ sub uninstall { (@_ == 1) || die "Invalid number of arguments."; my $pServerStats = shift @_; (ref($pServerStats)) || die "Invalid argument passed."; if (not exists $pServerStats->{engine_dir}) { return(error("Passed server statistics does not contain 'engine_dir' - uninstall.")); } ########## Stop running engines. print STDERR ("Stopping ASP Server $pServerStats->{engine_name} ... ") if interactive; my $pid = fork; if (not $pid) { # dont worry about the error value here. stop_engine($pServerStats); exit (0); } else { if (interactive) { INSTALL::common::spinner_wait($pid); print "\n" if interactive; } else { waitpid($pid,0); } if ($?) { $result = 1; } } ########## Handle the relocation of the logfile. my $logfile; if (open(CASP_CNFG,"$pServerStats->{engine_dir}/casp.cnfg")) { foreach () { if (/^\s*logfile\s*=\s*(.*)\s*$/) { $logfile = $1; } } close(CASP_CNFG); } ########## Ask for logfile relocation # if ((-f $logfile) && interactive) { # my $answer = INSTALL::query::user # ("Would you like to keep a copy of your servers log file","terminal_mark=?", # "y","default=n"); # # if ($answer eq "y") { # sub file_save_ok # { # ($filename) = @_; # # if ($filename ne "none") { # my $parent = $filename; # $parent =~ s@^(.*)/.*$@$1@; # # if ($parent eq "") { # $parent = "/"; # } # # # Allow save only if valid parent directory, and the file does not # # already exist. # return((-d $parent) && (-w $parent) && (not -f $filename)); # } # else { # return(1); # } # } # # $answer = INSTALL::query::user_function # ("Input the full path of the file you want to save the log to\n", # *file_save_ok, # "default=none", # "invalid_choice=The specified path was not writable or that file already exists."); # # if ($answer ne "none") { # system("cp -f $logfile $answer"); # } # } # } ########## if (-f $logfile) { chmod(0666,$logfile); unlink($logfile); } ########## Deallocate the used ports. INSTALL::services::deallocate_ports ($pServerStats->{engine_port},$pServerStats->{engine_portcount}); ########## Remove all files in the server directory. recursive_rmdir($pServerStats->{engine_dir}); ########## Uninstall the reboot script, if there is one. my $start_asp = 0; if (exists $pServerStats->{engine_asponboot}) { $start_asp = boolean_value($pServerStats->{engine_asponboot}); } if ($start_asp) { uninstall_boot_script($pServerStats); } ########## Uninstall the installed_db section associated with this server. my $pIni = INSTALL::ini::open($pGlobals->{installed_db}); if (!$pIni) { return(error("Could not open $pGlobals->{installed_db}.")); } summary_log ("Server uninstalled ($pServerStats->{engine_name}):", " Associated Web server conf file: $pServerStats->{webserver_conf}", " Associated Web server port: $pServerStats->{webserver_port}" ); return(INSTALL::ini::deleteSection($pIni,$pServerStats->{engine_dir}) && INSTALL::ini::save($pIni,$pGlobals->{installed_db})); } 1;