# $Id: routines.sh 106061 2024-09-16 14:03:52Z vboxsync $
# Oracle VirtualBox
# VirtualBox installer shell routines
#

#
# Copyright (C) 2007-2024 Oracle and/or its affiliates.
#
# This file is part of VirtualBox base platform packages, as
# available from https://www.virtualbox.org.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation, in version 3 of the
# License.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <https://www.gnu.org/licenses>.
#
# SPDX-License-Identifier: GPL-3.0-only
#

ro_LOG_FILE=""
ro_X11_AUTOSTART="/etc/xdg/autostart"
ro_KDE_AUTOSTART="/usr/share/autostart"

## Aborts the script and prints an error message to stderr.
#
# syntax: abort message

abort()
{
    echo 1>&2 "$1"
    exit 1
}

## Creates an empty log file and remembers the name for future logging
# operations
create_log()
{
    ## The path of the file to create.
    ro_LOG_FILE="$1"
    if [ "$ro_LOG_FILE" = "" ]; then
        abort "create_log called without an argument!  Aborting..."
    fi
    # Create an empty file
    echo > "$ro_LOG_FILE" 2> /dev/null
    if [ ! -f "$ro_LOG_FILE" -o "`cat "$ro_LOG_FILE"`" != "" ]; then
        abort "Error creating log file!  Aborting..."
    fi
}

## Writes text to standard error, as standard output is masked.
#
# Syntax: info text
info()
{
    echo 1>&2 "$1"
}

## Copies standard input to standard error, as standard output is masked.
#
# Syntax: info text
catinfo()
{
    cat 1>&2
}

## Writes text to the log file
#
# Syntax: log text
log()
{
    if [ "$ro_LOG_FILE" = "" ]; then
        abort "Error!  Logging has not been set up yet!  Aborting..."
    fi
    echo "$1" >> $ro_LOG_FILE
    return 0
}

## Writes test to standard output and to the log file
#
# Syntax: infolog text
infolog()
{
    info "$1"
    log "$1"
}

## Checks whether a module is loaded with a given string in its name.
#
# syntax: module_loaded string
module_loaded()
{
    if [ "$1" = "" ]; then
        log "module_loaded called without an argument.  Aborting..."
        abort "Error in installer.  Aborting..."
    fi
    lsmod | grep -q $1
}

## Abort if we are not running as root
check_root()
{
    if [ `id -u` -ne 0 ]; then
        abort "This program must be run with administrator privileges.  Aborting"
    fi
}

## Abort if dependencies are not found
check_deps()
{
    for i in ${@}; do
        type "${i}" >/dev/null 2>&1 ||
            abort "${i} not found.  Please install: ${*}; and try again."
    done
}

## Abort if a copy of VirtualBox is already running
check_running()
{
    VBOXSVC_PID=`pidof VBoxSVC 2> /dev/null`
    if [ -n "$VBOXSVC_PID" ]; then
        if [ -f /etc/init.d/vboxweb-service ]; then
            kill -USR1 $VBOXSVC_PID
        fi
        sleep 1
        if pidof VBoxSVC > /dev/null 2>&1; then
            echo 1>&2 "A copy of VirtualBox is currently running.  Please close it and try again."
            abort "Please note that it can take up to ten seconds for VirtualBox to finish running."
        fi
    fi
}

