Creating Install Scripts

Script Installers

A Virtualmin script installer is a small program that contains the information needed to install a web application into a virtual server's home directory, and configure it to run with that server's permissions and using its database. Most script installers are for PHP programs like phpMyAdmin, Drupal and SugarCRM, but it is possible to write an installer for Perl, Python or Ruby or Rails applications too.

Virtualmin Pro ships with a large number of built-in installers, which domain owners can add to their websites using the Install Scripts link on the left menu. However, there are many applications that are not covered yet, simply because we don't have time to implement installers for them or they are judged too rarely used or too specific. For this reason, Virtualmin provides an API for adding your own script installers.

Script Installer Files and Directories

Each script installer is a single file containing a set of Perl functions. Those that ship with Virtualmin Pro can be found in the virtual-server/scripts directory under the Webmin root, which is usually /usr/libexec/webmin or /usr/share/webmin . If you open up one of those files (such as phpbb.pl) in a text editor, you will see a series of funtions like :

sub script_phpbb_desc
{
return "phpBB";
}

sub script_phpbb_uses
{
return ( "php" );
}

sub script_phpbb_longdesc
{
return "A high powered, fully scalable, and highly customizable Open Source bulletin board package.";
}

Your own script installers will be in files if a similar format - the major difference will be the script ID, which appears in each function name after the word script_ .

Script installers that are local to your Virtualmin installation are stored in the /etc/webmin/virtual-server/scripts directory. In most cases, each script is just a single .pl file, but it is possible for other source or support files to be part of the script too. In general though, most script installers download the files they need from the website of the application that they are installing.

Script Installer IDs

Every script installer has a unique ID, which must consist of only letters, numbers and the underscore character. The ID determines both the installer filename (which must be scriptid.pl), and the names of functions within the script (which must be like script_scriptid_desc).

The same ID cannot be used by two different installers on the same system, even if one is built-in to Virtualmin and one is custom. For this reason, when writing an installer you should select an ID that is unlikely to clash with any that might be included in Virtualmin in the future. Starting it with the first part of your company's domain name (like foocorp_billingapp) would be a good way to ensure this.

The Lifetime of a Script

Virtualmin allows multiple instances of a single script to be installed, either on different domains or in different directories of the same domain. The installer defines the steps that must be taken to setup a script in some directory - in object-oriented coding parlance, it is like a class, while installed scripts are objects.

When a script is installed via the web interface, Virtualmin performs the following steps :

  1. Checks if all required dependencies are satisfied, such as required commands, a database and a website.
  2. If the script uses PHP, checks that the versions it supports are available on the system.
  3. Displays a form asking for installation options, such as the destination directory and database.
  4. Parses inputs from the form.
  5. Checks if the same script is already installed in the selected directory.
  6. Configures the domain's website to use the correct PHP version.
  7. Downloads files needed by the script, such as its source code.
  8. Installs any needed PHP modules, Pear modules, Perl modules or Ruby Gems.
  9. Calls the script's install function. This typically does the following :
    1. Creates a database for the script, if needed and if requested.
    2. Creates the destination directory.
    3. Extracts the downloaded source into a temporary directory.
    4. Copies the source to the destination directory.
    5. Updates any config files used by the application being installed, so that it knows how to connect to the database and which directory it runs in.
  10. Records the fact that the script has been installed.
  11. Configures PHP for the domain, to set any options that the script has requested.
  12. Restarts Apache.

Script Installer Implementation

In this section, the functions that each script installer must implement will be covered. Not all functions are mandatory - some deal with PHP dependencies that make no sense if your script does not use PHP, or if it has no non-core module or Pear dependencies. The example code for each function is taken from the Wordpress Blog installer, in wordpress.pl. This is a PHP application whose installation process is relatively simple, yet common to many other PHP programs.

In your own script, you would of course replace scriptname with the script ID you have selected.

Also, just like a Perl module -- make sure your Install Script file ends with the line:

1;

script_scriptname_desc

This function must return a short name for the script, usually a couple of words at most.

sub script_wordpress_desc
{
return "WordPress";
}

script_scriptname_uses

This must return a list of the languages the script uses. Supported language codes are php, perl and ruby. Most scripts will return only one.

sub script_wordpress_uses
{
return ( "php" );
}

script_scriptname_versions

Must return a list of versions of the script that the installer supports. Most can only install one, but in some cases you may want to offer the user the ability to install development and stable versions of some application. The version the user chooses will be passed to many other functions as a parameter.

