diff --git a/.github/workflows/build-and-publish.yaml b/.github/workflows/build-and-publish.yaml new file mode 100644 index 0000000..50dd639 --- /dev/null +++ b/.github/workflows/build-and-publish.yaml @@ -0,0 +1,60 @@ +name: Build and Publish Docker Image + +on: + workflow_call: + inputs: + service_name: { required: true, type: string } + docker_tags: { required: true, type: string } + build_args: { required: false, type: string } + image_title: { required: true, type: string } + image_description: { required: true, type: string } + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate context + run: | + [ -d "./${{ inputs.service_name }}" ] || { echo "Directory missing"; exit 1; } + [ -f "./${{ inputs.service_name }}/Dockerfile" ] || { echo "Dockerfile missing"; exit 1; } + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build local image + uses: docker/build-push-action@v6 + with: + context: ./${{ inputs.service_name }} + load: true # Loads image into local Docker daemon for scanning + tags: local_scan_target:latest + build-args: ${{ inputs.build_args }} + +# - name: Scan image with Anchore +# uses: anchore/scan-action@v3 +# with: +# image: "local_scan_target:latest" +# fail-build: true +# severity-cutoff: high # Fails on High or Critical +# auto-update: true +# # extra-args: "--skip-db-update-check" + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./${{ inputs.service_name }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ inputs.docker_tags }} + build-args: ${{ inputs.build_args }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file diff --git a/.github/workflows/hms.yaml b/.github/workflows/hms.yaml new file mode 100644 index 0000000..8f16ec3 --- /dev/null +++ b/.github/workflows/hms.yaml @@ -0,0 +1,43 @@ +name: Build and Publish auto-fpt-hms Image + +on: + push: + branches: ["main"] + tags: ["v*"] + paths: ["hms/**", ".github/workflows/hms.yaml", ".github/workflows/build-and-publish.yaml"] + pull_request: + branches: ["main"] + paths: ["hms/**", ".github/workflows/hms.yaml", ".github/workflows/build-and-publish.yaml"] + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + owner_lc: ${{ steps.set_owner.outputs.owner_lc }} + steps: + - id: set_owner + run: echo "owner_lc=${OWNER,,}" >> $GITHUB_OUTPUT + env: + OWNER: ${{ github.repository_owner }} + + build-and-push: + needs: prepare + strategy: + matrix: + hms_version: + - { version: '4.12', tag_suffix: '4.12', is_latest: false } + - { version: '4.13', tag_suffix: '4.13', is_latest: true } + - { version: '4.13-beta.6', tag_suffix: '4.13-beta.6', is_latest: false } + - { version: '4.14-beta.1', tag_suffix: '4.14-beta.1', is_latest: false } + uses: ./.github/workflows/build-and-publish.yaml + with: + service_name: hms + docker_tags: | + ghcr.io/${{ needs.prepare.outputs.owner_lc }}/auto-fpt-hms:${{ matrix.hms_version.tag_suffix }} + ${{ matrix.hms_version.is_latest && format('ghcr.io/{0}/auto-fpt-hms:latest', needs.prepare.outputs.owner_lc) || '' }} + build_args: HMS_VERSION=${{ matrix.hms_version.version }} + image_title: auto-fpt-hms + image_description: Docker container for running HEC-HMS hydrological simulations + permissions: + contents: read + packages: write \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..79dcecd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,36 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a +harassment-free experience for everyone. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior: + +- The use of sexualized language or imagery +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional + setting + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to +the project maintainers. All complaints will be reviewed and investigated promptly and +fairly. + +## Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org), version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9d2ae1b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing + +Thank you for your interest in contributing! + +## Commit Messages + +This project uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). +All commit messages **must** follow this format: + +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +### Types + +| Type | When to use | Changelog section | +|---|---|---| +| `feat` | A new feature | Added | +| `fix` | A bug fix | Fixed | +| `perf` | A performance improvement | Changed | +| `refactor` | Code restructuring, no behavior change | Changed | +| `revert` | Reverting a previous commit | Fixed | +| `docs` | Documentation only | _(skipped)_ | +| `test` | Adding or updating tests | _(skipped)_ | +| `chore` | Maintenance, dependencies, tooling | _(skipped)_ | +| `ci` | CI/CD changes | _(skipped)_ | + +### Breaking Changes + +Append `!` after the type, or add `BREAKING CHANGE:` in the footer: + +``` +feat!: drop support for Python 3.11 +``` + +``` +feat: new API + +BREAKING CHANGE: `old_function` has been removed. +``` + +### Examples + +``` +feat(io): add support for GeoParquet output +fix: handle missing CRS in raster inputs +chore: bump ruff to v0.15.5 +docs: add example notebook for elevation grid +``` + +## Submitting Changes + +1. Fork the repository. +1. Create a feature branch. +1. Make your changes with tests. +1. Ensure all checks pass. +1. Submit a pull request. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5808e69 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Dewberry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 6be4411..0c62cb5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # auto-fpt Automated Flood Prediction Tools + +## Directories + +- **hms/** - HEC-HMS runner application. Contains Gradle build configuration and Dockerfile for containerized HEC-HMS (Hydrologic Engineering Center's Hydrologic Modeling System) execution. diff --git a/hms/.dockerignore b/hms/.dockerignore new file mode 100644 index 0000000..c262d5a --- /dev/null +++ b/hms/.dockerignore @@ -0,0 +1,15 @@ +.git +.gitignore +.gitattributes +.vscode +.idea +*.md +.DS_Store +.gradle +build/ +*.iml +.classpath +.project +.settings/ +.miniodata/ +*.log diff --git a/hms/.gitignore b/hms/.gitignore new file mode 100644 index 0000000..da4f499 --- /dev/null +++ b/hms/.gitignore @@ -0,0 +1,47 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +# Gradle +.gradle/ +build/ + +# IDE +.idea/ +.vscode/ +*.iml +.classpath +.project +.settings/ + +# OS +.DS_Store +Thumbs.db + +# Environment & Local +.env +.env.local +.miniodata/ +main +model/ \ No newline at end of file diff --git a/hms/Dockerfile b/hms/Dockerfile new file mode 100644 index 0000000..86d74f4 --- /dev/null +++ b/hms/Dockerfile @@ -0,0 +1,194 @@ +# Build arguments +ARG HMS_VERSION=4.12 + +# First stage: Build the jar for calling hms compute +FROM eclipse-temurin:17-jdk-jammy AS javabuilder + +ARG HMS_VERSION + +RUN apt update && apt -y install wget unzip +RUN mkdir /app +WORKDIR /app + +# Java and Gradle setup +RUN wget https://services.gradle.org/distributions/gradle-7.3.1-bin.zip && \ + mkdir /opt/gradle && \ + unzip -d /opt/gradle gradle-7.3.1-bin.zip && \ + rm gradle-7.3.1-bin.zip +ENV PATH=$PATH:/opt/gradle/gradle-7.3.1/bin + +# Install dependencies for HMS and build +RUN apt -y install libxrender1 libxtst6 libxi6 libfreetype6 libgfortran5 libfontconfig1 + +# Download and extract HMS once (large layer, cached separately) +# Use tar --exclude to skip unnecessary files during extraction +# Note: HMS 4.13 has a malformed filename with duplicate -linux64 suffix +RUN set -e && \ + mkdir -p /tmp/hms-extract && \ + FILE1="hec-hms-${HMS_VERSION}-linux64.tar.gz" && \ + FILE2="hec-hms-${HMS_VERSION}-linux64-linux64.tar.gz" && \ + URL1="https://www.hec.usace.army.mil/nexus/repository/maven-public/mil/army/usace/hec/hec-hms/${HMS_VERSION}-linux64/${FILE1}" && \ + URL2="https://www.hec.usace.army.mil/nexus/repository/maven-public/mil/army/usace/hec/hec-hms/${HMS_VERSION}-linux64/${FILE2}" && \ + (wget "${URL1}" -P /tmp 2>/dev/null) || wget "${URL2}" -P /tmp && \ + if [ -f /tmp/${FILE1} ]; then TARFILE=${FILE1}; else TARFILE=${FILE2}; fi && \ + tar -xzf /tmp/${TARFILE} -C /tmp/hms-extract \ + --exclude='*.zip' \ + --exclude='*.exe' \ + --exclude='*.bat' \ + --exclude='*.cmd' \ + --exclude='*/samples' \ + --exclude='*/docs' \ + --exclude='*/examples' \ + --exclude='*/vortex' && \ + mkdir -p /hec-hms && \ + echo "Contents of /tmp/hms-extract:" && ls -la /tmp/hms-extract && \ + # Find the main HMS directory (could be HEC-HMS-* or hec-hms-*) + HMS_DIR=$(find /tmp/hms-extract -maxdepth 1 -type d -name "*HEC-HMS*" -o -name "*hec-hms*" | head -1) && \ + if [ -z "$HMS_DIR" ]; then \ + echo "ERROR: Could not find HMS directory in extracted files"; \ + ls -la /tmp/hms-extract; \ + exit 1; \ + fi && \ + echo "Found HMS directory: $HMS_DIR" && \ + mv "$HMS_DIR" /hec-hms/HEC-HMS-${HMS_VERSION} && \ + rm -rf /tmp/hms-extract /tmp/${TARFILE} + +# Copy and build source (separate layer allows better caching of HMS deps) +COPY ./build.gradle ./gradlew /app/ +RUN gradle --version + +RUN mkdir -p /app/src/main/java/dewberry +COPY ./RunHMS.java /app/src/main/java/dewberry/RunHMS.java +RUN gradle build --no-daemon -PhmsVersion=${HMS_VERSION} && chmod +x /app/build/libs/hms-compute.jar + + +# Final stage: create the production container +FROM debian:bookworm-slim AS prod + +ARG HMS_VERSION=4.12 +ENV HMS_VERSION=${HMS_VERSION} + +LABEL org.opencontainers.image.title="HEC-HMS Container" \ + org.opencontainers.image.description="Docker container for running HEC-HMS hydrological simulations" \ + org.opencontainers.image.source="https://github.com/Dewberry/autp-fpt" \ + org.opencontainers.image.version="${HMS_VERSION}" + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libxrender1 libxtst6 libxi6 libfreetype6 libgfortran5 libfontconfig1 \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN useradd -m -u 1000 hms + +COPY --from=javabuilder /hec-hms /hec-hms +RUN rm -f /hec-hms/lib && mkdir -p /hec-hms/lib +COPY --from=javabuilder /app/build/libs/hms-compute.jar /hec-hms/lib/hms-compute.jar + +# Verify HMS directory structure +RUN echo "=== HMS Directory Structure ===" && \ + ls -la /hec-hms/ && \ + echo "=== HEC-HMS subdirectories ===" && \ + find /hec-hms -maxdepth 2 -type d | head -20 && \ + echo "=== Checking for hms.jar ===" && \ + find /hec-hms -name "hms.jar" && \ + echo "=== Checking for lib directory ===" && \ + ls -la /hec-hms/HEC-HMS-* 2>/dev/null | head -20 || echo "HEC-HMS directory not found with expected naming" && \ + echo "=== Searching for GDAL native libraries ===" && \ + find /hec-hms -name "*gdal*jni*" -o -name "libgdal*" | head -20 && \ + echo "=== Checking bin directories ===" && \ + find /hec-hms -type d -name "bin" -exec ls -la {} \; + +# Create logging configuration files to suppress verbose output +RUN mkdir -p /etc/hms-logging && \ + cat > /etc/hms-logging/logging.properties <<'EOF' +handlers= java.util.logging.ConsoleHandler +.level= INFO +java.util.logging.ConsoleHandler.level = INFO +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s: %5$s%n +EOF + +RUN cat > /etc/hms-logging/logback.xml <<'EOF' + + + +EOF + +RUN cat > /etc/hms-logging/log4j2.xml <<'EOF' + + + + + +EOF + +ENV HMS_HOME=/hec-hms/HEC-HMS-${HMS_VERSION} +ENV JAVA_EXE=$HMS_HOME/jre/bin/java +ENV JAVA_HOME=$HMS_HOME/jre +ENV PROG=hms.hms +ENV PATH=$HMS_HOME/bin/taudem:$HMS_HOME/bin/mpi:$HMS_HOME/bin:$HMS_HOME/jre/bin/:$HMS_HOME/bin/:$HMS_HOME/jre/lib/:$PATH +ENV GDAL_DATA=$HMS_HOME/bin/gdal/gdal-data +ENV PROJ_LIB=$HMS_HOME/bin/gdal/proj +ENV CLASSPATH=$HMS_HOME/*:$HMS_HOME/lib/*:$HMS_HOME/lib/hec/*:$HMS_HOME/jre/lib/* +ENV HDF5_DISABLE_ERROR_STACK=1 + +RUN chmod +x $HMS_HOME/jre/bin/java && chown -R hms:hms /hec-hms + +# Create a shell wrapper for proper variable expansion at build time (before USER hms) +RUN cat > /usr/local/bin/run-hms.sh <<'SCRIPT' +#!/bin/bash +set -e +HMS_VERSION=${HMS_VERSION:-4.12} +HMS_HOME=/hec-hms/HEC-HMS-${HMS_VERSION} +JAVA_EXE=${HMS_HOME}/jre/bin/java + +if [ ! -f "$JAVA_EXE" ]; then + echo "ERROR: Java executable not found at $JAVA_EXE" + ls -la ${HMS_HOME}/jre/bin/ 2>/dev/null || echo "JRE bin directory not found" + exit 1 +fi + +# Build library path by finding all potential library locations +LIBRARY_PATH="${HMS_HOME}/bin:${HMS_HOME}/lib:${HMS_HOME}/jre/lib/server" + +# Add any gdal-related directories +if [ -d "${HMS_HOME}/bin/gdal" ]; then + LIBRARY_PATH="${LIBRARY_PATH}:${HMS_HOME}/bin/gdal" +fi + +# Search for native libraries recursively +for libdir in $(find ${HMS_HOME} -type d -name "lib*" 2>/dev/null); do + LIBRARY_PATH="${LIBRARY_PATH}:${libdir}" +done + +# Add /hec-hms bin directories +LIBRARY_PATH="${LIBRARY_PATH}:/hec-hms/bin" +if [ -d "/hec-hms/bin/gdal" ]; then + LIBRARY_PATH="${LIBRARY_PATH}:/hec-hms/bin/gdal" +fi + +exec $JAVA_EXE \ + -XX:+ExitOnOutOfMemoryError \ + -XX:MaxRAMPercentage=75 \ + -XX:+UseContainerSupport \ + -Djava.library.path=${LIBRARY_PATH} \ + -Djava.util.logging.config.file=/etc/hms-logging/logging.properties \ + -Dorg.slf4j.simpleLogger.defaultLogLevel=error \ + -Dorg.slf4j.simpleLogger.showDateTime=false \ + -Dorg.slf4j.simpleLogger.showThreadName=false \ + -Dlogback.configurationFile=/etc/hms-logging/logback.xml \ + -Dlog4j.configurationFile=/etc/hms-logging/log4j2.xml \ + -Dlog4j2.configurationFile=/etc/hms-logging/log4j2.xml \ + -Dlog4j2.statusLoggerLevel=OFF \ + -Djava.awt.headless=true \ + -cp "/hec-hms/lib/*:${HMS_HOME}/*:${HMS_HOME}/lib/*:${HMS_HOME}/lib/hec/*" \ + dewberry.RunHMS "$@" +SCRIPT + +RUN chmod +x /usr/local/bin/run-hms.sh + +WORKDIR /app +RUN chown hms:hms /app +USER hms + +ENTRYPOINT ["/usr/local/bin/run-hms.sh"] \ No newline at end of file diff --git a/hms/README.md b/hms/README.md new file mode 100644 index 0000000..d999450 --- /dev/null +++ b/hms/README.md @@ -0,0 +1,60 @@ +# auto-fpt-hms + +Docker container for running HEC-HMS hydrological simulations. + +## Image Size + +~1.88GB - Primarily HMS binaries, libraries, and bundled Java runtime. Image size cannot be significantly reduced without removing essential HMS components. + +## Supported Versions + +**Stable Release (Tested & Working):** +- HMS 4.13 (default) + +**Previous Stable Release:** +- HMS 4.12 + +**Beta Versions (Tested & Working):** +- HMS 4.13-beta.6 +- HMS 4.14-beta.1 + +**Legacy (Not Tested):** +- HMS 4.11, 4.9, 4.10 (available but not actively supported) + +## Usage + +Run an HMS simulation by passing the HMS project file path and simulation name as arguments: + +```bash +docker run -v /path/to/models:/mnt/model hms-docker /mnt/model/project.hms simulation_name +``` + +### Arguments + +- `filepath`: Path to the HMS project file (`.hms`) +- `simname`: Name of the simulation to run within the project + +### Volume Mounts + +Mount your HMS model files at `/mnt/model` in the container. The container expects to find `.hms` project files at this location. + +## Building + +Build the Docker image with a specific HMS version: + +```bash +# Build with default version (4.13) +docker build -t hms-docker . + +# Build with specific versions +docker build --build-arg HMS_VERSION=4.12 -t hms-docker:4.12 . +docker build --build-arg HMS_VERSION=4.13 -t hms-docker:4.13 . +docker build --build-arg HMS_VERSION=4.13-beta.6 -t hms-docker:4.13-beta . +docker build --build-arg HMS_VERSION=4.14-beta.1 -t hms-docker:4.14-beta . +``` + +The build process: +1. Compiles the Java HMS runner application using reflection-based API detection +2. Downloads HEC-HMS binaries and dependencies for the specified version (excludes samples/docs) +3. Creates a minimal production image with all required libraries and non-root user execution +4. Single `RunHMS` class supports all versions through runtime API detection \ No newline at end of file diff --git a/hms/RunHMS.java b/hms/RunHMS.java new file mode 100644 index 0000000..2999df3 --- /dev/null +++ b/hms/RunHMS.java @@ -0,0 +1,488 @@ +package dewberry; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.logging.Logger; +import java.util.logging.ConsoleHandler; +import java.util.logging.SimpleFormatter; + +public class RunHMS { + private static final Logger logger = Logger.getLogger(RunHMS.class.getName()); + + static { + // Configure logger with timestamp format + logger.setUseParentHandlers(false); + ConsoleHandler handler = new ConsoleHandler(); + handler.setFormatter(new SimpleFormatter()); + logger.addHandler(handler); + logger.setLevel(java.util.logging.Level.INFO); + } + + public static void main(String[] args) { + if (args.length < 2) { + logger.severe("Error: You must provide both `hmsFilePath` and `simulationName` as command-line arguments."); + System.exit(1); + } + + String hmsFilePath = args[0]; + String simulationName = args[1]; + + logger.info("Starting HMS simulation: model='" + hmsFilePath + "', simulation='" + simulationName + "'"); + + try { + // Try multiple API versions in order of preference + + // Try 4.14+ API first (static factory method) + int result = tryStaticOpen(hmsFilePath, simulationName); + if (result == 0) { + // Success + System.exit(0); + } else if (result == 1) { + // Errors detected in simulation + System.exit(1); + } + // result == 2 means API not available, try next + + // Fall back to 4.12-4.13 API (constructor pattern) + result = tryConstructorOpen(hmsFilePath, simulationName); + if (result == 0) { + // Success + System.exit(0); + } else if (result == 1) { + // Errors detected in simulation + System.exit(1); + } + // result == 2 means API not available + + // If we get here, no compatible API was found + throw new Exception("No compatible HMS API found in classpath"); + + } catch (Exception e) { + logger.severe("Error running HMS: " + e.getMessage()); + System.exit(1); + } + } + + /** + * Print available runs in the project for debugging + */ + private static void printAvailableRuns(Class projectClass, Object project) { + try { + // Try various method names to get list of runs + String[] methodNames = {"getRunNames", "getRuns", "getSimulations", "getComputeRuns", + "getComputeRunManager", "getRunManager", "getSimulationManager"}; + + // First, try to get runs directly + for (String methodName : methodNames) { + try { + Method method = projectClass.getMethod(methodName); + Object result = method.invoke(project); + + if (result != null) { + System.err.println("Available runs in project:"); + if (result instanceof java.util.Collection) { + java.util.Collection collection = (java.util.Collection) result; + if (collection.isEmpty()) { + System.err.println(" (none - project has no runs)"); + } else { + for (Object run : collection) { + System.err.println(" - " + run); + } + } + return; + } else if (result.getClass().isArray()) { + Object[] array = (Object[]) result; + if (array.length == 0) { + System.err.println(" (none - project has no runs)"); + } else { + for (Object run : array) { + System.err.println(" - " + run); + } + } + return; + } else { + // Try to get runs from the manager object + System.err.println("Attempting to get runs from manager..."); + printAvailableRunsFromManager(result.getClass(), result); + return; + } + } + } catch (NoSuchMethodException e) { + // Try next method + continue; + } + } + + System.err.println("Could not retrieve available runs - method not found"); + System.err.println(" Available methods on Project:"); + // List all available methods to help with debugging + for (Method m : projectClass.getMethods()) { + if (m.getName().toLowerCase().contains("run") || + m.getName().toLowerCase().contains("simul") || + m.getName().toLowerCase().contains("compute")) { + System.err.println(" - " + m.getName()); + } + } + } catch (Exception e) { + System.err.println("Error while trying to list available runs: " + e.getMessage()); + } + } + + /** + * Try to get runs from a manager object + */ + private static void printAvailableRunsFromManager(Class managerClass, Object manager) { + try { + System.err.println("Manager class: " + managerClass.getName()); + String[] methodNames = {"getRunNames", "getNames", "getAll", "getAllRuns", "listAll", "values", + "getRuns", "getSimulations", "getComputeRuns", "elements", "toArray"}; + + boolean foundMethod = false; + for (String methodName : methodNames) { + try { + Method method = managerClass.getMethod(methodName); + foundMethod = true; + Object result = method.invoke(manager); + + if (result != null) { + System.err.println("Available runs in project (via " + methodName + "):"); + if (result instanceof java.util.Collection) { + java.util.Collection collection = (java.util.Collection) result; + if (collection.isEmpty()) { + System.err.println(" (none)"); + } else { + for (Object run : collection) { + System.err.println(" - " + run); + } + } + return; + } else if (result.getClass().isArray()) { + Object[] array = (Object[]) result; + if (array.length == 0) { + System.err.println(" (none)"); + } else { + for (Object run : array) { + System.err.println(" - " + run); + } + } + return; + } + } + } catch (NoSuchMethodException e) { + // Method doesn't exist, try next + continue; + } catch (Exception e) { + System.err.println("Error calling " + methodName + ": " + e.getMessage()); + } + } + + // Fallback: list all available methods on the manager + if (!foundMethod) { + System.err.println("None of the standard run-listing methods were found."); + } + System.err.println("Available methods on manager (" + managerClass.getName() + "):"); + Method[] allMethods = managerClass.getMethods(); + int count = 0; + for (Method m : allMethods) { + String methodName = m.getName(); + if (!methodName.startsWith("wait") && !methodName.equals("getClass") && !methodName.equals("hashCode") && + !methodName.equals("equals") && !methodName.equals("toString") && !methodName.equals("notify") && + !methodName.equals("notifyAll")) { + System.err.println(" - " + methodName); + count++; + } + } + if (count == 0) { + System.err.println(" (no public methods found)"); + } + } catch (Exception e) { + System.err.println("Exception in printAvailableRunsFromManager: " + e.getMessage()); + } + } + + /** + * Additional error checking via reflection (fallback if stderr capture misses errors) + */ + private static boolean hasProjectErrors(Class projectClass, Object project) { + try { + // Try common error checking methods on the project + String[] errorMethodNames = {"getErrorCount", "hasErrors", "getErrors", "getMessageCount", "getMessages"}; + + for (String methodName : errorMethodNames) { + try { + Method method = projectClass.getMethod(methodName); + Object result = method.invoke(project); + + if (result instanceof Integer) { + int count = (Integer) result; + if (count > 0) { + System.err.println("Project has " + count + " errors"); + return true; + } + } else if (result instanceof Boolean) { + boolean hasErrors = (Boolean) result; + if (hasErrors) { + System.err.println("Project reports errors"); + return true; + } + } + } catch (NoSuchMethodException e) { + // Method doesn't exist, try next one + continue; + } + } + + return false; + } catch (Exception e) { + // If we can't check for errors via API, stderr capture should have caught them + return false; + } + } + + /** + * Try HMS 4.14+ API using static Project.open(String) method + * Returns: 0=success, 1=errors detected, 2=API not available + */ + private static int tryStaticOpen(String hmsFilePath, String simulationName) { + Class projectClass = null; + Object project = null; + + try { + projectClass = Class.forName("hms.model.Project"); + Method openMethod = projectClass.getMethod("open", String.class); + Method computeRunMethod = projectClass.getMethod("computeRun", String.class); + Method closeMethod = projectClass.getMethod("close"); + + + project = openMethod.invoke(null, hmsFilePath); + + // Capture both stdout and stderr to check for HMS errors + PrintStream originalOut = System.out; + PrintStream originalErr = System.err; + ByteArrayOutputStream capturedOut = new ByteArrayOutputStream(); + ByteArrayOutputStream capturedErr = new ByteArrayOutputStream(); + PrintStream outStream = new PrintStream(capturedOut); + PrintStream errStream = new PrintStream(capturedErr); + System.setOut(outStream); + System.setErr(errStream); + + boolean hasErrors = false; + Object result = null; + + try { + result = computeRunMethod.invoke(project, simulationName); + } finally { + // Always restore stdout and stderr + System.setOut(originalOut); + System.setErr(originalErr); + outStream.flush(); + errStream.flush(); + String stdoutOutput = capturedOut.toString(); + String stderrOutput = capturedErr.toString(); + + // Print all captured output + if (!stdoutOutput.isEmpty()) { + System.out.print(stdoutOutput); + } + if (!stderrOutput.isEmpty()) { + System.err.print(stderrOutput); + } + + // Check for HMS error patterns in either output + String allOutput = stdoutOutput + stderrOutput; + if (allOutput.contains("ERROR") && (allOutput.contains("ERROR 2") || allOutput.contains("Could not find") || allOutput.contains("does not exist"))) { + hasErrors = true; + } + } + + // If HMS errors were detected, fail + if (hasErrors) { + logger.warning("HMS simulation failed - errors detected in output"); + closeMethod.invoke(project); + return 1; + } + + // Check if computeRun returned a status code (usually 0 for success) + if (result instanceof Integer) { + int status = (Integer) result; + if (status != 0) { + logger.warning("HMS simulation failed with status code: " + status); + closeMethod.invoke(project); + return 1; + } + } + + // Check for errors in the project after simulation + if (hasProjectErrors(projectClass, project)) { + logger.warning("HMS simulation completed but with errors"); + closeMethod.invoke(project); + return 1; + } + + closeMethod.invoke(project); + + logger.info("Completed HMS simulation: model='" + hmsFilePath + "', simulation='" + simulationName + "'"); + return 0; + + } catch (NoSuchMethodException e) { + logger.severe("4.14+ API not available: method not found - " + e.getMessage()); + return 2; + } catch (ClassNotFoundException e) { + logger.severe("4.14+ API not available: class not found - " + e.getMessage()); + return 2; + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + String errorMsg = cause != null ? cause.getMessage() : e.getMessage(); + logger.severe("4.14+ API method invocation failed: " + errorMsg); + if (cause != null) { + } else { + } + // "Invalid run" and similar errors are simulation failures, not API incompatibility + if (errorMsg != null && (errorMsg.contains("Invalid run") || errorMsg.contains("not found") || + errorMsg.contains("does not exist") || errorMsg.contains("Could not") || + errorMsg.contains("error") || errorMsg.toLowerCase().contains("failed"))) { + // Provide debugging info for invalid run + if (errorMsg.contains("Invalid run")) { + logger.severe("ERROR: Invalid run name: '" + simulationName + "'"); + logger.info("Hint: The run name '" + simulationName + "' was not found in the HMS project."); + logger.info("Verify that:"); + logger.info(" 1. The run exists in the .hms file"); + logger.info(" 2. The spelling/capitalization matches exactly"); + logger.info(" 3. The file path '" + hmsFilePath + "' is correct"); + } else if (errorMsg.contains("does not exist")) { + logger.severe("ERROR: HMS model file not found: '" + hmsFilePath + "'"); + logger.info("Hint: The HMS project file could not be opened."); + logger.info("Verify that:"); + logger.info(" 1. The file path is correct: '" + hmsFilePath + "'"); + logger.info(" 2. The file exists and is readable"); + logger.info(" 3. If using Docker, the volume mount is correct: -v LOCAL_PATH:CONTAINER_PATH"); + logger.info(" 4. Ensure LOCAL_MODEL_DIR exists and contains the .hms file"); + } + return 1; + } + return 2; + } catch (Exception e) { + logger.severe("4.14+ API failed: " + e.getMessage()); + return 2; + } + } + + /** + * Try HMS 4.12-4.13 API using Project constructor + * Returns: 0=success, 1=errors detected, 2=API not available + */ + private static int tryConstructorOpen(String hmsFilePath, String simulationName) { + Class projectClass = null; + Object project = null; + + try { + projectClass = Class.forName("hms.model.Project"); + Method computeRunMethod = projectClass.getMethod("computeRun", String.class); + Method closeMethod = projectClass.getMethod("close"); + + + project = projectClass.getConstructor(String.class).newInstance(hmsFilePath); + + // Capture both stdout and stderr to check for HMS errors + PrintStream originalOut = System.out; + PrintStream originalErr = System.err; + ByteArrayOutputStream capturedOut = new ByteArrayOutputStream(); + ByteArrayOutputStream capturedErr = new ByteArrayOutputStream(); + PrintStream outStream = new PrintStream(capturedOut); + PrintStream errStream = new PrintStream(capturedErr); + System.setOut(outStream); + System.setErr(errStream); + + boolean hasErrors = false; + Object result = null; + + try { + result = computeRunMethod.invoke(project, simulationName); + } finally { + // Always restore stdout and stderr + System.setOut(originalOut); + System.setErr(originalErr); + outStream.flush(); + errStream.flush(); + String stdoutOutput = capturedOut.toString(); + String stderrOutput = capturedErr.toString(); + + // Print all captured output + if (!stdoutOutput.isEmpty()) { + System.out.print(stdoutOutput); + } + if (!stderrOutput.isEmpty()) { + System.err.print(stderrOutput); + } + + // Check for HMS error patterns in either output + String allOutput = stdoutOutput + stderrOutput; + if (allOutput.contains("ERROR") && (allOutput.contains("ERROR 2") || allOutput.contains("Could not find") || allOutput.contains("does not exist"))) { + hasErrors = true; + } + } + + // If HMS errors were detected, fail + if (hasErrors) { + logger.warning("HMS simulation failed - errors detected in output"); + closeMethod.invoke(project); + return 1; + } + + // Check for errors in the project after simulation + if (hasProjectErrors(projectClass, project)) { + logger.warning("HMS simulation completed but with errors"); + closeMethod.invoke(project); + return 1; + } + + closeMethod.invoke(project); + + logger.info("Completed HMS simulation: model='" + hmsFilePath + "', simulation='" + simulationName + "'"); + return 0; + + } catch (NoSuchMethodException e) { + logger.severe("4.12-4.13 API not available: method not found - " + e.getMessage()); + return 2; + } catch (ClassNotFoundException e) { + logger.severe("4.12-4.13 API not available: class not found - " + e.getMessage()); + return 2; + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + String errorMsg = cause != null ? cause.getMessage() : e.getMessage(); + logger.severe("4.12-4.13 API method invocation failed: " + errorMsg); + if (cause != null) { + } else { + } + // "Invalid run" and similar errors are simulation failures, not API incompatibility + if (errorMsg != null && (errorMsg.contains("Invalid run") || errorMsg.contains("not found") || + errorMsg.contains("does not exist") || errorMsg.contains("Could not") || + errorMsg.contains("error") || errorMsg.toLowerCase().contains("failed"))) { + // Provide debugging info for invalid run + if (errorMsg.contains("Invalid run")) { + logger.severe("ERROR: Invalid run name: '" + simulationName + "'"); + logger.info("Hint: The run name '" + simulationName + "' was not found in the HMS project."); + logger.info("Verify that:"); + logger.info(" 1. The run exists in the .hms file"); + logger.info(" 2. The spelling/capitalization matches exactly"); + logger.info(" 3. The file path '" + hmsFilePath + "' is correct"); + } else if (errorMsg.contains("does not exist")) { + logger.severe("ERROR: HMS model file not found: '" + hmsFilePath + "'"); + logger.info("Hint: The HMS project file could not be opened."); + logger.info("Verify that:"); + logger.info(" 1. The file path is correct: '" + hmsFilePath + "'"); + logger.info(" 2. The file exists and is readable"); + logger.info(" 3. If using Docker, the volume mount is correct: -v LOCAL_PATH:CONTAINER_PATH"); + logger.info(" 4. Ensure LOCAL_MODEL_DIR exists and contains the .hms file"); + } + return 1; + } + return 2; + } catch (Exception e) { + logger.severe("4.12-4.13 API failed: " + e.getMessage()); + return 2; + } + } +} diff --git a/hms/build.gradle b/hms/build.gradle new file mode 100644 index 0000000..0087ada --- /dev/null +++ b/hms/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java-library' +} + +repositories { + mavenCentral() +} + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +// Allow HMS version to be passed as gradle property (-PhmsVersion=4.12) +// Default to 4.12 for local builds +ext.hmsVersion = project.hasProperty('hmsVersion') ? project.hmsVersion : '4.12' + +configurations { + linux_x64 +} + +dependencies { + // HMS libraries are provided at runtime by the Docker container + compileOnly files("/hec-hms/HEC-HMS-${hmsVersion}/hms.jar") + compileOnly fileTree(dir: "/hec-hms/HEC-HMS-${hmsVersion}/lib", include: '*.jar') +} + +jar { + zip64 = true + manifest { + attributes 'Main-Class': 'dewberry.RunHMS' + } + archiveFileName = 'hms-compute.jar' +} + +group 'dewberry' +version '0.1.0' + diff --git a/hms/example-usage.sh b/hms/example-usage.sh new file mode 100755 index 0000000..d7ab9f2 --- /dev/null +++ b/hms/example-usage.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# HMS_VERSION=${1:-4.12} +HMS_VERSION=${1:-4.13} +# HMS_VERSION=${1:-4.13-beta.6} +# HMS_VERSION=${1:-4.14-beta.1} + +IMAGE=auto-fpt-hms:$HMS_VERSION + +docker build -t $IMAGE --build-arg HMS_VERSION=$HMS_VERSION . +# docker pull ghcr.io/dewberry/auto-fpt-hms:$HMS_VERSION || true + +LOCAL_MODEL_DIR=/home/username/samples/castro +HMS_MODEL_NAME=castro.hms +SIMULATION='Current' + +CONTAINER_MODEL_DIR=/mnt/model +s +docker run \ + -v $LOCAL_MODEL_DIR:$CONTAINER_MODEL_DIR \ + $IMAGE \ + $CONTAINER_MODEL_DIR/$HMS_MODEL_NAME \ + $SIMULATION diff --git a/hms/gradlew b/hms/gradlew new file mode 100755 index 0000000..cbede15 --- /dev/null +++ b/hms/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright � 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions �$var�, �${var}�, �${var:-default}�, �${var+SET}�, +# �${var#prefix}�, �${var%suffix}�, and �$( cmd )�; +# * compound commands having a testable exit status, especially �case�; +# * various built-in commands including �command�, �set�, and �ulimit�. +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" \ No newline at end of file