#!/usr/bin/env bash
# =============================================================================
#  start-tomcat.sh - install (offline) and start an Apache Tomcat server
#                    on RHEL 9.6, ready for performance testing
# =============================================================================
#
#  USAGE (as root)
#      ./start-tomcat.sh            install if needed, configure, start
#      ./start-tomcat.sh start      same as above
#      ./start-tomcat.sh stop       stop the kit's Tomcat
#      ./start-tomcat.sh restart    stop, then start again
#      ./start-tomcat.sh status     show whether Tomcat is running, with live counters
#      ./start-tomcat.sh cleanup    stop Tomcat and remove everything the kit created
#
#  THE KIT'S OWN TOMCAT INSTANCE
#      RHEL can run several Tomcat "instances" from one installation. The kit
#      creates its own, so the normal Tomcat (/etc/tomcat, tomcat.service)
#      is never changed:
#          service          tomcat@perftest.service
#          instance folder  /var/lib/tomcats/perftest/  (conf, webapps, logs, work, temp)
#          Java settings    /etc/sysconfig/tomcat@perftest
#
#  WHAT "start" DOES, STEP BY STEP
#      1. Checks the operating system (RHEL 9 expected).
#      2. Installs tomcat, Java (OpenJDK headless) and httpd-tools (for the
#         "ab" load tool) if missing, WITHOUT internet:
#           a) from RPM files in ./rpms/  (see download-rpms.sh), or
#           b) from a local dnf repository already set up on the host
#              (for example the mounted RHEL 9.6 DVD / ISO).
#      3. Checks that the port is free.
#      4. Creates the instance folder with its configuration (server.xml ...).
#      5. Copies the test web application to webapps/perf-test/ and adds
#         static test files.
#      6. Writes the Java settings (heap size, garbage collector, GC log).
#      7. Writes a systemd drop-in for the instance (open-files limit).
#      8. Starts Tomcat, waits until the first page is served, and opens
#         every test page once (Tomcat compiles each JSP on first use).
#
#  "cleanup" undoes steps 4 to 7. Installed packages are kept.
# =============================================================================

set -euo pipefail
source "$(dirname -- "${BASH_SOURCE[0]}")/lib/common.sh"

# Marker written into every file the kit creates.
MARKER="Created by the Tomcat performance kit ($KIT_DIR/start-tomcat.sh)"

PACKAGES=(tomcat "java-${JAVA_VERSION}-openjdk-headless" httpd-tools)


# ----------------------------------------------------------------------------
#  Step 1 - operating system check
# ----------------------------------------------------------------------------
check_operating_system() {
    # /etc/os-release defines NAME, VERSION_ID, ...
    source /etc/os-release

    if [[ ${VERSION_ID%%.*} != 9 ]]; then
        die "This kit is made for RHEL 9. Found: $PRETTY_NAME"
    fi
    if [[ $ID != rhel || $VERSION_ID != 9.6 ]]; then
        warn "Target is RHEL 9.6; this host is '$PRETTY_NAME'. Continuing."
    fi
    log "Operating system: $PRETTY_NAME"
}