sub script_wordpress_versions
{
return ( "2.2.1" );
}

script_scriptname_category (optional)

This function should return a category for the script, which controls how it is categorized in Virtualmin's list of those available to install. At the time of writing, available categories were Blog, Calendar, Commerce, Community, Content Management System, Database, Development, Email, Guestbook, Helpdesk, Horde, Photos, Project Management, Survey, Tracker and Wiki.

sub script_wordpress_category
{
return "Blog";
}

script_scriptname_php_vers (PHP scripts only)

Scripts that use PHP must implement this function, which should return a list of versions the installed application can run under. At the time of writing, Virtualmin only supports PHP versions 4 and 5. On systems that have more than one version of PHP installed, Virtualmin will configure the website to use the correct version for the path the script is installed to.

sub script_wordpress_php_vers
{
return ( 4, 5 );
}

script_scriptname_php_modules (PHP scripts only)

If the application being installed is written in PHP and requires any non-core PHP modules, this function should return them as a list. Any script that talks to a MySQL database will need the mysql module, or pgsql if it uses PostgreSQL. Virtualmin will attempt to install the required modules if they are missing from the system.

sub script_wordpress_php_modules
{
return ("mysql");
}

script_scriptname_pear_modules (PHP scripts only)

Pear is a repository of additional modules for PHP, which some Virtualmin scripts make use of. If the application you are installing requires some Pear modules, this function can be implemented to return a list of module names. At installation time, Virtualmin will check for and try to automatically install the needed modules.

sub script_horde_pear_modules
{
return ("Log", "Mail", "Mail_Mime", "DB");
}

script_scriptname_perl_modules (Perl scripts only)

For scripts written in Perl that require modules that are not part of the standard Perl distribution, you should implement this function to return a list of additional modules required. Virtualmin will try to automatically install them from YUM, APT or CPAN where possible, and will prevent the script from being installed if they are missing.

sub script_twiki_perl_modules
{
return ( "CGI::Session", "Net::SMTP" );
}

script_scriptname_python_modules (Python scripts only)

For scripts written in Python that require modules that are not part of the standard distribution, you should implement this function to return a list of additional modules required. Virtualmin will try to automatically install them from YUM or APT where possible, and will prevent the script from being installed if they are missing.

sub script_django_python_modules
{
return ( "setuptools", "MySQLdb" );
}

script_scriptname_depends(&domain, version)

This function must check for any dependencies the script has before it can be installed, such as a MySQL database or virtual server features. It is given two parameters - the domain hash containing details of the virtual server being installed into, and the version number selected.

sub script_wordpress_depends
{
local ($d, $ver) = @_;
&has_domain_databases($d, [ "mysql" ]) ||
        return "WordPress requires a MySQL database" if (!@dbs);
&require_mysql();
if (&mysql::get_mysql_version() < 4) {
        return "WordPress requires MySQL version 4 or higher";
        }
return undef;
}

As of Virtualmin 3.57, this function can return a list of missing dependency error messages instead of a single string, which is more user-friendly as they are all reported to users at once.

script_scriptname_dbs(&domain, version)

If defined, this function should return a list of database types that the script can use. At least one of these types must be enabled in the virtual server the script is being installed into.

sub script_wordpress_dbs
{
local ($d, $ver) = @_;
return ("mysql");
}

Only Virtualmin 3.57 and above make use of this function.

script_scriptname_params(&domain, version, &upgrade)

This function is responsible for generating the installation form inputs, such as the destination directory and target database. When upgrading (indicated by the upgrade hash being non-null) these are fixed and should just be displayed to the user. Otherwise, it must return inputs for selecting them. The functions return value must be HTML for form fields, generated using the ui_table_row and other ui_ functions.

The example below from Wordpress is a good source to copy from, as most PHP scripts that you would want to install will need a target directory and a database. The ui_database_select function can be used to generate a menu of databases in the domain, with an option to have a new one created automatically just for this script.