## Creates a systemd wrapper in /lib for an LSB init script
systemd_wrap_init_script()
{
    self="systemd_wrap_init_script"
    ## The init script to be installed.  The file may be copied or referenced.
    script="$(readlink -f -- "${1}")"
    ## Name for the service.
    name="$2"
    test -x "$script" && test ! "$name" = "" || \
        { echo "$self: invalid arguments" >&2 && return 1; }
    test -d /usr/lib/systemd/system && unit_path=/usr/lib/systemd/system
    test -d /lib/systemd/system && unit_path=/lib/systemd/system
    test -n "${unit_path}" || \
        { echo "$self: systemd unit path not found" >&2 && return 1; }
    conflicts=`sed -n 's/# *X-Conflicts-With: *\(.*\)/\1/p' "${script}" | sed 's/\$[a-z]*//'`
    description=`sed -n 's/# *Short-Description: *\(.*\)/\1/p' "${script}"`
    required=`sed -n 's/# *Required-Start: *\(.*\)/\1/p' "${script}" | sed 's/\$[a-z]*//'`
    required_target=`sed -n 's/# *X-Required-Target-Start: *\(.*\)/\1/p' "${script}"`
    startbefore=`sed -n 's/# *X-Start-Before: *\(.*\)/\1/p' "${script}" | sed 's/\$[a-z]*//'`
    runlevels=`sed -n 's/# *Default-Start: *\(.*\)/\1/p' "${script}"`
    servicetype=`sed -n 's/# *X-Service-Type: *\(.*\)/\1/p' "${script}"`
    test -z "${servicetype}" && servicetype="forking"
    targets=`for i in ${runlevels}; do printf "runlevel${i}.target "; done`
    before=`for i in ${startbefore}; do printf "${i}.service "; done`
    after=`for i in ${required_target}; do printf "${i}.target "; done; for i in ${required}; do printf "${i}.service "; done`
    cat > "${unit_path}/${name}.service" << EOF
[Unit]
SourcePath=${script}
Description=${description}
Before=${targets}shutdown.target ${before}
After=${after}
Conflicts=shutdown.target ${conflicts}

[Service]
Type=${servicetype}
Restart=no
TimeoutSec=5min
IgnoreSIGPIPE=no
KillMode=process
GuessMainPID=no
RemainAfterExit=yes
ExecStart=${script} start
ExecStop=${script} stop

[Install]
WantedBy=multi-user.target
EOF
}

# Checks if systemctl is present and functional (i.e., systemd is the init process).
use_systemd()
{
    systemctl status >/dev/null 2>&1
}

## Installs a file containing a shell script as an init script.  Call
# finish_init_script_install when all scripts have been installed.
install_init_script()
{
    self="install_init_script"
    ## The init script to be installed.  The file may be copied or referenced.
    script="$1"
    ## Name for the service.
    name="$2"

    test -x "${script}" && test ! "${name}" = "" ||
        { echo "${self}: invalid arguments" >&2; return 1; }
    # Do not unconditionally silence the following "ln".
    test -L "/sbin/rc${name}" && rm "/sbin/rc${name}"
    ln -s "${script}" "/sbin/rc${name}"
    if test -x "`which systemctl 2>/dev/null`"; then
        if use_systemd; then
            { systemd_wrap_init_script "$script" "$name"; return; }
        fi
    fi
    if test -d /etc/rc.d/init.d; then
        cp "${script}" "/etc/rc.d/init.d/${name}" &&
            chmod 755 "/etc/rc.d/init.d/${name}"
    elif test -d /etc/init.d; then
        cp "${script}" "/etc/init.d/${name}" &&
            chmod 755 "/etc/init.d/${name}"
    else
        { echo "${self}: error: unknown init type" >&2; return 1; }
    fi
}

## Remove the init script "name"
remove_init_script()
{
    self="remove_init_script"
    ## Name of the service to remove.
    name="$1"

    test -n "${name}" ||
        { echo "$self: missing argument"; return 1; }
    rm -f "/sbin/rc${name}"
    rm -f /lib/systemd/system/"$name".service /usr/lib/systemd/system/"$name".service
    rm -f "/etc/rc.d/init.d/$name"
    rm -f "/etc/init.d/$name"
}

## Tell systemd services have been installed or removed.  Should not be done
# after each individual one, as systemd can crash if it is done too often
# (reported by the OL team for OL 7.6, may not apply to other versions.)
finish_init_script_install()
{
    if use_systemd; then
        systemctl daemon-reload
    fi
}

## Did we install a systemd service?
systemd_service_installed()
{
    ## Name of service to test.
    name="${1}"

    test -f /lib/systemd/system/"${name}".service ||
        test -f /usr/lib/systemd/system/"${name}".service
}

## Perform an action on a service
do_sysvinit_action()
{
    self="do_sysvinit_action"
    ## Name of service to start.
    name="${1}"
    ## The action to perform, normally "start", "stop" or "status".
    action="${2}"

    test ! -z "${name}" && test ! -z "${action}" ||
        { echo "${self}: missing argument" >&2; return 1; }
    if systemd_service_installed "${name}"; then
        systemctl -q ${action} "${name}"
    elif test -x "/etc/rc.d/init.d/${name}"; then
        "/etc/rc.d/init.d/${name}" "${action}" quiet
    elif test -x "/etc/init.d/${name}"; then
        "/etc/init.d/${name}" "${action}" quiet
    fi
}

## Start a service
start_init_script()
{
    do_sysvinit_action "${1}" start
}

## Stop the init script "name"
stop_init_script()
{
    do_sysvinit_action "${1}" stop
}