# ----------------------------------------------------------------------------
#  Step 2 - install Tomcat and Java without internet access
# ----------------------------------------------------------------------------
install_packages_offline() {
    if rpm -q "${PACKAGES[@]}" >/dev/null 2>&1; then
        log "Already installed: $(rpm -q tomcat), $(rpm -q "java-${JAVA_VERSION}-openjdk-headless")"
        return
    fi

    local rpm_dir="$KIT_DIR/rpms"

    if compgen -G "$rpm_dir/*.rpm" >/dev/null; then
        # a) RPM files shipped next to this script. All other repositories
        #    are disabled so dnf never tries to reach the internet.
        log "Installing ${PACKAGES[*]} from RPM files in $rpm_dir"
        dnf install -y --disablerepo='*' "$rpm_dir"/*.rpm
    else
        # b) A local repository already configured on this host (RHEL DVD).
        log "Installing ${PACKAGES[*]} from the local dnf repositories"
        if ! dnf install -y "${PACKAGES[@]}"; then
            die "Could not install the packages offline.
       Either copy RPMs into $rpm_dir (see download-rpms.sh),
       or mount the RHEL 9.6 DVD and configure it as a local repository."
        fi
    fi

    # Check that everything really arrived (the rpms/ folder may be incomplete).
    local package
    for package in "${PACKAGES[@]}"; do
        rpm -q "$package" >/dev/null || die "Package $package is still missing."
    done
    ok "Installed $(rpm -q tomcat) and $(rpm -q "java-${JAVA_VERSION}-openjdk-headless")"
}

check_java() {
    if [[ ! -x $JAVA_HOME_DIR/bin/java ]]; then
        die "Java $JAVA_VERSION not found at $JAVA_HOME_DIR.
       Install java-${JAVA_VERSION}-openjdk-headless, or change JAVA_VERSION in settings.conf."
    fi
    log "Java: $("$JAVA_HOME_DIR/bin/java" -version 2>&1 | head -1)"
}


# ----------------------------------------------------------------------------
#  Step 3 - port check
# ----------------------------------------------------------------------------

# Process ID of the program listening on TCP port $1, or nothing if it is free.
port_owner_pid() {
    ss -Hltnp "sport = :$1" 2>/dev/null | grep -o 'pid=[0-9]*' | head -1 | cut -d= -f2 || true
}

check_port() {
    local owner_pid
    owner_pid="$(port_owner_pid "$TOMCAT_PORT")"

    if [[ -n $owner_pid && $owner_pid != "$(tomcat_pid)" ]]; then
        local owner_name
        owner_name="$(ps -o comm= -p "$owner_pid" 2>/dev/null || echo unknown)"
        die "Port $TOMCAT_PORT is already used by '$owner_name' (PID $owner_pid).
       If it is the normal Tomcat:  systemctl stop tomcat
       Or choose another port in settings.conf (TOMCAT_PORT), for example 8081."
    fi

    # On RHEL 9 Tomcat's SELinux domain may use any port, but the ports
    # labelled for web servers are the safe choice.
    if selinux_is_enabled && command -v semanage >/dev/null; then
        if ! semanage port -l | grep -E '^http_(cache_)?port_t +tcp' |
                grep -qE "[ ,]$TOMCAT_PORT(,|$)"; then
            warn "Port $TOMCAT_PORT is not labelled as a web port for SELinux."
            warn "If Tomcat cannot open it:  semanage port -a -t http_cache_port_t -p tcp $TOMCAT_PORT"
        fi
    fi
    log "Port $TOMCAT_PORT is available"
}


# ----------------------------------------------------------------------------
#  Step 4 - the instance folder and its configuration
# ----------------------------------------------------------------------------
#  /var/lib/tomcats/perftest/
#      conf/      server.xml (written here), web.xml, context.xml, ...
#      webapps/   perf-test/  (the test application)
#      logs/      catalina.*.log, gc.log ...
#      work/      compiled JSP pages
#      temp/      temporary files
create_instance_folder() {
    log "Creating the instance folder $CATALINA_BASE"
    mkdir -p "$CATALINA_BASE"/{conf,webapps,logs,work,temp}

    # Start from RHEL's standard files ...
    local file
    for file in web.xml context.xml catalina.properties logging.properties; do
        cp -f "/etc/tomcat/$file" "$CATALINA_BASE/conf/$file"
    done

    # ... switch off JSP "development mode" (see settings.conf) ...
    set_jsp_development_mode

    # ... and write our own server.xml.
    write_server_xml

    # Tomcat (user "tomcat") reads conf/ and webapps/, and writes the rest.
    chown root:tomcat "$CATALINA_BASE" "$CATALINA_BASE"/{conf,webapps}
    chmod 0755 "$CATALINA_BASE" "$CATALINA_BASE/webapps"
    chmod 0750 "$CATALINA_BASE/conf"
    chown root:tomcat "$CATALINA_BASE"/conf/*
    chmod 0640 "$CATALINA_BASE"/conf/*
    chown -R tomcat:tomcat "$CATALINA_BASE"/{logs,work,temp}
    chmod 0770 "$CATALINA_BASE"/{logs,work,temp}
}

# Add the "development" setting to the JSP engine in conf/web.xml.
# The line we look for:  <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
set_jsp_development_mode() {
    local web_xml="$CATALINA_BASE/conf/web.xml"
    local anchor='<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>'

    if [[ $(grep -cF "$anchor" "$web_xml") != 1 ]]; then
        warn "Could not find the JSP engine in $web_xml; JSP development mode left unchanged."
        return
    fi
    sed -i "s|$anchor|$anchor\\
        <init-param>  <!-- added by the Tomcat performance kit -->\\
            <param-name>development</param-name>\\
            <param-value>$JSP_DEVELOPMENT_MODE</param-value>\\
        </init-param>|" "$web_xml"
}

write_server_xml() {
    local access_log_valve="<!-- access log switched off (TEST_ACCESS_LOG=off in settings.conf) -->"
    if [[ $TEST_ACCESS_LOG == on ]]; then
        access_log_valve='<Valve className="org.apache.catalina.valves.AccessLogValve"
               directory="logs" prefix="perf-test-access" suffix=".log"
               pattern="%h %l %u %t &quot;%r&quot; %s %b %D" buffered="true" />'
    fi

    log "Writing $CATALINA_BASE/conf/server.xml"
    cat > "$CATALINA_BASE/conf/server.xml" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!--
  $MARKER
  Values come from settings.conf. Re-run ./start-tomcat.sh after changing them.
-->

<!-- port="-1": no shutdown port. Tomcat is stopped with a signal (systemctl stop). -->
<Server port="-1" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <Service name="Catalina">

    <!-- The HTTP connector: receives the requests.
         NIO = a few poller threads watch every open connection; a worker
         thread (at most maxThreads) is used only while a request is processed. -->
    <Connector protocol="org.apache.coyote.http11.Http11NioProtocol"
               address="$TOMCAT_HOST"
               port="$TOMCAT_PORT"
               maxThreads="$MAX_THREADS"
               minSpareThreads="$MIN_SPARE_THREADS"
               maxConnections="$MAX_CONNECTIONS"
               acceptCount="$ACCEPT_COUNT"
               connectionTimeout="$CONNECTION_TIMEOUT_MS"
               keepAliveTimeout="$KEEPALIVE_TIMEOUT_MS"
               maxKeepAliveRequests="$MAX_KEEPALIVE_REQUESTS"
               compression="$COMPRESSION"
               compressionMinSize="$COMPRESSION_MIN_BYTES"
               compressibleMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml" />

    <Engine name="Catalina" defaultHost="localhost">
      <!-- autoDeploy="false": Tomcat does not keep scanning webapps/ for changes. -->
      <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="false">
        $access_log_valve
      </Host>
    </Engine>
  </Service>
</Server>
EOF
}


# ----------------------------------------------------------------------------
#  Step 5 - the test web application
# ----------------------------------------------------------------------------
create_test_webapp() {
    log "Installing the test application in $WEBAPP_DIR"
    rm -rf "$WEBAPP_DIR"
    mkdir -p "$WEBAPP_DIR/WEB-INF"

    # Dynamic pages (JSP) and META-INF/context.xml from the kit's webapp/ folder.
    cp -r "$KIT_DIR/webapp/." "$WEBAPP_DIR/"

    # The application's own settings.
    cat > "$WEBAPP_DIR/WEB-INF/web.xml" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!-- $MARKER -->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <display-name>Tomcat performance test</display-name>

    <!-- Minutes an unused session is kept. -->
    <session-config>
        <session-timeout>$SESSION_TIMEOUT_MINUTES</session-timeout>
    </session-config>
</web-app>
EOF

    # Health page: the scripts read it to know Tomcat is really serving.
    echo "tomcat-perf-test-ok" > "$WEBAPP_DIR/health.txt"

    # 1 KB HTML page - a typical small static request.
    {
        echo "<html><head><title>perf test</title></head><body><pre>"
        head -c 700 /dev/zero | tr '\0' 'x' | fold -w 70
        echo "</pre></body></html>"
    } > "$WEBAPP_DIR/small.html"

    # About 40 KB of HTML text - like a real page (compression test).
    # It stays below 48 KB: Tomcat sends larger files with "sendfile",
    # and files sent that way are never compressed.
    # A fixed random seed makes the page identical on every run.
    awk 'BEGIN {
        srand(42)
        print "<!DOCTYPE html><html><head><title>perf test - text page</title></head><body>"
        print "<h1>Monthly service report</h1><table>"
        for (row = 1; row <= 400; row++) {
            printf "<tr><td>%05d</td><td>host-%03d.example.lan</td><td>%s</td><td>%d ms</td><td>%d requests</td></tr>\n",
                   row, int(rand() * 500), (rand() < 0.9 ? "OK" : "WARNING"),
                   int(rand() * 900) + 1, int(rand() * 100000)
        }
        print "</table></body></html>"
    }' > "$WEBAPP_DIR/text.html"

    # 1 MB binary file - a download (transfer-rate test).
    head -c 1048576 /dev/urandom > "$WEBAPP_DIR/large.bin"

    # Readable for the user "tomcat", owned by root (Tomcat cannot change it).
    chown -R root:tomcat "$WEBAPP_DIR"
    find "$WEBAPP_DIR" -type d -exec chmod 0755 {} +
    find "$WEBAPP_DIR" -type f -exec chmod 0644 {} +

    # Give every file the SELinux label of Tomcat's data (tomcat_var_lib_t).
    if command -v restorecon >/dev/null; then
        restorecon -R "$CATALINA_BASE"
    fi
}


# ----------------------------------------------------------------------------
#  Step 6 - Java settings for the instance
# ----------------------------------------------------------------------------
#  tomcat@perftest.service reads /etc/tomcat/tomcat.conf first, then this
#  file, so the values here win.
write_java_settings() {
    log "Writing $INSTANCE_ENV_FILE (heap ${HEAP_INITIAL_MB}-${HEAP_MAX_MB} MB, Java $JAVA_VERSION)"

    # -Xlog:gc writes one line per garbage collection to logs/gc.log
    # (kept in 5 files of 20 MB); the "gc" test reads the pause times there.
    local gc_log_option="-Xlog:gc:file=$GC_LOG:uptime,level,tags:filecount=5,filesize=20m"

    cat > "$INSTANCE_ENV_FILE" <<EOF
# $MARKER
# Settings of the Tomcat instance "$INSTANCE_NAME" (tomcat@$INSTANCE_NAME.service).
# Values come from settings.conf.

# Which Java runs Tomcat.
JAVA_HOME="$JAVA_HOME_DIR"

# Java memory (heap) and garbage collector, plus the GC log.
CATALINA_OPTS="-Xms${HEAP_INITIAL_MB}m -Xmx${HEAP_MAX_MB}m $GC_OPTIONS $gc_log_option $EXTRA_JAVA_OPTS"

# Temporary files of this instance.
CATALINA_TMPDIR="$CATALINA_BASE/temp"
EOF
    chmod 0644 "$INSTANCE_ENV_FILE"
    command -v restorecon >/dev/null && restorecon "$INSTANCE_ENV_FILE"
    return 0
}


# ----------------------------------------------------------------------------
#  Step 7 - systemd drop-in for the instance
# ----------------------------------------------------------------------------
write_systemd_dropin() {
    if ! has_systemd; then
        return      # without systemd, tomcat_start sets these itself
    fi
    log "Writing $SYSTEMD_DROPIN"
    mkdir -p "$(dirname "$SYSTEMD_DROPIN")"
    cat > "$SYSTEMD_DROPIN" <<EOF
# $MARKER
# Delete this file (and run "systemctl daemon-reload") to undo.
[Service]
# Every connection is an open file; the RHEL default (1024) is too low.
LimitNOFILE=65536

# server.xml has no shutdown port, so do not run "server stop"; systemd
# stops Tomcat with a normal TERM signal instead (exit code 143 = success).
ExecStop=
EOF
    systemctl daemon-reload
}


# ----------------------------------------------------------------------------
#  Step 8 - start Tomcat
# ----------------------------------------------------------------------------
start_daemon() {
    # A full restart is needed so every new setting takes effect.
    if tomcat_is_running; then
        log "Tomcat ($SERVICE_NAME) is running - restarting it to apply the configuration"
        tomcat_restart
    else
        log "Starting Tomcat ($SERVICE_NAME)"
        tomcat_start
    fi

    if ! wait_until_serving 120; then
        die "Tomcat did not answer $HEALTH_URL within 120 s.
       Look at:  journalctl -u $SERVICE_NAME   and   $LOG_DIR/"
    fi
}

# Tomcat translates each JSP page into Java code and compiles it the first
# time it is requested. Do that now, so the tests measure normal requests.
compile_test_pages() {
    log "Opening every test page once (Tomcat compiles each JSP on first use)"
    local page
    for page in hello.jsp slow.jsp?ms=1 session.jsp status.jsp; do
        if ! curl --noproxy '*' -sf --max-time 60 -o /dev/null "$BASE_URL/perf-test/$page"; then
            die "The test page $BASE_URL/perf-test/$page does not work.
       Look at:  $LOG_DIR/localhost.*.log  and  $LOG_DIR/catalina.*.log"
        fi
    done
}

print_summary() {
    local status
    status="$(tomcat_status)"

    echo
    ok "Tomcat is running and serving pages."
    echo "    Tomcat       : $(status_value "$status" tomcat_version)"
    echo "    Java         : $(status_value "$status" jvm_version)"
    echo "    Process ID   : $(tomcat_pid)"
    echo "    Connector    : $(status_value "$status" connector), up to $MAX_THREADS worker threads, $MAX_CONNECTIONS connections"
    echo "    Heap         : max $(status_value "$status" heap_max_mb) MB, used now $(status_value "$status" heap_used_mb) MB"
    echo "    Test pages   : $BASE_URL/perf-test/  (small.html, hello.jsp, ...)"
    echo "    Status page  : $STATUS_URL"
    echo "    Instance     : $CATALINA_BASE"
    echo "    SELinux      : $(getenforce 2>/dev/null || echo 'not available')"
    if has_systemd; then
        echo "    Managed by   : systemd  (systemctl status $SERVICE_NAME)"
    else
        echo "    Managed by   : this script (no systemd found, e.g. inside a container)"
    fi
    echo
    echo "    Next step    : ./test-tomcat.sh"
}


# ----------------------------------------------------------------------------
#  Other actions
# ----------------------------------------------------------------------------
show_status() {
    if tomcat_is_serving; then
        ok "Tomcat is running (PID $(tomcat_pid)) and serving $HEALTH_URL"
        # The live counters are extra information; never fail on them.
        tomcat_status | sed 's/^/    /' || true
    elif tomcat_is_running; then
        warn "The Tomcat process exists (PID $(tomcat_pid)), but $HEALTH_URL does not answer."
        warn "It may still be starting. Logs: $LOG_DIR/"
        exit 1
    else
        warn "Tomcat ($SERVICE_NAME) is not running."
        exit 1
    fi
}

cleanup_test_setup() {
    if tomcat_is_running; then
        log "Stopping Tomcat ($SERVICE_NAME)"
        tomcat_stop
    fi

    if [[ -f $SYSTEMD_DROPIN ]]; then
        log "Removing $SYSTEMD_DROPIN"
        rm -f "$SYSTEMD_DROPIN"
        rmdir --ignore-fail-on-non-empty "$(dirname "$SYSTEMD_DROPIN")"
        systemctl daemon-reload
    fi

    if [[ -f $INSTANCE_ENV_FILE ]]; then
        log "Removing $INSTANCE_ENV_FILE"
        rm -f "$INSTANCE_ENV_FILE"
    fi

    if [[ -d $CATALINA_BASE ]]; then
        log "Removing $CATALINA_BASE (including its logs)"
        rm -rf "$CATALINA_BASE"
    fi
    ok "Cleanup finished. The packages (tomcat, java, httpd-tools) are still installed."
}


# ----------------------------------------------------------------------------
#  Main
# ----------------------------------------------------------------------------
main() {
    local action="${1:-start}"
    require_root "$@"

    case "$action" in
        start)
            check_operating_system
            install_packages_offline
            check_java
            check_port
            create_instance_folder
            create_test_webapp
            write_java_settings
            write_systemd_dropin
            start_daemon
            compile_test_pages
            print_summary
            ;;
        stop)
            tomcat_stop
            ok "Tomcat stopped."
            ;;
        restart)
            tomcat_restart
            wait_until_serving 120 || die "Tomcat did not come back. See $LOG_DIR/"
            ok "Tomcat restarted."
            ;;
        status)
            show_status
            ;;
        cleanup)
            cleanup_test_setup
            ;;
        *)
            echo "Usage: $0 [start|stop|restart|status|cleanup]"
            exit 2
            ;;
    esac
}

main "$@"