sub script_wordpress_params
{
local ($d, $ver, $upgrade) = @_;
local $rv;
local $hdir = &public_html_dir($d, 1);
if ($upgrade) {
        # Options are fixed when upgrading
        local ($dbtype, $dbname) = split(/_/, $upgrade->{'opts'}->{'db'}, 2);
        $rv .= &ui_table_row("Database for WordPress tables", $dbname);
        local $dir = $upgrade->{'opts'}->{'dir'};
        $dir =~ s/^$d->{'home'}\///;
        $rv .= &ui_table_row("Install directory", $dir);
        }
else {
        # Show editable install options
        local @dbs = &domain_databases($d, [ "mysql" ]);
        $rv .= &ui_table_row("Database for WordPress tables",
                     &ui_database_select("db", undef, \@dbs, $d, "wordpress"));
        $rv .= &ui_table_row("Install sub-directory under <tt>$hdir</tt>",
                             &ui_opt_textbox("dir", "wordpress", 30,
                                             "At top level"));
        }
return $rv;
}

script_scriptname_parse(&domain, version, &in, &upgrade)

This function takes the inputs from the form generated by script_scriptname_params, parses them an returns an object containing options that will be used when the installation actually happens. If it detects any errors in the input, it should return an error message string instead.

As in the example below, when upgrading the options are almost never changed, so it should return just $upgradeā†’{'opts'}, which are the options it was originally installed with. Otherwise, it should look at the hash reference in which will contain all CGI form variables, and use that to construct a hash of options. The most important keys in the hash are dir (the installation target directory) and path (the URL path under the domain's root).

sub script_wordpress_parse
{
local ($d, $ver, $in, $upgrade) = @_;
if ($upgrade) {
        # Options are always the same
        return $upgrade->{'opts'};
        }
else {
        local $hdir = &public_html_dir($d, 0);
        $in{'dir_def'} || $in{'dir'} =~ /\S/ && $in{'dir'} !~ /\.\./ ||
                return "Missing or invalid installation directory";
        local $dir = $in{'dir_def'} ? $hdir : "$hdir/$in{'dir'}";
        local ($newdb) = ($in->{'db'} =~ s/^\*//);
        return { 'db' => $in->{'db'},
                 'newdb' => $newdb,
                 'multi' => $in->{'multi'},
                 'dir' => $dir,
                 'path' => $in{'dir_def'} ? "/" : "/$in{'dir'}", };
        }
}

script_scriptname_check(&domain, version, &opts, &upgrade)

This function must verify the installation options in the opts hash, and return an error message if any are invalid (or undef if they all look OK). Possible problems include a missing or invalid install directory, a clash with an existing install of the same script in the directory, or a clash of tables in the selected database. As the example below shows, the find_database_table function provides a convenient way to search for tables by name or regular expression - for most applications, all tables used will be prefixed by a short code, like wp_ in the case of WordPress.

If you are wondering why these checks are not performed in script_scriptname_parse, the reason is that when a script is installed from the command line, that function is never called. Instead, install options are generated using a different method, and then validated by this function.

sub script_wordpress_check
{
local ($d, $ver, $opts, $upgrade) = @_;
$opts->{'dir'} =~ /^\// || return "Missing or invalid install directory";
$opts->{'db'} || return "Missing database";
if (-r "$opts->{'dir'}/wp-login.php") {
        return "WordPress appears to be already installed in the selected directory";
        }
local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2);
local $clash = &find_database_table($dbtype, $dbname, "wp_.*");
$clash && return "WordPress appears to be already using the selected database (table $clash)";
return undef;
}

script_scriptname_files(&domain, version, &opts, &upgrade)

This is the function where the script installer indicates to Virtualmin what files need to be downloaded for the installation to go ahead. Most scripts need only one, which contains the source code - but it is possible to request any number, even zero.

The function must return a list of hash references, each of which should contain the following keys :

  • name A unique name for this file, used later by the script_scriptname_install function.
  • file A short filename for the file, to which it will be saved in /tmp/.webmin after being downloaded.
  • url The URL that it can be downloaded from.
  • nocache Optional, but can be to 1 to force a download even if the URL is cached by Virtualmin.

In most cases, the ver parameter is used in the URL and filename to get the correct source archive. WordPress (shown below) is an exception, as it has only a single download URL which always serves up the latest version.


sub script_wordpress_files
{
local ($d, $ver, $opts, $upgrade) = @_;
local @files = ( { 'name' => "source",
           'file' => "latest.tar.gz",
           'url' => "http://wordpress.org/latest.zip",
           'nocache' => 1 } );
return @files;
}

script_scriptname_commands

If your script installer requires any commands to do its job that may not be available on a typical Unix system, this function should return a list of them. In most cases, it just returns the programs needed to un-compress the tar.gz or zip file containing the source.

sub script_wordpress_commands
{
return ("unzip");
}

script_scriptname_install(&domain, version, &opts, &files, &upgrade, username, password)

This function is where the real work of installing a script actually happens. It is responsible for setting up the database, un-compressing the downloaded source, copying it to the correct directory, modifying configuration files to match the domain and database, and returning a URL that can be used to login. If anything goes wrong, it must return an array whose first element is zero, and the second is an error message.

Upon success, it must return an an array containing the following elements :

  • The number 1 (indicating success)
  • An HTML message to display to the user. This should include a link that can be used to access the script.
  • A description of where it was installed, usually formatted like Under /wordpress using mysql database yourdomain.
  • The URL that can be used to access the script.
  • The initial administration login, if any.
  • The initial administration password.

If given, the username and password parameters should be used to set the initial administrative login for the script. If not, it should default to the domain's login and password.

The code snippets below show each step of the install process, taken from the standard WordPress installer. The first part simply parses the database connection options and creates a new DB for the script, if one was requested :

sub script_wordpress_install
{
local ($d, $version, $opts, $files, $upgrade) = @_;
local ($out, $ex);
if ($opts->{'newdb'} && !$upgrade) {
        local $err = &create_script_database($d, $opts->{'db'});
        return (0, "Database creation failed : $err") if ($err);
}
local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2);
local $dbuser = $dbtype eq "mysql" ? &mysql_user($d) : &postgres_user($d);
local $dbpass = $dbtype eq "mysql" ? &mysql_pass($d) : &postgres_pass($d, 1);
local $dbphptype = $dbtype eq "mysql" ? "mysql" : "psql";
local $dbhost = &get_database_host($dbtype);
local $dberr = &check_script_db_connection($dbtype, $dbname, $dbuser, $dbpass);
return (0, "Database connection failed : $dberr") if ($dberr);
}