## Extract chkconfig information from a sysvinit script.
get_chkconfig_info()
{
    ## The script to extract the information from.
    script="${1}"

    set `sed -n 's/# *chkconfig: *\([0-9]*\) *\(.*\)/\1 \2/p' "${script}"`
    ## Which runlevels should we start in?
    runlevels="${1}"
    ## How soon in the boot process will we start, from 00 (first) to 99
    start_order="${2}"
    ## How soon in the shutdown process will we stop, from 99 (first) to 00
    stop_order="${3}"
    test ! -z "${name}" || \
        { echo "${self}: missing name" >&2; return 1; }
    expr "${start_order}" + 0 > /dev/null 2>&1 && \
        expr 0 \<= "${start_order}" > /dev/null 2>&1 && \
        test `expr length "${start_order}"` -eq 2 > /dev/null 2>&1 || \
        { echo "${self}: start sequence number must be between 00 and 99" >&2;
            return 1; }
    expr "${stop_order}" + 0 > /dev/null 2>&1 && \
        expr 0 \<= "${stop_order}" > /dev/null 2>&1 && \
        test `expr length "${stop_order}"` -eq 2 > /dev/null 2>&1 || \
        { echo "${self}: stop sequence number must be between 00 and 99" >&2;
            return 1; }
}

## Add a service to its default runlevels (annotated inside the script, see get_chkconfig_info).
addrunlevel()
{
    self="addrunlevel"
    ## Service name.
    name="${1}"

    test -n "${name}" || \
        { echo "${self}: missing argument" >&2; return 1; }
    systemd_service_installed "${name}" && \
        { systemctl -q enable "${name}"; return; }
    if test -x "/etc/rc.d/init.d/${name}"; then
        init_d_path=/etc/rc.d
    elif test -x "/etc/init.d/${name}"; then
        init_d_path=/etc
    else
        { echo "${self}: error: unknown init type" >&2; return 1; }
    fi
    get_chkconfig_info "${init_d_path}/init.d/${name}" || return 1
    # Redhat based sysvinit systems
    if test -x "`which chkconfig 2>/dev/null`"; then
        chkconfig --add "${name}"
    # SUSE-based sysvinit systems
    elif test -x "`which insserv 2>/dev/null`"; then
        insserv "${name}"
    # Debian/Ubuntu-based systems
    elif test -x "`which update-rc.d 2>/dev/null`"; then
        # Old Debians did not support dependencies
        update-rc.d "${name}" defaults "${start_order}" "${stop_order}"
    # Gentoo Linux
    elif test -x "`which rc-update 2>/dev/null`"; then
        rc-update add "${name}" default
    # Generic sysvinit
    elif test -n "${init_d_path}/rc0.d"
    then
        for locali in 0 1 2 3 4 5 6
        do
            target="${init_d_path}/rc${locali}.d/K${stop_order}${name}"
            expr "${runlevels}" : ".*${locali}" >/dev/null && \
                target="${init_d_path}/rc${locali}.d/S${start_order}${name}"
            test -e "${init_d_path}/rc${locali}.d/"[KS][0-9]*"${name}" || \
                ln -fs "${init_d_path}/init.d/${name}" "${target}"
        done
    else
        { echo "${self}: error: unknown init type" >&2; return 1; }
    fi
}


## Delete a service from a runlevel
delrunlevel()
{
    self="delrunlevel"
    ## Service name.
    name="${1}"

    test -n "${name}" ||
        { echo "${self}: missing argument" >&2; return 1; }
    systemctl -q disable "${name}" >/dev/null 2>&1
    # Redhat-based systems
    chkconfig --del "${name}" >/dev/null 2>&1
    # SUSE-based sysvinit systems
    insserv -r "${name}" >/dev/null 2>&1
    # Debian/Ubuntu-based systems
    update-rc.d -f "${name}" remove >/dev/null 2>&1
    # Gentoo Linux
    rc-update del "${name}" >/dev/null 2>&1
    # Generic sysvinit
    rm -f /etc/rc.d/rc?.d/[SK]??"${name}"
    rm -f /etc/rc?.d/[SK]??"${name}"
}


terminate_proc() {
    PROC_NAME="${1}"
    SERVER_PID=`pidof $PROC_NAME 2> /dev/null`
    if [ "$SERVER_PID" != "" ]; then
        killall -TERM $PROC_NAME > /dev/null 2>&1
        sleep 2
    fi
}


