diff --git a/server/basedir-includes/oieserver b/server/basedir-includes/oieserver new file mode 100755 index 000000000..b1180e5db --- /dev/null +++ b/server/basedir-includes/oieserver @@ -0,0 +1,281 @@ +#!/usr/bin/env bash +# +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2025 Tony Germano and Mitch Gaffigan +# + +# ============================================================================= +# Open Integration Engine Server Launcher Script +# +# Description: +# This script is the main launcher for the Open Integration Engine (OIE) +# server. It prepares the Java environment and executes the server launcher +# JAR file. +# +# The script automatically finds a compatible Java runtime (version 17+ by +# default) by searching for a valid executable in the following priority order: +# 1. The OIE_JAVA_PATH environment variable. +# 2. The -java-cmd directive in the oieserver.vmoptions file or included +# .vmoptions files. Must specify the path to the 'java' executable. +# This is the preferred way to declare the desired version for running +# the server and can be overridden by OIE_JAVA_PATH. Can be a relative +# path from the location of this script. +# 3. The JAVA_HOME environment variable. +# 4. The 'java' command available in the system's PATH. +# +# It also parses the 'oieserver.vmoptions' file to configure JVM options, +# system properties (-D...), and classpath modifications. +# +# Usage: +# ./oieserver.sh [app-arguments] +# +# All [app-arguments] are passed directly to the underlying Java application +# (com.mirth.connect.server.launcher.MirthLauncher). +# +# Configuration via Environment Variables: +# OIE_JAVA_PATH - (Highest priority) Set the full path to the 'java' +# executable to be used. Can be a relative path from the +# location of this script or a tilde path +# (e.g., ~/path/to/java). +# JAVA_HOME - Set the path to the root of a Java installation. The +# script will look for 'bin/java' within this path. +# ============================================================================= + +APP_ARGS=("$@") +MIN_JAVA_VERSION=17 + +# Set OIE_HOME to the script directory +OIE_HOME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +# The engine expects it's working directory to be OIE_HOME +if ! cd "$OIE_HOME"; then + echo "Error: Could not change to the OIE_HOME directory: $OIE_HOME" >&2 + exit 1 +fi +LAUNCHER_JAR="$OIE_HOME/mirth-server-launcher.jar" +CLASSPATH="$LAUNCHER_JAR" +VMOPTIONS=() +# This will hold the validated path to the Java executable. It is intentionally left empty for now. +FINAL_JAVA_CMD="" +# This will temporarily hold the result from parsing the vmoptions file. +VMOPTIONS_JAVA_CMD="" +VMOPTIONS_JAVA_CMD_FILE="" + + +# --- Function to resolve a path to a canonical absolute path --- +# Resolves a given path, handling tilde, relative, and '..' components. +# @param $1: The path to resolve. +# @echo: The resolved, canonical absolute path. +resolve_canonical_path() { + local path_to_resolve="$1" + + # Explicitly handle tilde expansion first + path_to_resolve=$(sed -E "s,^~(/|$),${HOME}\1," <<< "$path_to_resolve") + + # If the path is not absolute, assume it's relative to OIE_HOME + if [[ ! "$path_to_resolve" =~ ^/ ]]; then + path_to_resolve="$OIE_HOME/$path_to_resolve" + fi + + # Use cd and pwd to resolve '..' and '.' components for a canonical path. + if [[ -d "$(dirname "$path_to_resolve")" ]]; then + echo "$(cd "$(dirname "$path_to_resolve")" && pwd)/$(basename "$path_to_resolve")" + else + echo "$path_to_resolve" + fi +} + +# --- Function to expand environment variable in a string --- +# @param $1: The line to expand +# @echo: The expanded line +expand_line_variables() { + local line="$1" + local result_line="" + + # This loop consumes the line from left to right, preventing recursive expansion. + while [[ "$line" =~ (\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}) ]]; do + # Append the text before the match to our result. + result_line+="${line%%"${BASH_REMATCH[0]}"*}" + + # Get the variable name and its value. + local var_name="${BASH_REMATCH[2]}" + # Use indirect expansion to get the variable's value. Do not replace if not set. + if [[ -v "$var_name" ]]; then + local var_value="${!var_name}" + result_line+="$var_value" + else + result_line+="${BASH_REMATCH[0]}" + fi + + # Update the line to be only the portion *after* the match. + line="${line#*"${BASH_REMATCH[0]}"}" + done + + # Append any remaining part of the line after the last match and return. + echo "$result_line$line" +} + +# --- Function to validate Java version --- +# Checks if a given command points to a Java executable of the required minimum version. +# @param $1: The java command or path to check +# @return: 0 on success (is valid), 1 on failure. +is_valid_java_version() { + local java_cmd="$1" + + # Check if the command is found and is executable + if ! command -v "$java_cmd" &> /dev/null || ! [[ -x "$(command -v "$java_cmd")" ]]; then + return 1 + fi + + # Execute 'java -version' and capture the output from stderr + # Example output: openjdk version "17.0.2" 2022-07-19 + local version_output + version_output=$("$java_cmd" -version 2>&1) + + # Check if the version command succeeded + if [[ $? -ne 0 ]]; then + return 1 + fi + + # Extract the major version number. This works for formats like "1.8.0" and "17.0.2". + local major_version + major_version=$(echo "$version_output" | head -n 1 | cut -d '"' -f 2 | cut -d '.' -f 1) + + # Check if the extracted version is a number and meets the minimum requirement + if [[ "$major_version" =~ ^[0-9]+$ ]] && [[ "$major_version" -ge "$MIN_JAVA_VERSION" ]]; then + return 0 # Success + else + return 1 # Failure + fi +} + +# Set Java options by parsing the vmoptions file +parse_vmoptions() { + local file="$1" + + if [[ ! -f "$file" ]]; then + echo "Warning: VM options file not found: $file" >&2 + return 1 + fi + + # Read the file line by line + while IFS= read -r line; do + # Trim leading/trailing whitespace + line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + # Skip empty lines and comments + if [[ -z "$line" || "$line" =~ ^# ]]; then + continue + fi + + line=$(expand_line_variables "$line") + + # Check for -include-options directive + if [[ "$line" =~ ^-include-options[[:space:]]+(.+) ]]; then + local included_file="${BASH_REMATCH[1]}" + # Resolve relative paths + if [[ ! "$included_file" =~ ^/ ]]; then # Not an absolute path + included_file="$(dirname "$file")/$included_file" + fi + # Recursively call parse_vmoptions for the included file + parse_vmoptions "$included_file" + elif [[ "$line" =~ ^-classpath[[:space:]]+(.+) ]]; then + # Handle -classpath directive + CLASSPATH="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^-classpath/a[[:space:]]+(.+) ]]; then + # Handle -classpath/a directive (append to existing classpath) + CLASSPATH="${CLASSPATH}:${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^-classpath/p[[:space:]]+(.+) ]]; then + # Handle -classpath/p directive (prepend to existing classpath) + CLASSPATH="${BASH_REMATCH[1]}:${CLASSPATH}" + elif [[ "$line" =~ ^-java-cmd[[:space:]]+(.+) ]]; then + # Handle -java-cmd directive + # Store the path and the file it was found in. Validation is deferred. + VMOPTIONS_JAVA_CMD=$(resolve_canonical_path "${BASH_REMATCH[1]}") + VMOPTIONS_JAVA_CMD_FILE="$file" + else + # Add the option to the accumulated string + VMOPTIONS+=("$line") + fi + done < "$file" + return 0 +} + +# --- Main Logic --- + +# 1. Recursively parse the VM options file to populate vmoptions variables. +parse_vmoptions "$OIE_HOME/oieserver.vmoptions" + +# 2. Ensure the launcher JAR is always in the classpath. +case "$CLASSPATH" in + *"$LAUNCHER_JAR"*) + # It's already there, do nothing. + ;; + *) + # Prepend the launcher JAR if a custom classpath was provided. + echo "Info: Prepending mirth-server-launcher.jar to the classpath." + CLASSPATH="$LAUNCHER_JAR:$CLASSPATH" + ;; +esac + +# 3. Discover the Java executable using the documented priority order. + +# Check OIE_JAVA_PATH (fail-fast on invalid). +if [[ -n "$OIE_JAVA_PATH" ]]; then + resolved_path=$(resolve_canonical_path "$OIE_JAVA_PATH") + if is_valid_java_version "$resolved_path"; then + echo "Info: Found suitable java version specified by the OIE_JAVA_PATH environment variable" + FINAL_JAVA_CMD="$resolved_path" + else + echo "Error: '$resolved_path' is specified by the OIE_JAVA_PATH environment variable, which is not a valid Java executable of at least version $MIN_JAVA_VERSION. Exiting." >&2 + exit 1 + fi +fi + +# Check -java-cmd from vmoptions (fail-fast on invalid, only if not already found). +if [[ -z "$FINAL_JAVA_CMD" ]] && [[ -n "$VMOPTIONS_JAVA_CMD" ]]; then + if is_valid_java_version "$VMOPTIONS_JAVA_CMD"; then + echo "Info: Found suitable java version specified by the -java-cmd directive in '$VMOPTIONS_JAVA_CMD_FILE'" + FINAL_JAVA_CMD="$VMOPTIONS_JAVA_CMD" + else + echo "Error: '$VMOPTIONS_JAVA_CMD' is specified by the -java-cmd directive in '$VMOPTIONS_JAVA_CMD_FILE', which is not a valid Java executable of at least version $MIN_JAVA_VERSION. Exiting." >&2 + exit 1 + fi +fi + +# Check JAVA_HOME (no fail-fast). +if [[ -z "$FINAL_JAVA_CMD" ]] && [[ -d "$JAVA_HOME" ]]; then + java_home_path="$JAVA_HOME/bin/java" + if is_valid_java_version "$java_home_path"; then + echo "Info: Found suitable java version specified by the JAVA_HOME environment variable" + FINAL_JAVA_CMD="$java_home_path" + else + echo "Warning: '$java_home_path' is specified by the JAVA_HOME environment variable, which is not a valid Java executable of at least version $MIN_JAVA_VERSION. Ignoring." >&2 + fi +fi + +# Check system PATH (no fail-fast). +if [[ -z "$FINAL_JAVA_CMD" ]]; then + if is_valid_java_version "java"; then + echo "Info: Found suitable java version in the PATH" + FINAL_JAVA_CMD="java" + else + echo "Warning: 'java' does not exist in your PATH or is not a valid Java executable of at least version $MIN_JAVA_VERSION." >&2 + fi +fi + +# 4. Final check for a valid Java path before execution. +if [[ -z "$FINAL_JAVA_CMD" ]]; then + echo "Error: Could not find a Java ${MIN_JAVA_VERSION}+ installation." >&2 + echo "Please configure -java-cmd in conf/custom.vmoptions, set OIE_JAVA_PATH, set JAVA_HOME, or ensure 'java' in your PATH is version ${MIN_JAVA_VERSION} or higher." >&2 + exit 1 +fi + +JAVA_OPTS=("${VMOPTIONS[@]}" + "-cp" "$CLASSPATH" + "com.mirth.connect.server.launcher.MirthLauncher" + "${APP_ARGS[@]}") + +# Launch Open Integration Engine (as this PID with exec) +echo "Starting Open Integration Engine..." +echo "$FINAL_JAVA_CMD ${JAVA_OPTS[*]}" +exec "$FINAL_JAVA_CMD" "${JAVA_OPTS[@]}" diff --git a/server/basedir-includes/oieserver.ps1 b/server/basedir-includes/oieserver.ps1 new file mode 100644 index 000000000..79ff35350 --- /dev/null +++ b/server/basedir-includes/oieserver.ps1 @@ -0,0 +1,292 @@ +# +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2025 Tony Germano and Mitch Gaffigan +# + +<# +.SYNOPSIS + Open Integration Engine Server Launcher Script (PowerShell Version) + +.DESCRIPTION + This script is the main launcher for the Open Integration Engine (OIE) + server. It prepares the Java environment and executes the server launcher + JAR file. + + The script automatically finds a compatible Java runtime (version 17+ by + default) by searching for a valid executable in the following priority order: + + 1. The OIE_JAVA_PATH environment variable. + 2. The -java-cmd directive in the oieserver.vmoptions file or included .vmoptions + files. Must specify the path to the 'java' executable. This is the preferred + way to declare the desired version for running the server and can be overridden + by OIE_JAVA_PATH. Can be a relative path from the location of this script. + 3. The JAVA_HOME environment variable. + 4. The 'java' command available in the system's PATH. + + It also parses the 'oieserver.vmoptions' file to configure JVM options, + system properties (-D...), and classpath modifications. + +.NOTES + Configuration via Environment Variables: + OIE_JAVA_PATH - (Highest priority) Set the full path to the 'java' + executable to be used. Can be a relative path from the + location of this script or a tilde path + (e.g., ~/path/to/java). + JAVA_HOME - Set the path to the root of a Java installation. The + script will look for 'bin/java' within this path. + +.PARAMETER AppArgs + Arguments passed directly to the underlying Java application + (com.mirth.connect.server.launcher.MirthLauncher). + +.EXAMPLE + ./oieserver.ps1 + +.EXAMPLE + $env:OIE_JAVA_PATH = 'C:\path\to\java.exe'; + ./oieserver.ps1 -Dproperty=value --some-arg value +#> + +param( + [parameter(ValueFromRemainingArguments = $true)][string[]] $AppArgs +) + +# Stop on any error and exit non-zero +$ErrorActionPreference = "Stop" +$MinJavaVersion = 17 + +# Set OieHome to the script directory using PowerShell's built-in variable +$OieHome = $PSScriptRoot +$LauncherJar = Join-Path -Path $OieHome -ChildPath "mirth-server-launcher.jar" + # Use script scope to be modifiable by functions +$script:Classpath = [System.Collections.Generic.List[string]]::new() +$script:Classpath.Add($LauncherJar) +$script:VmOptions = [System.Collections.Generic.List[string]]::new() + +# This will hold the validated path to the Java executable. +$FinalJavaCmd = $null +# This will temporarily hold the result from parsing the vmoptions file. +$script:VmOptionsJavaCmd = $null +$script:VmOptionsJavaCmdFile = $null + +# --- Function to resolve a path to a canonical absolute path --- +function Resolve-CanonicalPath([string]$PathToResolve) { + # Explicitly handle simple tilde expansion first (`~/` or `~`) + if ($PathToResolve -match '^~(/|$)') { + $homePath = $env:HOME + if ([string]::IsNullOrWhiteSpace($homePath)) { + $homePath = $env:USERPROFILE + } + $PathToResolve = ($PathToResolve -replace '^~/', "$($homePath)/") -replace '^~$', $homePath + } + + # If the path is not absolute, assume it's relative to OieHome + if (-not [System.IO.Path]::IsPathRooted($PathToResolve)) { + $PathToResolve = Join-Path -Path $OieHome -ChildPath $PathToResolve + } + + $parentDir = [System.IO.Path]::GetDirectoryName($PathToResolve) + $leaf = [System.IO.Path]::GetFileName($PathToResolve) + + # Only resolve the path if the parent directory actually exists. + if (Test-Path -LiteralPath $parentDir -PathType Container) { + $resolvedParentDir = (Resolve-Path -LiteralPath $parentDir).Path + return Join-Path -Path $resolvedParentDir -ChildPath $leaf + } + else { + return $PathToResolve + } +} + +# --- Function to safely expand specific variable formats in a string --- +function Expand-LineVariables([string]$Line) { + # Define a "match evaluator" script block. This block will be called + # for every match the regex finds. + $evaluator = { + param($match) + + # The variable name is in the first capture group. + $varName = $match.Groups[1].Value + + # Look for a PowerShell variable first. + $varValue = (Get-Variable -Name $varName -Scope "global" -ErrorAction SilentlyContinue).Value + # If not found, look for an environment variable. + if ($null -eq $varValue) { + $varValue = (Get-Variable -Name "env:$varName" -ErrorAction SilentlyContinue).Value + } + + # If a value was found (it's not null), return it. + # Otherwise, return the original text that was matched (e.g., "${UNDEFINED_VAR}"). + if ($null -ne $varValue) { + return $varValue + } else { + return $match.Value + } + } + + # Define the regex pattern to find ${...} variables. + $regex = '\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}' + + # Use the static Replace method, passing the input line, the regex, and our evaluator. + $expandedLine = [regex]::Replace($Line, $regex, $evaluator) + + return $expandedLine +} + +# --- Function to validate Java version --- +function Test-IsValidJavaVersion([string] $JavaCmd) { + # Check if the command is found and is executable + if (-not (Get-Command $JavaCmd -ErrorAction SilentlyContinue)) { + return $false + } + + # Execute 'java -version' and capture the output from stderr + # Example output: openjdk version "17.0.2" 2022-07-19 + try { + $versionOutput = & $JavaCmd -version 2>&1 + } + catch { + return $false + } + + if ($LASTEXITCODE -ne 0) { + return $false + } + + # Extract the major version number. This works for formats like "1.8.0" and "17.0.2". + $match = $versionOutput | Select-String -Pattern '"(\d+)\.' + return ($match -and ($match.Matches[0].Groups[1].Value -as [int]) -ge $MinJavaVersion) +} + +# --- Function to parse vmoptions file --- +function Parse-VmOptions([string] $File) { + + if (-not (Test-Path -LiteralPath $File -PathType Leaf)) { + Write-Warning "VM options file not found: $File" + return + } + + # Read the file line by line + Get-Content -Path $File | ForEach-Object { + $line = $_.Trim() + + # Skip empty lines and comments + if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith('#')) { + return + } + + $line = Expand-LineVariables -Line $line + + if ($line -match '^-include-options\s+(.+)') { + $includedFile = $matches[1].Trim() + # Resolve relative paths to the current file's directory + if (-not [System.IO.Path]::IsPathRooted($includedFile)) { + $includedFile = Join-Path -Path (Split-Path -Path $File -Parent) -ChildPath $includedFile + } + Parse-VmOptions -File $includedFile + } + elseif ($line -match '^-classpath\s+(.+)') { + $script:Classpath.Clear() + $script:Classpath.Add($matches[1].Trim()) + } + elseif ($line -match '^-classpath/a\s+(.+)') { + $script:Classpath.Add($($matches[1].Trim()) + } + elseif ($line -match '^-classpath/p\s+(.+)') { + $script:Classpath.Insert(0, $($matches[1].Trim()) + } + elseif ($line -match '^-java-cmd\s+(.+)') { + # Store the path and the file it was found in. Validation is deferred. + $script:VmOptionsJavaCmd = Resolve-CanonicalPath -PathToResolve $matches[1].Trim() + $script:VmOptionsJavaCmdFile = $File + } + else { + $script:VmOptions.Add($line) + } + } +} + +# --- Main Logic --- + +# 1. Recursively parse the VM options file to populate vmoptions variables. +Parse-VmOptions -File (Join-Path -Path $OieHome -ChildPath "oieserver.vmoptions") + +# 2. Ensure the launcher JAR is always in the classpath. +if (-not $script:Classpath.Contains($LauncherJar)) { + Write-Host "Info: Prepending mirth-server-launcher.jar to the classpath." -ForegroundColor Green + $script:Classpath.Insert(0, $LauncherJar) +} + +# 3. Discover the Java executable using the documented priority order. + +# Check OIE_JAVA_PATH (fail-fast on invalid). +if (-not [string]::IsNullOrWhiteSpace($env:OIE_JAVA_PATH)) { + $resolvedPath = Resolve-CanonicalPath -PathToResolve $env:OIE_JAVA_PATH + if (Test-IsValidJavaVersion -JavaCmd $resolvedPath) { + Write-Host "Info: Found suitable java version specified by the OIE_JAVA_PATH environment variable" -ForegroundColor Green + $FinalJavaCmd = $resolvedPath + } else { + Write-Error "'$resolvedPath' is specified by the OIE_JAVA_PATH environment variable, which is not a valid Java executable of at least version $MinJavaVersion. Exiting." + } +} + +# Check -java-cmd from vmoptions (fail-fast on invalid, only if not already found). +if (-not $FinalJavaCmd -and -not [string]::IsNullOrWhiteSpace($script:VmOptionsJavaCmd)) { + if (Test-IsValidJavaVersion -JavaCmd $script:VmOptionsJavaCmd) { + Write-Host "Info: Found suitable java version specified by the -java-cmd directive in '$($script:VmOptionsJavaCmdFile)'" -ForegroundColor Green + $FinalJavaCmd = $script:VmOptionsJavaCmd + } else { + Write-Error "'$($script:VmOptionsJavaCmd)' is specified by the -java-cmd directive in '$($script:VmOptionsJavaCmdFile)', which is not a valid Java executable of at least version $MinJavaVersion. Exiting." + exit 1 + } +} + +# Check JAVA_HOME (no fail-fast). +if (-not $FinalJavaCmd -and (Test-Path -Path $env:JAVA_HOME -PathType Container)) { + $javaHomePath = Join-Path -Path (Join-Path -Path $env:JAVA_HOME -ChildPath "bin") -ChildPath "java" + if (Test-IsValidJavaVersion -JavaCmd $javaHomePath) { + Write-Host "Info: Found suitable java version specified by the JAVA_HOME environment variable" -ForegroundColor Green + $FinalJavaCmd = $javaHomePath + } else { + Write-Warning "'$javaHomePath' is specified by the JAVA_HOME environment variable, which is not a valid Java executable of at least version $MinJavaVersion. Ignoring." + } +} + +# Check system PATH (no fail-fast). +if (-not $FinalJavaCmd) { + if (Get-Command "java" -ErrorAction SilentlyContinue) { + if (Test-IsValidJavaVersion -JavaCmd "java") { + Write-Host "Info: Found suitable java version in the PATH" -ForegroundColor Green + $FinalJavaCmd = "java" + } else { + Write-Warning "'java' does not exist in your PATH or is not a valid Java executable of at least version $MinJavaVersion." + } + } +} + +# 4. Final check for a valid Java path before execution. +if (-not $FinalJavaCmd) { + Write-Error "Could not find a Java $($MinJavaVersion)+ installation. Please configure -java-cmd in conf/custom.vmoptions, set OIE_JAVA_PATH, set JAVA_HOME, or ensure 'java' in your PATH is version $($MinJavaVersion) or higher." +} + +# 5. Assemble final arguments and launch the process. +$javaOpts = [System.Collections.Generic.List[string]]::new() +$javaOpts.AddRange($script:VmOptions) +$javaOpts.Add("-cp") +$javaOpts.Add($script:Classpath -join [System.IO.Path]::PathSeparator) +$javaOpts.Add("com.mirth.connect.server.launcher.MirthLauncher") +if ($AppArgs) { $javaOpts.AddRange($AppArgs) } + +# Launch Open Integration Engine +Write-Host "Starting Open Integration Engine..." -ForegroundColor Green +Write-Host ("$FinalJavaCmd " + (($javaOpts | %{ "`"$_`"" }) -join ' ')); + +# The engine expects it's working directory to be OieHome +Push-Location -Path $OieHome +try { + & $FinalJavaCmd @javaOpts + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/server/build.xml b/server/build.xml index 9aa6606c1..153790891 100644 --- a/server/build.xml +++ b/server/build.xml @@ -1007,6 +1007,9 @@ + + + diff --git a/server/conf/custom.vmoptions b/server/conf/custom.vmoptions index 1acaf2e42..ab1f51fc0 100644 --- a/server/conf/custom.vmoptions +++ b/server/conf/custom.vmoptions @@ -1,7 +1,8 @@ # Add any custom vmoptions to this file. Special directives are: # # -include-options -# : include options from another .vmoptions file. Applied recursively. +# : include options from another .vmoptions file. Applied recursively. Relative paths +# : are relative to the current vmoptions file. # # -classpath # : replace the classpath built so far with @@ -13,7 +14,8 @@ # : append to classpath. # # -java-cmd -# : use the java binary specified to launch the engine. +# : use the java binary specified to launch the engine. Relative paths are relative to +# : the engine home directory. # # Additionally, the form ${ENV_VAR} can be used anywhere in the file to substitute the value # from an environment variable.