The next step is to extract the downloaded source code, and then copy it to the created destination directory. This is done by calling the unzip and cp commands as the Virtualmin domain owner, so that there is no risk of files that he is not supposed to have access to being over-written. The source code temporary file can be found from the files hash reference in the source key, which was defined by the script_scriptname_files function.

Note how the code checks for expected files after extracting and copying the source, to make sure that the commands called actually succeeded.

# Create target dir
if (!-d $opts->{'dir'}) {
        $out = &run_as_domain_user($d, "mkdir -p ".quotemeta($opts->{'dir'}));
        -d $opts->{'dir'} ||
                return (0, "Failed to create directory : <tt>$out</tt>.");
        }

# Extract tar file to temp dir
local $temp = &transname();
mkdir($temp, 0755);
chown($d->{'uid'}, $d->{'gid'}, $temp);
$out = &run_as_domain_user($d, "cd ".quotemeta($temp).
                               " && unzip $files->{'source'}");
local $verdir = "wordpress";
-r "$temp/$verdir/wp-login.php" ||
        return (0, "Failed to extract source : <tt>$out</tt>.");

# Move html dir to target
$out = &run_as_domain_user($d, "cp -rp ".quotemeta($temp)."/$verdir/* ".
                               quotemeta($opts->{'dir'}));
local $cfileorig = "$opts->{'dir'}/wp-config-sample.php";
local $cfile = "$opts->{'dir'}/wp-config.php";
-r $cfileorig || return (0, "Failed to copy source : <tt>$out</tt>.");

Most scripts or applications have a configuration file of some kind that defines where to access the database, what domain they are running under, the URL path, and possibly an initial login and password. The script installers is responsible for creating or modifying this file to use the database connection details supplied by the opts paramater, as shown in the code snippet below.

Be careful when upgrading, as in general the existing configuration file will be valid for the new version. This means that it doesn't need to be re-created, and should be preserved during the upgrade process if necessary.

