diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c209c7c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +FROM eclipse-temurin:21-jdk AS build +WORKDIR /home/gradle/project + +# 1. Copy Gradle wrapper and configuration files first. +# This includes the 'gradle' directory with the wrapper and libs.versions.toml +COPY gradlew . +COPY gradle gradle +COPY build.gradle settings.gradle ./ + +COPY regi-headless/build.gradle regi-headless/ +COPY buildSrc buildSrc + +RUN sed -i 's/\r$//' gradlew && chmod +x gradlew + +# 2. Pre-download dependencies. +# This layer is cached until you change your version catalog or build scripts. +RUN ./gradlew :regi-headless:dependencies --no-daemon + +# 3. Copy the actual source code and build the distribution +COPY . . +RUN sed -i 's/\r$//' gradlew && chmod +x gradlew +RUN ./gradlew :regi-headless:installDist --no-daemon + +# Download the OpenTelemetry Java Agent +ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar /home/gradle/project/opentelemetry-javaagent.jar + +FROM eclipse-temurin:21-jre-alpine +WORKDIR /app + +# Copy the application and the OTel agent +COPY --from=build /home/gradle/project/regi-headless/build/install/regi-headless ./ +COPY --from=build /home/gradle/project/opentelemetry-javaagent.jar ./ + +# AWS Batch/Lambda often prefer non-root, and it's better for OTel file permissions +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +RUN chown -R appuser:appgroup /app +USER appuser + +RUN chmod +x /app/bin/regi-headless + +ENTRYPOINT ["/app/bin/regi-headless"] diff --git a/Dockerfile-cwbi-copy.dockerfile b/Dockerfile-cwbi-copy.dockerfile new file mode 100644 index 0000000..01fc536 --- /dev/null +++ b/Dockerfile-cwbi-copy.dockerfile @@ -0,0 +1,30 @@ +FROM ghcr.io/usace/usace-wm-python:3.11 + +USER root + +RUN apk update && apk add --no-cache git openjdk-23-jre \ + && mkdir -p /jobs \ + && chown appuser:appuser /jobs + +# Set JAVA_HOME to the directory of Java 23 +ENV JAVA_HOME=/usr/lib/jvm/java-23-openjdk + +# Add Java to the PATH so it's available globally +# Add python bin for python command line tools like cwms-cli +ENV PATH="$JAVA_HOME/bin:/appuser/.local/bin:$PATH" + +RUN mkdir -p /jobs && chown appuser:appuser /jobs + +COPY --chown=appuser:appuser cwbi-docker/entrypoint.sh /entrypoint.sh +COPY --chown=appuser:appuser cwbi-docker/requirements.txt /requirements.txt + +# Set the user to the non-root user +USER appuser + +RUN chmod +x /entrypoint.sh && \ + pip install --no-cache-dir -r /requirements.txt + +ENTRYPOINT [ "/entrypoint.sh" ] + +# CMD ["sleep", "infinity"] +CMD ["/jobs/bin/daily.sh"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index e83d7ab..ec60644 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,10 @@ plugins { - id "com.palantir.git-version" version "3.0.0" id "org.sonarqube" version "4.0.0.2929" } -def versionLabel(gitInfo) { - def branch = gitInfo.branchName // all branches are snapshots, only tags get released - def tag = gitInfo.lastTag - // tag is returned as is. Branch may need cleanup - return branch == null ? tag : "99." + branch.replace("/","-") + "-SNAPSHOT" -} - allprojects { group = 'mil.army.wmist.regi-headless' - version = versionLabel(versionDetails()) + version = project.findProperty('projectVersion') ?: "99.99.99+unversioned" } tasks.register('logTeamCityBuildStatus') { diff --git a/buildSrc/src/main/groovy/regi-headless.deps-conventions.gradle b/buildSrc/src/main/groovy/regi-headless.deps-conventions.gradle index 9e8db3d..260d3c0 100644 --- a/buildSrc/src/main/groovy/regi-headless.deps-conventions.gradle +++ b/buildSrc/src/main/groovy/regi-headless.deps-conventions.gradle @@ -1,24 +1,8 @@ -def checkForNexusCredentials() { - if(!project.hasProperty('nexusUser')) { - println ('Please set the nexusUser property in the GRADLE_USER_HOME ($userHome/.gradle/gradle.properties) file or via -PnexusUser= .') - } - if(!project.hasProperty('nexusPassword')) { - println ('Please set the nexusPassword property in the GRADLE_USER_HOME ($userHome/.gradle/gradle.properties) file or via -PnexusPassword= .') - } -} repositories { maven { url 'https://www.hec.usace.army.mil/nexus/repository/maven-public' } - maven { - url 'https://www.hec.usace.army.mil/nexus/repository/hec-internal' - credentials { - checkForNexusCredentials() - username "$nexusUser" - password "$nexusPassword" - } - } mavenCentral() } diff --git a/buildSrc/src/main/groovy/regi-headless.java-conventions.gradle b/buildSrc/src/main/groovy/regi-headless.java-conventions.gradle index 5b5d71d..58c965d 100644 --- a/buildSrc/src/main/groovy/regi-headless.java-conventions.gradle +++ b/buildSrc/src/main/groovy/regi-headless.java-conventions.gradle @@ -3,8 +3,7 @@ plugins { } compileJava { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + options.release = 21 } dependencies { diff --git a/cwbi-docker/entrypoint.sh b/cwbi-docker/entrypoint.sh new file mode 100644 index 0000000..f92ca0b --- /dev/null +++ b/cwbi-docker/entrypoint.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +## Check if OFFICE is set +#if [ -z "$OFFICE" ]; then +# echo "OFFICE is not set" +# exit 1 +#fi +# +## Lowercase the OFFICE variable +#OFFICE_LOWER="${OFFICE,,}" +# +## Check if GITHUB_TOKEN is set +#if [ -z "$GITHUB_TOKEN" ]; then +# echo "GITHUB_TOKEN is not set" +# exit 1 +#fi +# +#if [ -z "$ENVIRONMENT" ]; then +# echo "ENVIRONMENT not set" +# exit 1 +#fi +# +#GITHUB_BRANCH="cwbi-$ENVIRONMENT" +# +#echo "Using GITHUB_BRANCH: $GITHUB_BRANCH" +# +## Clone the office repo +#git clone --branch $GITHUB_BRANCH \ +# https://$GITHUB_TOKEN@github.com/USACE-WaterManagement/$OFFICE_LOWER-wm-cwbi-jobs.git /jobs +#if [ $? -ne 0 ]; then +# echo "Failed to clone the repository for $OFFICE" +# exit 1 +#fi +# +#unset GITHUB_TOKEN + +# Set all shell scripts to executable +chmod +x /jobs/bin/*.sh + + +# Check if the requirements.txt file exists and install Python dependencies +REQUIREMENTS_FILE="/jobs/python/requirements.txt" + +if [ -f "$REQUIREMENTS_FILE" ]; then + echo "Installing Python dependencies from $REQUIREMENTS_FILE..." + pip install -r "$REQUIREMENTS_FILE" +else + echo "No requirements.txt found at $REQUIREMENTS_FILE" +fi + +if [ -d "/app/local_dist" ]; then + echo "Installing local wheels from /app/local_dist..." + pip install --force-reinstall /app/local_dist/*.whl +fi + +# Run whatever CMD was passed in +exec "$@" diff --git a/cwbi-docker/requirements.txt b/cwbi-docker/requirements.txt new file mode 100644 index 0000000..1bddc7d --- /dev/null +++ b/cwbi-docker/requirements.txt @@ -0,0 +1,52 @@ +cwms-python==1.0.1 +hec-python-library==1.0 +hecdss==0.1.29 +cwms-cli==0.1.5 +shef-parser==1.6.2 +beautifulsoup4==4.13.5 +bokeh==3.8.2 +boto3==1.40.39 +botocore==1.40.39 +certifi==2025.8.3 +charset-normalizer==3.4.3 +click==8.3.0 +colorama==0.4.6 +contourpy==1.3.3 +cycler==0.12.1 +dataretrieval==1.0.12 +flexcache==0.3 +flexparser==0.4 +fonttools==4.60.2 +idna==3.10 +Jinja2==3.1.6 +jmespath==1.0.1 +kiwisolver==1.4.9 +MarkupSafe==3.0.2 +matplotlib==3.10.6 +narwhals==2.5.0 +numpy==2.3.3 +packaging==25.0 +pandas==2.3.2 +pillow==11.3.0 +Pint==0.25 +Pint-Pandas==0.7.1 +platformdirs==4.4.0 +plotly==6.3.0 +pyparsing==3.2.5 +python-dateutil==2.9.0.post0 +pytz==2025.2 +PyYAML==6.0.2 +requests==2.32.5 +requests-toolbelt==1.0.0 +s3transfer==0.14.0 +scipy==1.16.2 +seaborn==0.13.2 +six==1.17.0 +soupsieve==2.8 +tornado==6.5.2 +typing_extensions==4.15.0 +tzdata==2025.2 +tzlocal==5.3.1 +urllib3==2.6.3 +xarray==2025.9.0 +xyzservices==2025.4.0 diff --git a/datasources.yaml b/datasources.yaml new file mode 100644 index 0000000..684c2d6 --- /dev/null +++ b/datasources.yaml @@ -0,0 +1,16 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + url: http://prometheus:9090 + access: proxy + isDefault: true + - name: Jaeger + type: jaeger + url: http://jaeger:16686 + access: proxy + - name: Loki + type: loki + url: http://loki:3100 + access: proxy \ No newline at end of file diff --git a/docker-compose-cwbi.yml b/docker-compose-cwbi.yml new file mode 100644 index 0000000..834a952 --- /dev/null +++ b/docker-compose-cwbi.yml @@ -0,0 +1,66 @@ +name: regi-headless-cwbi + +services: + app: + build: + context: . + dockerfile: Dockerfile-cwbi-copy.dockerfile + command: ["python", "/jobs/GateFlowCalc2_Jpype.py"] + environment: + - OTEL_SERVICE_NAME=regi-headless + - OTEL_TRACES_EXPORTER=otlp + - OTEL_METRICS_EXPORTER=otlp + - OTEL_LOGS_EXPORTER=otlp + - OTEL_BSP_SCHEDULE_DELAY=1000 + - OTEL_EXPORTER_OTLP_PROTOCOL=grpc + - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 + - OTEL_INSTRUMENTATION_OKHTTP_ENABLED=true + - OTEL_INSTRUMENTATION_METHODS_ENABLED=true + - CDA_URL=http://host.docker.internal:7001/swt-data + - API_KEY=apikey M5HECTEST + - OFFICE_ID=SWT + depends_on: + - otel-collector + volumes: + - ./regi-headless/build/install/regi-headless/dist:/app/local_dist + - ./regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2_Jpype.py:/jobs/GateFlowCalc2_Jpype.py + - ./empty_dir:/jobs/bin + - ./empty_dir:/jobs/python + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./otel-config.yaml:/etc/otel-collector-config.yaml + + jaeger: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "4317:4317" + + prometheus: + image: prom/prometheus + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + + loki: + image: grafana/loki:latest + ports: + - "3100:3100" + + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + volumes: + - ./datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + depends_on: + - prometheus + - jaeger + - loki \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f6d44fd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ +name: regi-headless-java + +services: + app: + build: . + environment: + - OTEL_SERVICE_NAME=regi-headless + - OTEL_TRACES_EXPORTER=otlp + - OTEL_METRICS_EXPORTER=otlp + - OTEL_LOGS_EXPORTER=otlp + - OTEL_BSP_SCHEDULE_DELAY=1000 + - OTEL_EXPORTER_OTLP_PROTOCOL=grpc + - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 + - OTEL_INSTRUMENTATION_OKHTTP_ENABLED=true + - OTEL_INSTRUMENTATION_METHODS_ENABLED=true +# - REGI_HEADLESS_OPTS=-javaagent:/app/opentelemetry-javaagent.jar + - JAVA_TOOL_OPTIONS=-javaagent:/app/opentelemetry-javaagent.jar + - CDA_URL=http://host.docker.internal:7001/swt-data + - API_KEY=apikey M5HECTEST + - OFFICE_ID=SWT + - SCRIPT=/scripts/GateFlowCalc2.py + depends_on: + - otel-collector + volumes: + - ./otel-config.yaml:/etc/otel-collector-config.yaml + - ./regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2.py:/scripts/GateFlowCalc2.py + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./otel-config.yaml:/etc/otel-collector-config.yaml + + jaeger: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "4317:4317" + + prometheus: + image: prom/prometheus + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + + loki: + image: grafana/loki:latest + ports: + - "3100:3100" + + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + volumes: + - ./grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + depends_on: + - prometheus + - jaeger + - loki \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f41f616 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,11 @@ +# +# Copyright (c) 2026 +# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC) +# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL. +# Source may not be released without written approval from HEC +# + +CDA_URL=http://localhost:7001/swt-data +API_KEY=apikey M5HECTEST +OFFICE_ID=SWT +SCRIPT=C:/Git/HEC/BitBucket/regi-headless/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2.py \ No newline at end of file diff --git a/gradle/docs/jpype.png b/gradle/docs/jpype.png new file mode 100644 index 0000000..d1e2686 Binary files /dev/null and b/gradle/docs/jpype.png differ diff --git a/gradle/docs/jpype.puml b/gradle/docs/jpype.puml new file mode 100644 index 0000000..4884599 --- /dev/null +++ b/gradle/docs/jpype.puml @@ -0,0 +1,34 @@ +@startuml +autonumber +skinparam BoxPadding 10 + +box "Python Environment (CPython)" #LightBlue + participant "test_script.py" as Test + participant "regi_cli" as Pkg + participant "JPype Engine" as JPype +end box + +box "Java Virtual Machine" #LightYellow + participant "JVM Internal" as JVM + participant "regi-headless.jar" as Jar +end box + +Note over Test: env variables set externally \nJAVA_HOME\nCDA_URL\nAPI_KEY\nOFFICE_ID + +Test -> Pkg: import regi_cli +Pkg -> JPype: startJVM(classpath=["lib/*"]) + +group JVM Initialization + JVM -> Jar: Scan Classpath + JVM --> Pkg: JVM Started Successfully +end group + +Test -> JPype: Call Java-backed functions (ex. registry.getCalculation(1.0, "Gate Flow")) +JPype -> Jar: Reflect & Load Class + +JPype -> Jar: Instantiate Object / Call Method +activate Jar #DarkSalmon +Note right of Jar: Java Logic Executes +deactivate Jar + +@enduml \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c8dacb..39f8b1e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,15 @@ [versions] # HEC Dependencies -service-annotations = "1.2.2" +service-annotations = "2.1.0" win-java-heclib = "7-IE-win-x64" solaris-java-heclib = "7-IE-Solaris64" -hec-core = "6.1-SNAPSHOT" # required for PasswordFileEditor +hec-monolith = "7.0.4" # required for PasswordFileEditor +hec-server-suite = "8.4.5" # REGI Dependencies -regi-tools = "3.4.3" -regi = "3.4.4" +regi-tools = "9.1.1" +regi = "3.5.0-alpha001" # Third Party jython-standalone = "2.7.2" @@ -26,9 +27,14 @@ junit = "5.9.3" [libraries] # HEC Dependencies service-annotations = { module = "mil.army.usace.hec:service-annotations", version.ref = "service-annotations" } -hec-core = { module="mil.army.usace.hec:hec-core", version.ref = "hec-core" } +hec-monolith = { module="mil.army.usace.hec:hec-monolith", version.ref = "hec-monolith" } win-java-heclib = {module="mil.army.usace.hec:javaHeclib", version.ref = "win-java-heclib"} solaris-java-heclib = {module="mil.army.usace.hec:javaHeclib", version.ref = "solaris-java-heclib"} +serversuite = { module = "mil.army.usace.hec:hec-server-suite", version.ref = "hec-server-suite" } +serversuite-cda = { module = "mil.army.usace.hec:cda-server-suite", version.ref = "hec-server-suite" } +serversuite-jdbc = { module = "mil.army.usace.hec:jdbc-server-suite", version.ref = "hec-server-suite" } +hec-db-cda = { module = "mil.army.usace.hec:hec-db-cda", version = "14.1.0" } +hec-cwms-ratings-cda = { module = "mil.army.usace.hec:hec-cwms-ratings-io-cda", version = "4.2.2"} # REGI Dependencies regi-basinpie-ui = {module = "mil.army.wmist.regi:basin-pie-ui", version.ref = "regi"} @@ -48,6 +54,7 @@ regi-tools-regi-cache-ui = {module = "mil.army.wmist.regi-tools:regi-cache-ui", # Third Party jython-standalone = {module = "org.python:jython-standalone", version.ref = "jython-standalone"} args4j = {module = "args4j:args4j", version.ref = "args4j"} +otel = {module = "io.opentelemetry:opentelemetry-api", version="1.58.0"} # Natives windows_jre = { module = "com.oracle:oracle-jre", version.ref = "windows-jre" } @@ -76,12 +83,13 @@ regi-tools = [ "regi-tools-regi-data" ] hec = [ - "hec-core" + "hec-monolith" ] sys = [ "args4j", "jython-standalone" ] +serversuite = ["serversuite", "serversuite-cda", "serversuite-jdbc", "hec-db-cda"] junit-api = ["junit-jupiter-api", "junit-jupiter-params", "junit4"] junit-engine = ["junit-jupiter-engine", "junit-vintage-engine"] \ No newline at end of file diff --git a/otel-config.yaml b/otel-config.yaml new file mode 100644 index 0000000..5ee34fc --- /dev/null +++ b/otel-config.yaml @@ -0,0 +1,36 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +exporters: + debug: + verbosity: detailed + # Jaeger handles OTLP natively + otlp/jaeger: + endpoint: jaeger:4317 + tls: + insecure: true + # Standard Prometheus exporter + prometheus: + endpoint: "0.0.0.0:8889" + # Use the standard OTLP HTTP exporter to talk to Loki + otlphttp/loki: + endpoint: "http://loki:3100/otlp" + tls: + insecure: true + +service: + pipelines: + traces: + receivers: [otlp] + exporters: [otlp/jaeger, debug] + metrics: + receivers: [otlp] + exporters: [prometheus, debug] + logs: + receivers: [otlp] + exporters: [otlphttp/loki, debug] \ No newline at end of file diff --git a/prometheus.yaml b/prometheus.yaml new file mode 100644 index 0000000..cbbe048 --- /dev/null +++ b/prometheus.yaml @@ -0,0 +1,4 @@ +scrape_configs: + - job_name: 'otel-collector' + static_configs: + - targets: ['otel-collector:8889'] \ No newline at end of file diff --git a/regi-headless/build.gradle b/regi-headless/build.gradle index e528b8c..37aedb8 100644 --- a/regi-headless/build.gradle +++ b/regi-headless/build.gradle @@ -1,24 +1,138 @@ plugins { id 'regi-headless.deps-conventions' id 'regi-headless.java-conventions' + id 'application' +} + +configurations { + otelAgent } dependencies { implementation(libs.bundles.hec) + implementation(libs.bundles.serversuite) implementation(libs.jython.standalone) implementation(libs.args4j) implementation(libs.bundles.regi) implementation(libs.bundles.regi) {artifact {extension = "jar"}} implementation(libs.bundles.regi.tools) implementation(libs.bundles.regi.tools) {artifact {extension = "jar"} } + implementation(libs.otel) + runtimeOnly(libs.hec.cwms.ratings.cda) + otelAgent("io.opentelemetry.javaagent:opentelemetry-javaagent:2.23.0") testImplementation(libs.bundles.junit.api) testRuntimeOnly(libs.bundles.junit.engine) } +configurations.configureEach { + exclude group: 'mil.army.usace.hec', module: 'hec-cwmsvue' + exclude group: 'mil.army.usace.hec', module: 'hec-gt-crs' + exclude group: 'org.openjfx', module: '*' + exclude group: 'mil.army.usace.hec.swingx', module: '*' + exclude group: 'org.swinglabs', module: '*' + exclude group: 'org.jfree', module: '*' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + + jar { manifest { attributes('Implementation-Version': project.version) } -} \ No newline at end of file +} + +application { + mainClass = 'usace.rowcps.headless.RegiCLI' +} + +tasks.named('run') { + environment 'CDA_URL', project.findProperty('CDA_URL') + environment 'API_KEY', project.findProperty('API_KEY') + environment 'OFFICE_ID', project.findProperty('OFFICE_ID') + environment 'SCRIPT', project.findProperty('SCRIPT') +} + +distributions { + main { + contents { + exclude "**/*.nbm" + } + } +} + +task bundlePython(type: Sync) { + description = 'Bundles Python scripts and creates the java_lib directory' + into "${buildDir}/install/${project.name}/" + from('src/main/python') { + include 'pyproject.toml' + filter { line -> line.replaceAll('@VERSION@', project.version.toString()) } + } + + from('src/main/python') { + exclude 'pyproject.toml' + } + + into('regi_cli/lib') { + from configurations.runtimeClasspath + from jar.archiveFile + exclude "**/*.nbm" + } + + from(configurations.otelAgent) { + into "regi_cli/lib" + rename { "opentelemetry-javaagent.jar" } + } +} + +task buildPythonWheel(type: Exec) { + group = 'distribution' + description = 'Builds a Python .whl file from the installDist output' + dependsOn bundlePython + + workingDir "${buildDir}/install/${project.name}" + + commandLine 'python', '-m', 'build', '--wheel' + + doLast { + println "Python Wheel built in: ${workingDir}/dist" + } +} + +tasks.register('testPythonWheel') { + group = 'verification' + description = 'Creates a venv, installs the wheel, and runs a test script.' + dependsOn buildPythonWheel + + doLast { + def venvName = "test_venv" + def wheelDir = "${projectDir}/build/install/regi-headless/dist" + def testScript = "src\\test\\resources\\usace\\rowcps\\headless\\examples\\GateFlowCalc2_Jpype.py" + + // 1. Create VENV if it doesn't exist + exec { + commandLine 'cmd', '/c', "python -m venv ${venvName}" + } + + // 2. Install Wheel and Run Script + // We chain these using '&&' so they run in the same shell session where the venv is active + exec { + workingDir projectDir + def wheelFile = fileTree(dir: wheelDir, include: '*.whl').singleFile.absolutePath + def venvActivate = file("${venvName}/Scripts/activate.bat").absolutePath + environment 'CDA_URL', project.findProperty('CDA_URL') ?: "" + environment 'API_KEY', project.findProperty('API_KEY') ?: "" + environment 'OFFICE_ID', project.findProperty('OFFICE_ID') ?: "" + environment 'JAVA_HOME', System.getProperty('java.home') + commandLine 'cmd', '/c', "\"${venvActivate}\" && pip install --force-reinstall \"${wheelFile}\" && python -u ${testScript}" + standardOutput = System.out + errorOutput = System.err + } + } +} diff --git a/regi-headless/src/main/java/usace/rowcps/headless/CLIOptions.java b/regi-headless/src/main/java/usace/rowcps/headless/CLIOptions.java deleted file mode 100644 index bf6a1f6..0000000 --- a/regi-headless/src/main/java/usace/rowcps/headless/CLIOptions.java +++ /dev/null @@ -1,341 +0,0 @@ -package usace.rowcps.headless; - -import hec.lang.PasswordFileEntry; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.TimeZone; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.Option; -import rma.services.ServiceLookup; -import rma.services.tz.TimeZoneDisplayService; - -/** - * - * @author ryan - */ -public class CLIOptions -{ - private static final Logger logger = Logger.getLogger(CLIOptions.class.getName()); - //public String rowcpsTimezone; - //public String oracleUrl; - //public String oracleUser; - //public String oraclePassword; - //private Map properties = new HashMap(); - Properties props; - - public final static String URL = "oracle.url"; - public final static String USER = "oracle.user"; - public final static String PASSWORD = "oracle.password"; - public final static String OFFICEID = "oracle.officeId"; - - public final static String TIMEZONE = "rowcps.timezone"; - public final static String PROJ_DIR = "rowcps.projectDir"; - public final static String PROJ_NAME = "rowcps.projectName"; - - public final static String HEC_PASSWD_FILE = "hec.passwd"; - - public CLIOptions() - { - this(System.getProperties()); - } - - public CLIOptions(Properties defaultProperties) - { - props = new Properties(defaultProperties); - } - - @Option(name = "-D", metaVar = "=", usage = "use value for given property") - private void setProperty(final String property) throws CmdLineException - { - String[] arr = property.split("="); - setProperty(arr); - } - - public void setProperty(String[] arr) throws CmdLineException - { - if (arr.length != 2) { - throw new CmdLineException("Properties must be specified in the form:" + - "="); - } - props.setProperty(arr[0], arr[1]); - //properties.put(arr[0], arr[1]); - } - - public Object getProperty(String key) - { - return props.get(key); - } - - /** - * @return the rowcpsTimezone - */ - public String getRowcpsTimezone() - { - return props.getProperty(TIMEZONE); - //return rowcpsTimezone; - } - - public File getRowcpsProjectDir() - { - File retval = null; - - String path = props.getProperty(PROJ_DIR); - if(path != null){ - retval = new File(path); - } - - return retval; - } - - @Option(name = "-D" + PROJ_DIR, metaVar = "", usage = "directory containing Regi project") - public void setRowcpsProjectDir(String filepath) - { - props.setProperty(PROJ_DIR, filepath); - } - - @Option(name = "-D" + HEC_PASSWD_FILE, metaVar = "", usage = "directory containing Regi project") - public void setHecPasswordFilepath(String filepath) - { - props.setProperty(HEC_PASSWD_FILE, filepath); - } - - public String getHecPasswordFilepath(){ - return props.getProperty(HEC_PASSWD_FILE); - } - - public PasswordFileEntry getHecPasswordFileEntry() - { - PasswordFileEntry retval = null; - - // String office = System.getProperty("cwms.dbi.OfficeId"); - String dburl = getOracleUrl(); - if (dburl != null && !dburl.isEmpty()) { - String instance = dburl; - - int idx = dburl.indexOf("@"); - if (idx != -1) { - instance = dburl.substring(idx + 1); - } - - hec.io.PasswordFile passwordFile = null; - try { - String filePath = getHecPasswordFilepath(); - passwordFile = new hec.io.PasswordFile(filePath, false); - retval = passwordFile.getEntry(instance); - - if (retval == null) { - /* - * System.out.println( - * "getConnectionInfo: Failed to find Password Entry for instance " - * + instance); - */ - logger.severe("getConnectionInfo: Failed to find Password Entry for instance " + instance); - - } -// // _connectionInfo = new -// // ConnectionInfo(office,dburl,entry.getUserName(),entry.getPassword()); -// _connectionLoginInfo = new ConnectionLoginInfoImpl(dburl, entry.getUserName(), entry.getPassword(), -// getOfficeId()); - } catch (java.io.IOException ioe) { - /* - * System.out.println( - * "getConnectionInfo: Error reading password file " + ioe); - */ - logger.severe("getConnectionInfo: Error reading password file " + ioe); - - } finally { - if (passwordFile != null) { - passwordFile.close(); - } - } - } - - return retval; - } - - - public String getRowcpsProjectName() - { - return props.getProperty(PROJ_NAME); - } - - @Option(name = "-D" + PROJ_NAME, usage = "name of Regi project") - public void setRowcpsProjectName(String name) - { - props.setProperty(PROJ_NAME, name); - } - - public String getOracleOfficeId() - { - return props.getProperty(OFFICEID); - } - - @Option(name = "-D" + OFFICEID, usage = "office id") - public void setOracleOfficeId(String id) - { - props.setProperty(OFFICEID, id); - } - - /** - * @param rowcpsTimezone the rowcpsTimezone to set - */ - @Option(name = "-D" + TIMEZONE) - public void setRowcpsTimezone(String rowcpsTimezone) throws CmdLineException - { - setProperty(new String[]{TIMEZONE, rowcpsTimezone}); - TimeZone timeZone = TimeZone.getTimeZone(rowcpsTimezone); - if(timeZone == null) - { - timeZone = TimeZone.getDefault(); - Logger.getLogger(CLIOptions.class.getName()).log(Level.WARNING, "Attempted to set invalid time zone to Regi Domain: "+rowcpsTimezone); - } - TimeZoneDisplayService timeZoneDisplayService = ServiceLookup.getTimeZoneDisplayService(); - timeZoneDisplayService.setTimeZone(timeZone); - } - - /** - * @return the oracleUrl - */ - public String getOracleUrl() - { - return props.getProperty(URL); - //return oracleUrl; - } - - /** - * @param oracleUrl the oracleUrl to set - */ - @Option(name = "-D" + URL) - public void setOracleUrl(String oracleUrl) throws CmdLineException - { - //this.oracleUrl = oracleUrl; - setProperty(new String[]{URL, oracleUrl}); - } - - /** - * @return the oracleUser - */ - public String getOracleUser() - { - String user = props.getProperty(USER); - - if (user == null) { - - PasswordFileEntry entry = getHecPasswordFileEntry(); - if (entry != null) { - user = entry.getUserName(); - } - - } - return user; - //return oracleUser; - } - - /** - * @param oracleUser the oracleUser to set - */ - @Option(name = "-D" + USER) - public void setOracleUser(String oracleUser) throws CmdLineException - { - //this.oracleUser = oracleUser; - setProperty(new String[]{USER, oracleUser}); - } - - /** - * @return the oraclePassword - */ - public char[] getOraclePassword() - { - char[] pass = null; - //return oraclePassword; - String passStr = props.getProperty(PASSWORD); - if (passStr != null) { - pass = passStr.toCharArray(); - } else { - PasswordFileEntry entry = getHecPasswordFileEntry(); - if(entry != null){ - pass = entry.getPassword().toCharArray(); - } - } - - return pass; - } - - /** - * @param oraclePassword the oraclePassword to set - */ - @Option(name = "-D" + PASSWORD) - public void setOraclePassword(String oraclePassword) throws CmdLineException - { - setProperty(new String[]{PASSWORD, oraclePassword}); - //this.oraclePassword = oraclePassword; - } - - @Option(name = "-p", aliases = {"-properties"}, metaVar = "", - usage = "import properties from given file") - public void importProperties(File file) - { - if (file != null && file.exists()) { - Properties fileProps = new Properties(); - - try (BufferedReader br = new BufferedReader(new FileReader(file))) { - fileProps.load(br); - - Set> entrySet = fileProps.entrySet(); - for (Map.Entry entry : entrySet) { - Object keyObj = entry.getKey(); - Object valueObj = entry.getValue(); - - if (keyObj != null && valueObj != null) { - props.put(keyObj, valueObj); - } - } - } catch (FileNotFoundException ex) { - Logger.getLogger(CLIOptions.class.getName()).log(Level.SEVERE, null, ex); - } catch (IOException ex) { - Logger.getLogger(CLIOptions.class.getName()).log(Level.SEVERE, null, ex); - } - } - else{ - Logger.getLogger(CLIOptions.class.getName()).log(Level.SEVERE, "Unable to find credentials file at: "+(file ==null ? "null" : file)); - } - } - - @Option(name = "-f", aliases = {"-file"}, metaVar = "", - usage = "script file to execute") - public void setScriptFile(File file) - { - props.setProperty("script", file.getAbsolutePath()); - } - - public String getScriptPath() - { - return props.getProperty("script"); - } - - public File getScriptFile() - { - File retval = null; - String scriptPath = getScriptPath(); - if (scriptPath != null && !scriptPath.isEmpty()) { - File afile = new File(scriptPath); - if (afile.exists()) { - retval = afile; - } - } - return retval; - } - - Properties getProperties() { - return new Properties(props); - } - -} diff --git a/regi-headless/src/main/java/usace/rowcps/headless/HeadlessRegiDomainFactory.java b/regi-headless/src/main/java/usace/rowcps/headless/HeadlessRegiDomainFactory.java index f631bf0..2ecf3fb 100644 --- a/regi-headless/src/main/java/usace/rowcps/headless/HeadlessRegiDomainFactory.java +++ b/regi-headless/src/main/java/usace/rowcps/headless/HeadlessRegiDomainFactory.java @@ -1,154 +1,102 @@ package usace.rowcps.headless; -import com.rma.io.FileManager; -import com.rma.io.FileManagerImpl; import com.rma.io.RmaFile; -import com.rma.model.Manager; import com.rma.model.Project; +import hec.db.DataAccessFactory; import hec.db.DbConnectionException; +import hec.db.DbIoException; import hec.db.DbPluginNotFoundException; -import hec.db.InvalidDbConnectionException; -import hec.io.Identifier; +import hec.db.cwms.CwmsSecurityDao; import hec.lang.LoginException; +import hec.serversuite.ServerSuite; import hec.serversuite.ServerSuiteUtil; -import hec.serversuite.data.DirectOracleAuthenticationSource; -import java.io.File; -import java.util.List; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.logging.Level; import java.util.logging.Logger; +import mil.army.usace.hec.serversuite.cda.CdaAuthenticationSource; +import mil.army.usace.hec.serversuite.cda.CwmsApiKeyAuthExtension; import rma.services.ServiceLookup; import rma.services.tz.TimeZoneDisplayService; +import usace.rowcps.regi.executor.ManagerIdType; import usace.rowcps.regi.factories.RegiDomainFactory; import usace.rowcps.regi.interfaces.model.ManagerIdProvider; import usace.rowcps.regi.model.DatabaseConnectionManager; import usace.rowcps.regi.model.ManagerId; -import usace.rowcps.regi.executor.ManagerIdType; - import usace.rowcps.regi.model.RegiDomain; -/** - * - * @author ryan - */ public class HeadlessRegiDomainFactory { private static final Logger logger = Logger.getLogger(HeadlessRegiDomainFactory.class.getName()); + private ManagerIdProvider idProvider = buildNewProvider(); - public void setPluginsDirFromClasspath() - { - String cp = System.getProperties().getProperty("java.class.path"); - String[] split = cp.split(File.pathSeparator); - final String dbiClientjar = "dbiClient-v3.1.1.jar"; - for (String cpentry : split) { - if (cpentry.endsWith(dbiClientjar)) { - String pluginDir = cpentry.split(dbiClientjar)[0]; - logger.log(Level.INFO, "Setting plugin dir to: {0}", pluginDir); - System.setProperty("PLUGINS", pluginDir); - } - } - } - - public RegiDomain createDomain(CLIOptions options, ManagerId managerId) throws DbConnectionException, - DbPluginNotFoundException, InvalidDbConnectionException - { - RegiDomain regiDomain = null; - - setPluginsDirFromClasspath(); - File rowcpsPojectDir = options.getRowcpsProjectDir(); - - String rowcpsProjectName = options.getRowcpsProjectName(); - logger.log(Level.INFO, "Creating project dir: "+ (rowcpsPojectDir == null ? "null" : rowcpsPojectDir), rowcpsProjectName == null ? "null" : rowcpsProjectName); - File projectDir = new File(rowcpsPojectDir, rowcpsProjectName); - - if (projectDir == null) { - String missingProjDirMessage - = "A Rowcps Project Dir is required and must be specified on the command line or in a properties file."; - throw new IllegalArgumentException(missingProjDirMessage); - } else { - if (!projectDir.exists()) { - // If we are being run headlessly I'm not sure how much hand-holding and sanity checking we have to do. - projectDir.mkdirs(); - if (!projectDir.exists()) { - throw new IllegalArgumentException("The directory " + projectDir.getAbsolutePath() + - " did not exist and could not be created."); - } - } - - String testProjDir = projectDir.getAbsolutePath(); - FileManager fileManager = FileManagerImpl.getFileManager(); - final String projectFilePath = testProjDir + "/" + options.getRowcpsProjectName() + ".prj"; - - RmaFile prjFile; - if (!fileManager.fileExists(projectFilePath)) { - final Identifier identifier = new Identifier(projectFilePath); - Identifier prjId = fileManager.createFile(identifier); - prjFile = fileManager.getFile(prjId.getPath()); - } else { - prjFile = fileManager.getFile(projectFilePath); - } - - logger.log(Level.INFO, "Temp project file: " + prjFile.getAbsolutePath()); + public RegiDomain createDomain() throws DbConnectionException, + DbPluginNotFoundException, IOException { - File projReportsDir = new File(projectDir, "reports"); - File projXmlDir = new File(projectDir, "xml"); - projReportsDir.mkdir(); - projXmlDir.mkdir(); + Path projectDir = Paths.get("regi-projects", "regi-cli"); + logger.log(Level.INFO, "Creating project dir: "+ projectDir); + Files.createDirectories(projectDir); - String name = "Headless"; - String description = "Created for Headless execution."; - regiDomain = new RegiDomainFactory().createProject(name, description, prjFile); - - regiDomain.loadProjectFile(); - - DatabaseConnectionManager connectionManager = (DatabaseConnectionManager) regiDomain.getManager( - RegiDomain.DOMAIN_CONNECTION_MANAGER, DatabaseConnectionManager.class); - - if (connectionManager == null) { - connectionManager = regiDomain.buildDatabaseConnectionManager(); - } - - //conigure the database connection. - String dbUrl = options.getOracleUrl(); - String username = options.getOracleUser(); - char[] password = options.getOraclePassword(); - String tzId = options.getRowcpsTimezone(); - String officeId = options.getOracleOfficeId(); - - DirectOracleAuthenticationSource directOracleAuthenticationSource = new DirectOracleAuthenticationSource("", dbUrl, officeId); - connectionManager.setTimeZoneId(tzId); - connectionManager.setUsername(username); - connectionManager.setUserOfficeId(officeId); - connectionManager.saveData(); - - TimeZoneDisplayService tsDS = ServiceLookup.getTimeZoneDisplayService(); - tsDS.setTimeZone(connectionManager.getTimeZone()); - try - { - ServerSuiteUtil.login("REGI Headless", directOracleAuthenticationSource, username, password); - } - catch(LoginException ex) - { - throw new DbConnectionException(ex); - } - - regiDomain.connect(ServerSuiteUtil.getServerSuite()); - List managerList = regiDomain.getManagerList(); - - regiDomain.saveProject(); - Project.setCurrentProject(regiDomain); + Path projectFile = projectDir.resolve("regi-cli.prj"); + if(!Files.exists(projectFile)) { + Files.createFile(projectFile); } - return regiDomain; + + Files.createDirectories(projectDir.resolve("reports")); + Files.createDirectories(projectDir.resolve("xml")); + + String name = "Headless"; + String description = "Created for Headless execution."; + RegiDomain regiDomain = new RegiDomainFactory().createProject(name, description, new RmaFile(projectFile.toAbsolutePath().toString())); + + regiDomain.loadProjectFile(); + + DatabaseConnectionManager connectionManager = (DatabaseConnectionManager) regiDomain.getManager( + RegiDomain.DOMAIN_CONNECTION_MANAGER, DatabaseConnectionManager.class); + + if (connectionManager == null) { + connectionManager = regiDomain.buildDatabaseConnectionManager(); + } + + String cdaUrl = System.getenv("CDA_URL"); + String apiKey = System.getenv("API_KEY"); + String officeId = System.getenv("OFFICE_ID"); + + CdaAuthenticationSource cdaAuthenticationSource = new CdaAuthenticationSource("", cdaUrl, officeId, new CwmsApiKeyAuthExtension(apiKey)); + try + { + ServerSuite serverSuite = ServerSuiteUtil.login("REGI CLI", cdaAuthenticationSource, false, false, false); + DataAccessFactory dataAccessFactory = serverSuite.getDataAccessFactory(); + try(var key = dataAccessFactory.getDataAccessKey("REGI CLI")) { + String username = dataAccessFactory.getDao(CwmsSecurityDao.class).getCurrentUserId(key); + connectionManager.setUsername(username); + } + connectionManager.setTimeZoneId("UTC"); + connectionManager.setUserOfficeId(officeId); + connectionManager.saveData(); + TimeZoneDisplayService tsDS = ServiceLookup.getTimeZoneDisplayService(); + tsDS.setTimeZone(connectionManager.getTimeZone()); + regiDomain.connect(ServerSuiteUtil.getServerSuite()); + regiDomain.getManagerList(); + regiDomain.saveProject(); + RegiDomain.setCurrentProject(regiDomain); + Project.setCurrentProject(regiDomain); + return regiDomain; + } + catch(LoginException | DbIoException ex) + { + throw new DbConnectionException(ex); + } } - public ManagerId getManagerId(CLIOptions opt) + public ManagerId getManagerId() { - ManagerId retval = idProvider.getManagerId(); - - return retval; + return idProvider.getManagerId(); } - private ManagerIdProvider idProvider = buildNewProvider(); private static ManagerIdProvider buildNewProvider() { diff --git a/regi-headless/src/main/java/usace/rowcps/headless/LoggingOptions.java b/regi-headless/src/main/java/usace/rowcps/headless/LoggingOptions.java index c955c60..9797122 100644 --- a/regi-headless/src/main/java/usace/rowcps/headless/LoggingOptions.java +++ b/regi-headless/src/main/java/usace/rowcps/headless/LoggingOptions.java @@ -9,7 +9,6 @@ import java.util.logging.Logger; import usace.rowcps.headless.metrics.RegiHeadlessMetricsServiceProvider; import usace.rowcps.metrics.RegiMetricsService; -import wcds.dbi.DbiProperties; /** * @@ -42,7 +41,6 @@ private LoggingOptions() */ public static void setDbMessageLevel(int messageLevel) { - DbiProperties.setMessageLevel(messageLevel); LOGGER.log(Level.FINE, "Setting Db Message Level to: {0}", messageLevel); } diff --git a/regi-headless/src/main/java/usace/rowcps/headless/RegiCLI.java b/regi-headless/src/main/java/usace/rowcps/headless/RegiCLI.java index b4dc93f..d6317df 100644 --- a/regi-headless/src/main/java/usace/rowcps/headless/RegiCLI.java +++ b/regi-headless/src/main/java/usace/rowcps/headless/RegiCLI.java @@ -1,13 +1,17 @@ package usace.rowcps.headless; -import usace.rowcps.headless.interfaces.ScriptEvaluator; import hec.db.DbConnectionException; import hec.db.DbIoException; import hec.db.DbPluginNotFoundException; import hec.db.InvalidDbConnectionException; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; +import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -15,50 +19,33 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.CmdLineParser; -import usace.rowcps.metrics.RegiMetricsService; +import usace.rowcps.headless.interfaces.ScriptEvaluator; import usace.rowcps.regi.factories.RowcpsExecutorService; import usace.rowcps.regi.model.ManagerId; import usace.rowcps.regi.model.RegiDomain; -import usace.rowcps.regi.preferences.RegiPreferences; -/** - * - * @author ryan - */ public class RegiCLI { private static final Logger LOGGER = Logger.getLogger(RegiCLI.class.getName()); - static - { - RegiMetricsService.init(RegiPreferences.getClientNode().node("Metrics"), "REGI Headless"); - } - public static void main(String[] args) { - CLIOptions opt = new CLIOptions(System.getProperties()); - CmdLineParser parser = new CmdLineParser(opt); - - try + Span rootSpan = GlobalOpenTelemetry.getTracer("regi-headless") + .spanBuilder("runHeadless") + .startSpan(); + try(Scope scope = rootSpan.makeCurrent()) { - runHeadless(parser, args, opt); + runHeadlessTest(args); } - catch (DbConnectionException | DbPluginNotFoundException | InvalidDbConnectionException ex) + catch (DbConnectionException | DbPluginNotFoundException | IOException | RuntimeException ex) { + rootSpan.recordException(ex); + rootSpan.setStatus(StatusCode.ERROR); LOGGER.log(Level.SEVERE, "Headless error connecting to database.", ex); System.exit(-1); return; } - catch (CmdLineException | RuntimeException e) - { - LOGGER.log(Level.SEVERE, "Error running headless", e); - System.err.println("java -jar myprogram.jar [options...] arguments..."); - parser.printUsage(System.err); - System.exit(-1); - return; - } LOGGER.info("Exiting."); System.exit(0); @@ -67,37 +54,29 @@ public static void main(String[] args) /** * Used by TestHeadless unit test class to run headless without calling System.exit(0) * - * @param args + * @param unused * @throws DbConnectionException * @throws InvalidDbConnectionException * @throws CmdLineException * @throws DbPluginNotFoundException */ - static void runHeadlessTest(String[] args) throws DbConnectionException, InvalidDbConnectionException, CmdLineException, DbPluginNotFoundException - { - CLIOptions opt = new CLIOptions(System.getProperties()); - CmdLineParser parser = new CmdLineParser(opt); - runHeadless(parser, args, opt); - } - - private static void runHeadless(CmdLineParser parser, String[] args, CLIOptions opt) throws DbConnectionException, InvalidDbConnectionException, CmdLineException, DbPluginNotFoundException - { - parser.parseArgument(args); - System.setProperties(opt.getProperties()); + static void runHeadlessTest(String[] unused) + throws DbConnectionException, DbPluginNotFoundException, + IOException { HeadlessRegiDomainFactory factory = new HeadlessRegiDomainFactory(); - ManagerId managerId = factory.getManagerId(opt); - RegiDomain regiDomain = factory.createDomain(opt, managerId); + RegiDomain regiDomain = factory.createDomain(); if (regiDomain != null) { ScriptEvaluator pe = new PythonEvaluator(); Map vars = new HashMap<>(); - + + ManagerId managerId = factory.getManagerId(); RegiCalcRegistry reg = new RegiCalcRegistry(regiDomain, managerId); vars.put("registry", reg); - File scriptFile = opt.getScriptFile(); + File scriptFile = new File(System.getenv("SCRIPT")); try { diff --git a/regi-headless/src/main/java/usace/rowcps/headless/sigstages/importdb/ScriptableImportSigStagesImpl.java b/regi-headless/src/main/java/usace/rowcps/headless/sigstages/importdb/ScriptableImportSigStagesImpl.java index 14ae0d6..b37ea76 100644 --- a/regi-headless/src/main/java/usace/rowcps/headless/sigstages/importdb/ScriptableImportSigStagesImpl.java +++ b/regi-headless/src/main/java/usace/rowcps/headless/sigstages/importdb/ScriptableImportSigStagesImpl.java @@ -10,8 +10,6 @@ import hec.db.DbIoException; import java.nio.file.Path; import java.nio.file.Paths; -import java.sql.Connection; -import java.sql.SQLException; import java.util.Date; import java.util.List; import java.util.logging.Level; @@ -19,8 +17,8 @@ import usace.rowcps.headless.interfaces.ScriptableCalc; import usace.rowcps.regi.level.importer.CSVLocationLevelImportUtil; import usace.rowcps.regi.model.AtLocationLevelManager; -import usace.rowcps.regi.model.RegiDomain; import usace.rowcps.regi.model.ManagerId; +import usace.rowcps.regi.model.RegiDomain; /** * @@ -28,12 +26,8 @@ */ public class ScriptableImportSigStagesImpl implements ScriptableImportSigstages, ScriptableCalc { - private RegiDomain _regiDomain; - private ManagerId _managerId; - - public static final String DELIMETER = System.getProperty("sigstages.delim", ";"); - public static final String CSVDELIMETER = System.getProperty("sigstages.csv.delim", "\n"); - public static final String CSVSEPARATOR = System.getProperty("sigstages.csv.separator", ","); + private final RegiDomain _regiDomain; + private final ManagerId _managerId; public ScriptableImportSigStagesImpl(RegiDomain regiDomain, ManagerId managerId) { @@ -46,48 +40,28 @@ public boolean importSigStages(String file, Date effectiveDate) { boolean retval = true; AtLocationLevelManager atLocLevelMgr = _regiDomain.getAtLocationLevelManager(_managerId); - Connection c = null; - try { - Path p = Paths.get(file); - c = atLocLevelMgr.getPooledConnection(); - CSVLocationLevelImportUtil importUtil = new CSVLocationLevelImportUtil(); - importUtil.setPath(p); - importUtil.readFile(true); - List locationLevels = importUtil.getLocationLevelList(); - for(int locationLevelIndex = 0; locationLevelIndex < locationLevels.size(); locationLevelIndex++) - { - ILocationLevel level = locationLevels.get(locationLevelIndex); - try { - level.setDate(effectiveDate); - atLocLevelMgr.addLocationLevel(level); - } catch (DbConnectionException | DbIoException ex) { - Logger.getLogger(ScriptableImportSigStagesImpl.class.getName()).log(Level.SEVERE, null, ex); - retval = false; - break; - } - } - - if(retval) - { - try { - atLocLevelMgr.commitData(); - } catch (DbConnectionException | DbIoException ex) { - Logger.getLogger(ScriptableImportSigStagesImpl.class.getName()).log(Level.SEVERE, null, ex); - retval = false; - } + Path p = Paths.get(file); + CSVLocationLevelImportUtil importUtil = new CSVLocationLevelImportUtil(); + importUtil.setPath(p); + importUtil.readFile(true); + List locationLevels = importUtil.getLocationLevelList(); + for (ILocationLevel level : locationLevels) { + try { + level.setDate(effectiveDate); + atLocLevelMgr.addLocationLevel(level); + } catch (DbConnectionException | DbIoException ex) { + Logger.getLogger(ScriptableImportSigStagesImpl.class.getName()).log(Level.SEVERE, null, ex); + retval = false; + break; } - } catch (DbConnectionException ex) { - Logger.getLogger(ScriptableImportSigStagesImpl.class.getName()).log(Level.SEVERE, null, ex); } - finally - { - try - { - if(c != null && !c.isClosed()) c.close(); - } - catch (SQLException ex) - { + + if (retval) { + try { + atLocLevelMgr.commitData(); + } catch (DbConnectionException | DbIoException ex) { Logger.getLogger(ScriptableImportSigStagesImpl.class.getName()).log(Level.SEVERE, null, ex); + retval = false; } } return retval; diff --git a/regi-headless/src/main/python/pyproject.toml b/regi-headless/src/main/python/pyproject.toml new file mode 100644 index 0000000..f32f65d --- /dev/null +++ b/regi-headless/src/main/python/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "regi-cli" +version = "@VERSION@" +description = "USACE Regi-Headless Python Bridge" +dependencies = [ + "JPype1>=1.4.1", +] + +[tool.setuptools] +packages = ["regi_cli"] + +[tool.setuptools.package-data] +"regi_cli" = ["lib/*.jar"] \ No newline at end of file diff --git a/regi-headless/src/main/python/regi_cli/__init__.py b/regi-headless/src/main/python/regi_cli/__init__.py new file mode 100644 index 0000000..e3f8a5c --- /dev/null +++ b/regi-headless/src/main/python/regi_cli/__init__.py @@ -0,0 +1,10 @@ +from .regi_cli import regi_session, run_headless +from importlib.metadata import version, PackageNotFoundError + +__all__ = ["regi_session", "run_headless"] + +try: + __version__ = version("regi_cli") # Use the 'name' from pyproject.toml +except PackageNotFoundError: + # package is not installed + __version__ = "unknown" \ No newline at end of file diff --git a/regi-headless/src/main/python/regi_cli/regi_cli.py b/regi-headless/src/main/python/regi_cli/regi_cli.py new file mode 100644 index 0000000..f5fcc5f --- /dev/null +++ b/regi-headless/src/main/python/regi_cli/regi_cli.py @@ -0,0 +1,107 @@ +# Copyright (c) 2026 +# United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC) +# All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL. +# Source may not be released without written approval from HEC + +import os +import logging +import jpype +import jpype.imports +from contextlib import contextmanager +from pathlib import Path + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger("regi-launcher") + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +LIB_PATH = os.path.join(BASE_DIR, "lib", "*") +java_home = os.environ.get('JAVA_HOME') +java_bin = os.path.join(java_home, 'bin') +if java_bin not in os.environ['PATH']: + os.environ['PATH'] = java_bin + os.pathsep + os.environ['PATH'] + +@contextmanager +def regi_session(): + """ + Context manager to handle JVM lifecycle. + Usage: + with regi_session(): + run_headless(my_func) + """ + if not jpype.isJVMStarted(): + agent_jar = Path(os.path.join(BASE_DIR, "lib", "opentelemetry-javaagent.jar")) + agent_flag = f"-javaagent:{agent_jar.absolute()}" + + logger.info(f"Starting JVM with agent: {agent_flag}") + logger.info("Starting JVM...") + jpype.startJVM( + jpype.getDefaultJVMPath(), + agent_flag, + convertStrings=True, + classpath=[LIB_PATH] + ) + + try: + yield + finally: + if jpype.isJVMStarted(): + logger.info("Shutting down JVM...") + jpype.shutdownJVM() + +def run_headless(calculation_callback): + # We must import these inside the function or after JVM starts + from usace.rowcps.headless import HeadlessRegiDomainFactory, RegiCalcRegistry + from usace.rowcps.regi.factories import RowcpsExecutorService + from java.util.concurrent import TimeUnit + GlobalOpenTelemetry = jpype.JClass("io.opentelemetry.api.GlobalOpenTelemetry") + builder = GlobalOpenTelemetry.getTracer("regi-headless").spanBuilder("runHeadless") + builder.setAttribute("cda.url", os.environ.get("CDA_URL", "unknown")) + builder.setAttribute("cwms.office", os.environ.get("OFFICE_ID", "unknown")) + root_span = builder.startSpan() + scope = root_span.makeCurrent() + try: + factory = HeadlessRegiDomainFactory() + logger.info("Attempting to create RegiDomain...") + regi_domain = factory.createDomain() + + if regi_domain is not None: + manager_id = factory.getManagerId() + registry = RegiCalcRegistry(regi_domain, manager_id) + + try: + logger.info("Executing callback...") + calculation_callback(registry) + regi_domain.commitData(manager_id) + except Exception as e: + logger.error("Execution failed.", exc_info=True) + raise + finally: + _shutdown_executor(manager_id) + regi_domain.closing() + except (jpype.JException, Exception) as ex: + # 2. Mirroring the Java catch block + root_span.recordException(ex) + StatusCode = jpype.JClass("io.opentelemetry.api.trace.StatusCode") + root_span.setStatus(StatusCode.ERROR) + + if isinstance(ex, jpype.JException): + logger.error("Java Exception occurred during headless execution:") + ex.printStackTrace() + else: + logger.error(f"Python Exception occurred during headless execution: {ex}") + + # Mirroring: System.exit(-1) + scope.close() + root_span.end() + finally: + # 3. Ensuring cleanup + scope.close() + root_span.end() + +def _shutdown_executor(manager_id): + from usace.rowcps.regi.factories import RowcpsExecutorService + from java.util.concurrent import TimeUnit + res = RowcpsExecutorService.getInstance(manager_id) + res.shutdown() + if not res.awaitTermination(3000, TimeUnit.MILLISECONDS): + res.shutdownNow() \ No newline at end of file diff --git a/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2.py b/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2.py index e5d6542..2606628 100644 --- a/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2.py +++ b/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2.py @@ -10,16 +10,16 @@ timeZone = TimeZone.getTimeZone("US/Central") startCal = Calendar.getInstance(timeZone) startCal.clear() -startCal.set(Calendar.YEAR, 2015) +startCal.set(Calendar.YEAR, 2022) startCal.set(Calendar.MONTH, 4) endCal = Calendar.getInstance(timeZone) endCal.clear() -endCal.set(Calendar.YEAR, 2015) +endCal.set(Calendar.YEAR, 2022) endCal.set(Calendar.MONTH, 6) #gateCalc.computeFlowGroup("SWF", "LEWT2", startCal.getTimeInMillis(), endCal.getTimeInMillis(), "Flow.LEWT2.ConduitGate_Total") -gateCalc.computeAll("SWF", "LEWT2", startCal.getTimeInMillis(), endCal.getTimeInMillis()) +gateCalc.computeAll("SWT", "TENK", startCal.getTimeInMillis(), endCal.getTimeInMillis()) diff --git a/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2_Jpype.py b/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2_Jpype.py new file mode 100644 index 0000000..62538d3 --- /dev/null +++ b/regi-headless/src/test/resources/usace/rowcps/headless/examples/GateFlowCalc2_Jpype.py @@ -0,0 +1,44 @@ +import logging +from regi_cli import regi_session, run_headless + +# Initialize logger for this specific script +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +def compute_gate_flow(registry): + """ + This function contains your specific calculation logic. + The 'registry' object is passed in by the launcher. + """ + # Java imports must happen here (after JVM has started) + from java.util import Calendar, TimeZone + + logger.info("Starting Gate Flow Calculation...") + + # Use the registry passed into the function + gate_calc = registry.getCalculation(1.0, "Gate Flow") + + # Time zone must be set because the Solaris time zone is UTC + time_zone = TimeZone.getTimeZone("US/Central") + + start_cal = Calendar.getInstance(time_zone) + start_cal.clear() + start_cal.set(2022, 4, 1) # YEAR, MONTH (0-indexed, 4=May), DAY + + end_cal = Calendar.getInstance(time_zone) + end_cal.clear() + end_cal.set(2022, 6, 1) # YEAR, MONTH (0-indexed, 6=July), DAY + + logger.info(f"Computing for SWT/TENK from {start_cal.getTime()} to {end_cal.getTime()}") + + # Execute the calculation + gate_calc.computeAll("SWT", "TENK", start_cal.getTimeInMillis(), end_cal.getTimeInMillis()) + + logger.info("Calculation successful.") + +if __name__ == "__main__": + # Use the context manager to handle JVM lifecycle and run the logic + with regi_session(): + run_headless(compute_gate_flow) + + diff --git a/settings.gradle b/settings.gradle index a5682e7..b97cc54 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ rootProject.name = 'regi-headless-parent' include "regi-headless" -include "regi-headless-installer-common" -include "regi-headless-installer-windows" -include "regi-headless-installer-solaris" \ No newline at end of file +//include "regi-headless-installer-common" +//include "regi-headless-installer-windows" +//include "regi-headless-installer-solaris" \ No newline at end of file