# install_python_bindings(PYTHON_BIN PYTHON_VER)
# failure: non fatal
#
## @todo r=andy Merge this code with darwin/VirtualBox/postflight!
install_python_bindings()
{
    PYTHON_BIN="$1"
    PYTHON_VER="$2"

    # The python binary might not be there, so just exit silently
    if test -z "$PYTHON_BIN"; then
        return 0
    fi

    if test -z "$PYTHON_VER"; then
        echo 1>&2 "missing argument to install_python_bindings"
        return 1
    fi

    echo 1>&2 "Python found: $PYTHON_BIN, installing bindings..."

    # Check if python has working distutils
    "$PYTHON_BIN" -c "from distutils.core import setup" > /dev/null 2>&1
    if test "$?" -ne 0; then
        echo 1>&2 "Python $PYTHON_VER does not have package 'distutils', checking for 'setuptools'..."
        # Since Python 3.12 there are no distutils anymore. See PEP632.
        "$PYTHON_BIN" -c "from setuptools import setup" > /dev/null 2>&1
        if test "$?" -ne 0; then
            echo 1>&2 "Python $PYTHON_VER also does not have package 'setuptools'. Skipping installation."
            return 0
        fi
        # When we reach here, we have to use 'pip' in order to install our bindings (Python >= 3.12).
        if test -x "`which pip 2>/dev/null`"; then
            PYTHON_PIP_BIN=$(which pip)
        else
            echo 1>&2 "Python package manager 'pip' not found. Skipping installation."
        fi
    fi

    PYTHON_INSTALLER_PATH="$VBOX_INSTALL_PATH/sdk/installer/python"

    # Pass install path via environment
    export VBOX_INSTALL_PATH

    if [ -n "$PYTHON_PIP_BIN" ]; then
        # Note: We use '-v' to show verbose output of our setup.py script on error.
        $SHELL -c "cd ${PYTHON_INSTALLER_PATH} && ${PYTHON_PIP_BIN} -v install ./vboxapi"
    else
        $SHELL -c "cd ${PYTHON_INSTALLER_PATH} && ${PYTHON_BIN} ./vboxapisetup.py install \
                   --record $CONFIG_DIR/python-$CONFIG_FILES"
        cat "$CONFIG_DIR/python-$CONFIG_FILES" >> "$CONFIG_DIR/$CONFIG_FILES"
        rm -f "$CONFIG_DIR/python-$CONFIG_FILES"
    fi

    # Remove files created by Python API setup.
    rm -rf "$PYTHON_INSTALLER_PATH/build"
}

## @todo r=andy Merge this code with darwin/VirtualBox/postflight!
maybe_run_python_bindings_installer() {
    VBOX_INSTALL_PATH="${1}"

    # Loop over all usual suspect Python executable names and try installing
    # the VirtualBox API bindings. Needs to prevent double installs which waste
    # quite a bit of time.
    PYTHON_VER_INSTALLED=""
    PYTHON_BINARIES="\
        python2.7 \
        python2   \
        python3.3 \
        python3.4 \
        python3.5 \
        python3.6 \
        python3.7 \
        python3.8 \
        python3.9 \
        python3.10 \
        python3.11 \
        python3.12 \
        python3 \
        python"

    for PYTHON_BIN in $PYTHON_BINARIES; do
        if [ "`$PYTHON_BIN -c 'import sys
if sys.version_info >= (2, 6) and (sys.version_info < (3, 0) or sys.version_info >= (3, 3)):
    print(\"test\")' 2> /dev/null`" != "test" ]; then
            continue
        fi
        # Get python major/minor version, and skip if it was already covered.
        # Uses grep -F to avoid trouble with '.' matching any char.
        PYTHON_VER="`$PYTHON_BIN -c 'import sys
print("%s.%s" % (sys.version_info[0], sys.version_info[1]))' 2> /dev/null`"
        if echo "$PYTHON_VER_INSTALLED" | grep -Fq ":$PYTHON_VER:"; then
            continue
        fi
        # Record which version will be installed. If it fails there is no point
        # trying with different executable/symlink reporting the same version.
        PYTHON_VER_INSTALLED="$PYTHON_VER_INSTALLED:$PYTHON_VER:"
        install_python_bindings "$PYTHON_BIN" "$PYTHON_VER"
    done
    if [ -z "$PYTHON_VER_INSTALLED" ]; then
        echo 1>&2 "Python (2.7 or 3.3 and later) unavailable, skipping bindings installation."
        return 1
    fi

    return 0
}