# Copy and update the config file
if (!-r $cfile) {
        &run_as_domain_user($d, "cp ".quotemeta($cfileorig)." ".
                                      quotemeta($cfile));
        local $lref = &read_file_lines($cfile);
        local $l;
        foreach $l (@$lref) {
                if ($l =~ /^define\('DB_NAME',/) {
                        $l = "define('DB_NAME', '$dbname');";
                        }
                if ($l =~ /^define\('DB_USER',/) {
                        $l = "define('DB_USER', '$dbuser');";
                        }
                if ($l =~ /^define\('DB_HOST',/) {
                        $l = "define('DB_HOST', '$dbhost');";
                        }
                if ($l =~ /^define\('DB_PASSWORD',/) {
                        $l = "define('DB_PASSWORD', '$dbpass');";
                        }
                if ($opts->{'multi'}) {
                        if ($l =~ /^define\('VHOST',/) {
                                $l = "define('VHOST', '');";
                                }
                        if ($l =~ /^\$base\s*=/) {
                                $l = "\$base = '$opts->{'path'}/';";
                                }
                        }
                }
        &flush_file_lines($cfile);
        }

In some cases, a script will come with a file of SQL statements that can be used to create and populate tables in its database. Others like WordPress do this automatically when they are first accessed. If the application you are installing needs SQL to be run as part of its setup process, you can use code like the fragment below, which was taken from the WebCalendar installer :

if (!$upgrade) {
        # Run the SQL setup script
        if ($dbtype eq "mysql") {
                local $sqlfile = "$opts->{'dir'}/tables-mysql.sql";
                &require_mysql();
                ($ex, $out) = &mysql::execute_sql_file($dbname, $sqlfile, $dbuser, $dbpass);
                $ex && return (0, "Failed to run database setup script : <tt>$out</tt>.");
                }

The final part of the script_scriptname_install function is returning information to Virtualmin about how to access the new script, and where it is installed. In some cases, a script will have two URLs - the one for administration (which should be references in the second element of the returned array), and the one for general use (which should be in the 4th element).

local $url = &script_path_url($d, $opts).
             ($upgrade ? "wp-admin/upgrade.php" : "wp-admin/install.php");
local $userurl = &script_path_url($d, $opts);
local $rp = $opts->{'dir'};
$rp =~ s/^$d->{'home'}\///;
return (1, "WordPress installation complete. It can be accessed at <a href='$url'>$url</a>.", "Under $rp using $dbphptype database $dbname", $userurl);
}

script_scriptname_uninstall(&domain, version, &opts)

This function is responsible for cleaning up all files and database tables created by the install code. It is only called when the user deletes a script from a domain, not when upgrading. If most cases, determining which tables to remove is simple, as they all start with some prefix (like wp_ in the case of WordPress). But if a script has created a tables that cannot be automatically identified, your installer will need to have this list hard-coded. See the oscommerce.pl installer for an example.

If the installer has created any cron jobs, server processes, custom Apache configuration entries or email aliases, they must also be removed by this function. On success, it should return a two-element array whose first element is 1, and the second a message to display to the user. On failure, it should return 0 and an error message explaining what went wrong.

sub script_wordpress_uninstall
{
local ($d, $version, $opts) = @_;

# Remove the contents of the target directory
local $derr = &delete_script_install_directory($d, $opts);
return (0, $derr) if ($derr);

# Remove all wp_ tables from the database
local ($dbtype, $dbname) = split(/_/, $opts->{'db'}, 2);
if ($dbtype eq "mysql") {
        # Delete from MySQL
        &require_mysql();
        foreach $t (&mysql::list_tables($dbname)) {
                if ($t =~ /^wp_/) {
                        &mysql::execute_sql_logged($dbname,
                                "drop table ".&mysql::quotestr($t));
                        }
                }
        }
else {
        # Delete from PostgreSQL
        &require_postgres();
        foreach $t (&postgresql::list_tables($dbname)) {
                if ($t =~ /^wp_/) {
                        &postgresql::execute_sql_logged($dbname,
                                "drop table $t");
                        }
                }
        }

# Take out the DB
if ($opts->{'newdb'}) {
        &delete_script_database($d, $opts->{'db'});
        }

return (1, "WordPress directory and tables deleted.");
}

script_scriptname_passmode(&domain, version)

Most scripts that setup an initial login and password use those from the virtual server the script is being added to. However, Virtualmin can prompt the user for alternative authentication details if you implement this function. All it has to do is return one of the following numeric codes :

  • 1 - Script needs a username and password
  • 2 - Script only needs a password
  • 3 - Script only needs a username

The custom login and password entered by the user will be passed to the script_scriptname_install function. If your script installer doesn't setup an initial login at all, you can either omit this function or have it return 0.

sub script_wordpress_passmode
{
return 1;
}

Other Installation Methods

The style of installation code above will work for most scripts that you want to install, but in some cases a slightly different approach is needed. This section covers two of them - scripts with their own configuration generators that cannot be easily replaced by creating the config file yourself, and installers for Ruby On Rails apps, which use a separate server process.

HTTP Requests

Many PHP applications come with a script that asks the user a series of questions, like the database login and name, domain name, and initial administration username and password. The script then uses this information to create a config file and perhaps populated the database.

Ideally, Virtualmin script installers should create any needed config files directly - but in some cases this is too difficult due to their complexity. Similarly, it may not be possible to create and populate all the needed database tables if no SQL file is provided for doing this. In cases like this, it is simpler for a script installer to invoke the application's install code directly, by making an HTTP request to the correct URL.

To figure out the installation URL and the CGI parameters it needs, you will need to install the application manually and run through its install process in a browser. The View source feature can then be used to find the names and meanings of all form fields, which can then be used to construct code to call the script the form would submit to.

The following code fragment from the script_phpbb_install function of the phpbb.pl installer gives an example of this :

# Make config.php writable
&make_file_php_writable($d, $cfile);

# Trigger the installation PHP script
local @params = (
        [ "lang", "english" ],
        [ "dbms", $dbtype eq "mysql" ? "mysql4" : "postgres" ],
        [ "upgrade", 0 ],
        [ "dbhost", $dbhost ],
        [ "dbname", $dbname ],
        [ "dbuser", $dbuser ],
        [ "dbpasswd", $dbpass ],
        [ "prefix", "phpbb_" ],
        [ "board_email", $d->{'emailto'} ],
        [ "server_name", "www.".$d->{'dom'} ],
        [ "server_port", $d->{'web_port'} ],
        [ "script_path", $opts->{'path'}."/" ],
        [ "admin_name", $d->{'user'} ],
        [ "admin_pass1", $d->{'pass'} ],
        [ "admin_pass2", $d->{'pass'} ],
        [ "install_step", 1 ],
        [ "current_lang", "english" ],
        );
local $params = join("&", map { $_->[0]."=".&urlize($_->[1]) } @params);
local $ipage = $opts->{'path'}."/install/install.php";

# Make an HTTP post to the installer page
local ($iout, $ierror);

&post_http_connection("www.$d->{'dom'}", $d->{'web_port'},
                      $ipage, $params, \$iout, \$ierror);
if ($ierror) {
        return (0, "phpBB post-install configuration failed : $ierror");
        }
elsif ($iout !~ /Finish Installation/i) {
        return (0, "phpBB post-install configuration failed");
        }

As you can see, it makes use of the post_http_connection function provided by Virtualmin which makes an HTTP POST request, which is expected by most applications. If the form is submitted using a GET, you could use Webmin's http_download function instead.

In some cases, the installation process is a multi-step wizard, which means that you will need to make several POST requests with different parameters, and possibly parse the output from each. In the worst case, the application may set cookies to track the progress of the wizard - see the SugarCRM installer in sugarcrm.pl for an example of this.

Ruby On Rails Installation

Most Rails applications installed by Virtualmin are actually run by a separate server process, typically a Mongrel webserver. To link them up to the domain's actual website, Apache proxy directives are added that pass all requests to a path like /typo to a local webserver at a URL like http://localhost:3001/typo. Starting and maintaining this server process and configuring Apache to use it requires a fair bit of work, but fortunately Virtualmin Pro version 3.44 and above include functions that make it easier.

Some Ruby applications are available from the Ruby Gems package installation service, while others must be downloaded and extracted like PHP applications. For example, typo.pl does installation entirely from a Gem, and so has a script_typo_files function that returns nothing. It then makes use of the install_ruby_gem function to Install gems for Mongrel and Typo itself.

The code fragment below is the first part of the script_typo_install function. As you can see, it checks for the gem command, and then calls functions to install those Gems that it needs.

sub script_typo_install
{
local ($d, $version, $opts, $files, $upgrade) = @_;
local ($out, $ex);

# Check for the gem command (here instead of earlier, as it may have been
# automatically installed).
&has_command("gem") || return (0, "The Ruby gem command is not installed");

# Create target dir
if (!-d $opts->{'dir'}) {
        $out = &run_as_domain_user($d, "mkdir -p ".quotemeta($opts->{'dir'}));
        -d $opts->{'dir'} ||
                return (0, "Failed to create directory : <tt>$out</tt>.");
        }

# Install mongrel first
local $err = &install_ruby_gem("mongrel");
if ($err) {
        return (0, "Mongrel GEM install failed : <pre>$err</pre>");
        }

# Install typo itself
local $err = &install_ruby_gem("typo");
if ($err) {
        return (0, "Typo GEM install failed : <pre>$err</pre>");
        }
if (!&has_command("typo")) {
        return (0, "Install appear to succeed, but the <tt>typo</tt> command ".
                   "could not be found");
        }

Just installing the Gem is not enough - the code for Typo needs to be somehow copied into the virtual server's directory. Fortunately, Typo provides a command to do this, shown in the code below. Other Ruby applications are distributed in tar.gz files, and need to be extracted and copied into place.

$out = &run_as_domain_user($d, "cd ".quotemeta($opts->{'dir'})." && ".
                               "typo install .");
if ($?) {
        return (0, "Typo setup failed : <pre>$out</pre>");
        }

Because Rails applications use a separate server process, your installer must find a free port for it to run on, and then start it. Virtualmin provides the allocate_mongrel_port function to do the former, and the mongrel_rails_start_cmd function to build a command for the latter. Some Rails applications (like Typo) provide their own server startup scripts, so check the documentation to see which commands they recommend.

$out = &run_as_domain_user($d, "cd ".quotemeta($opts->{'dir'})." && ".
                               "typo start .");
if ($?) {
        return (0, "Failed to start Typo server : <pre>$out</pre>");
        }

All Rails applications will need an Apache proxy configuration to direct requests from the Apache webserver to their Mongrel server. Virtualmin provides a simple function to set this up, shown in the code below. Be aware that many Rails apps don't like being run in a sub-directory, and most don't automatically support it. Your installer may need to modify configuration files and perhaps even application code to support this.

&setup_mongrel_proxy($d, $opts->{'path'}, $opts->{'port'},
                     $opts->{'path'} eq '/' ? undef : $opts->{'path'});

The server process that runs the Rails application must be running all the time - but what happens if the system gets rebooted? To handle this, Virtualmin makes it easy to create either an @reboot crontab entry or /etc/init.d script to start the server, as shown in the following code. The init script will only be added if the domain has the new Bootup Actions plugin installed and made available.

if (!$upgrade) {
        # Configure server to start at boot
        local $typo = &has_command("typo");
        local $startcmd = "cd ".quotemeta($opts->{'dir'})."; ".
                          "$typo start . 2>&1 </dev/null";
        local $stopcmd = "kill `cat ".quotemeta($pidfile)."`";
        &setup_mongrel_startup($d, $startcmd, $stopcmd, $opts,
                               1, "typo-".$opts->{'port'}, "Typo Blog Engine");
        }

Ruby On Rails Un-Installation

Just as special code is required to install Rails applications, your script must also implement the script_scriptname_uninstall function to clean up all server processes, boot scripts and Apache config entries. This is made easier by several convenience functions provided by Virtualmin, the use of which is show in the code below:

sub script_typo_uninstall
{
local ($d, $version, $opts) = @_;

# Shut down the server process
local $pidfile = "$opts->{'dir'}/tmp/pid.txt";
local $pid = &check_pid_file($pidfile);
if ($pid) {
        kill('KILL', $pid);
        }

# Remove bootup script

&delete_mongrel_startup($d, $opts, "typo start .");

# Remove the contents of the target directory
local $derr = &delete_script_install_directory($d, $opts);
return (0, $derr) if ($derr);

# Remove proxy Apache config entry for /typo
&delete_mongrel_proxy($d, $opts->{'path'});
&register_post_action(\&restart_apache);

return (1, "Typo directory deleted.");
}

When Virtualmin deletes a domain, it does not call the uninstall functions for any installed scripts, as this would generally be a waste of time - their directories and databases are going to be removed anyway. In the case of Rails applications, this is not true - their installers start server processes and create boot scripts that must be cleaned up.

To ensure that this happens, your script installer must implement the script_scriptname_stop function, which only has to shut down any server processes and prevent them from being run at boot time. This function is only called at virtual server deletion time, and is optional for installers that don't require it.

sub script_typo_stop
{
local ($d, $sinfo) = @_;
local $pidfile = "$sinfo->{'opts'}->{'dir'}/tmp/pid.txt";
local $pid = &check_pid_file($pidfile);
if ($pid) {
        kill('KILL', $pid);
        }
&delete_mongrel_startup($d, $sinfo->{'opts'}, "typo start .");
}