From f22043ed6e69919900ce3283f33ec6d4e4544f79 Mon Sep 17 00:00:00 2001 From: tyronechrisharris <142608718+tyronechrisharris@users.noreply.github.com> Date: Tue, 5 May 2026 17:49:36 -0400 Subject: [PATCH 1/2] release: finalize OSCAR 3.5.1 deployment hardening, monitoring, docs, and DB pool fixes --- README.md | 416 +++++++++----- build-all.sh | 17 +- changelog.md | 34 +- .../MediaMTX_OSCAR_camera_proxy_guide.md | 389 +++++++++++++ .../Node_Administration_3.5.1_addendum.md | 25 + .../OSCAR_System_Documentation_Manual_3.5.md | 82 ++- .../OSCAR_launch_monitoring_guide.md | 511 +++++++++++++++++ dist/documentation/Release_Notes_3.5.1.md | 533 ++++++++++++++++++ .../Standard_PostgreSQL_Setup.md | 101 ++++ dist/release/check-oscar-status.ps1 | 136 +++++ dist/release/check-oscar-status.sh | 225 ++++++++ dist/release/env.template | 55 ++ dist/release/launch-all-arm.sh | 350 ++++++++++-- dist/release/launch-all-arm_old.sh | 247 ++++++++ dist/release/launch-all.bat | 381 ++++++++++--- dist/release/launch-all.sh | 335 +++++++++-- dist/release/launch-all_old.bat | 192 +++++++ dist/release/launch-all_old.sh | 111 ++++ dist/release/monitor-oscar.bat | 249 ++++++++ dist/release/monitor-oscar.sh | 342 +++++++++++ dist/release/monitor-oscar_old.bat | 129 +++++ dist/release/monitor-oscar_old.sh | 254 +++++++++ dist/scripts/standard/launch.bat | 259 ++++++++- dist/scripts/standard/launch.sh | 248 +++++++- dist/scripts/standard/launch_old.bat | 144 +++++ dist/scripts/standard/launch_old.sh | 141 +++++ dist/scripts/standard/load_trusted_certs.bat | 123 ++-- 27 files changed, 5554 insertions(+), 475 deletions(-) create mode 100644 dist/documentation/MediaMTX_OSCAR_camera_proxy_guide.md create mode 100644 dist/documentation/Node_Administration_3.5.1_addendum.md create mode 100644 dist/documentation/OSCAR_launch_monitoring_guide.md create mode 100644 dist/documentation/Release_Notes_3.5.1.md create mode 100644 dist/documentation/Standard_PostgreSQL_Setup.md create mode 100644 dist/release/check-oscar-status.ps1 create mode 100644 dist/release/check-oscar-status.sh create mode 100644 dist/release/env.template mode change 100755 => 100644 dist/release/launch-all-arm.sh create mode 100644 dist/release/launch-all-arm_old.sh mode change 100755 => 100644 dist/release/launch-all.bat mode change 100755 => 100644 dist/release/launch-all.sh create mode 100644 dist/release/launch-all_old.bat create mode 100644 dist/release/launch-all_old.sh create mode 100644 dist/release/monitor-oscar.bat create mode 100644 dist/release/monitor-oscar.sh create mode 100644 dist/release/monitor-oscar_old.bat create mode 100644 dist/release/monitor-oscar_old.sh mode change 100755 => 100644 dist/scripts/standard/launch.bat mode change 100755 => 100644 dist/scripts/standard/launch.sh create mode 100644 dist/scripts/standard/launch_old.bat create mode 100644 dist/scripts/standard/launch_old.sh diff --git a/README.md b/README.md index 4c0b9e4..1655ea3 100644 --- a/README.md +++ b/README.md @@ -1,180 +1,284 @@ # OSH OAKRIDGE BUILDNODE -This repository combines all the OSH modules and dependencies to deploy the OSH server and client for ORNL. +This repository packages the OSH server and OSCAR client deployment used for ORNL field and test systems. ## Requirements -- [Java 21.0.10+](https://www.oracle.com/java/technologies/downloads/#java21) -- [Docker engine](https://www.docker.com) -- [Oakridge Build Node Repository](https://github.com/Botts-Innovative-Research/osh-oakridge-buildnode) -- Node v22 - -## Quick Start -1. **Download the latest release** - - Go to the Releases section of the repository and download the latest compiled release archive (for example, `oscar-3.3.5.zip`). -2. **Extract the archive** - - Extract the downloaded ZIP file to a directory of your choice. -3. **Verify Docker Engine** - - Ensure that [Docker engine](https://www.docker.com) is installed and actively running on your host machine. -4. **Launch the system** - - Open a terminal or command prompt in the extracted directory and run the OS-specific launch script: - - **Windows:** Run `launch-all.bat` - - **Linux/macOS:** Run `./launch-all.sh` - - **ARM systems:** Run `./launch-all-arm.sh` - -For a complete guide covering architecture, deployment, configuration, operations, and troubleshooting, please refer to the [OSCAR System Documentation Manual](dist/documentation/OSCAR_System_Documentation_Manual_3.5.md). - -## Installation -Clone the repository and update all submodules recursively + +- Java 21 or newer +- Docker Engine or Docker Desktop, running before launch +- A packaged OSCAR release archive, or a local source checkout for build workflows +- Node v22 only when building from source + +## OSCAR 3.5.1 packaged release quick start + +This section is for operators using the **prebuilt OSCAR 3.5.1 release ZIP**. + +### 1. Verify required dependencies + +Windows PowerShell: + +```powershell +java -version +docker version +``` + +Linux: + +```bash +java -version +docker version +``` + +Use **Java 21 or newer**. The launch scripts validate dependencies and will stop early if Java or Docker is missing or too old. + +### 2. If you were previously running OSCAR, start fresh + +Before extracting **OSCAR 3.5.1**, stop the old deployment and remove old local runtime artifacts. + +Windows PowerShell: + +```powershell +Get-CimInstance Win32_Process | + Where-Object { + $_.Name -match '^java(\.exe)?$' -and + $_.CommandLine -like '*com.botts.impl.security.SensorHubWrapper*' + } | + Select-Object ProcessId, CommandLine + +# Stop the old OSCAR JVM if one is still running +Stop-Process -Id -Force + +# Stop and remove the old PostGIS container +docker rm -f oscar-postgis-container + +# Remove the old Docker network if it exists +docker network rm oscar-postgis-network +``` + +Linux: + +```bash +pgrep -af 'com.botts.impl.security.SensorHubWrapper' +kill + +docker rm -f oscar-postgis-container +docker network rm oscar-postgis-network || true +``` + +Then delete the old extracted release folder, such as **oscar-3.5.0**, before extracting **oscar-3.5.1**. + +### 3. Extract the release archive + +Extract the downloaded ZIP to a fresh working directory. + +### 4. Create the runtime environment file + +For packaged releases, use the environment file that ships with the archive: + +- if the package includes **env.txt**, rename it to **.env** +- if the package includes **env.template**, copy it to **.env** + +Windows PowerShell: + +```powershell +Copy-Item .\env.template .\.env +``` + +Linux: + +```bash +cp env.template .env +``` + +Edit `.env` before first launch and at minimum confirm: + +- `SYSTEM_PROFILE` +- `DB_NAME` +- `DB_USER` +- `DB_PASSWORD` +- `DB_PORT` +- `CONTAINER_NAME` + +Useful optional settings include: + +- `FORCE_RESTART=1` to replace an already-running OSCAR instance +- `ATTACH_TO_EXISTING=1` to monitor an already-running OSCAR instance +- `MAX_WAIT_SECONDS=300` +- `RETRY_MAX=120` +- `RETRY_INTERVAL=2` +- `POSTGIS_READY_DELAY=5` + +### 5. Preferred first start: use the monitoring script + +For testing, burn-in, and side-by-side field deployment, start OSCAR with the monitoring wrapper instead of launching the node directly. + +Windows: + +```bat +monitor-oscar.bat +``` + +Linux: + +```bash +chmod +x launch-all.sh osh-node-oscar/launch.sh monitor-oscar.sh check-oscar-status.sh +./monitor-oscar.sh +``` + +This is the recommended first-run path because it: + +- starts PostGIS and OSCAR using the current launch scripts +- captures memory, thread, JFR, and database snapshots over time +- produces a monitor directory and status report inputs automatically + +### 6. Routine start without monitoring + +When monitoring is not needed, use the top-level launch script: + +Windows: + +```bat +launch-all.bat +``` + +Linux: + +```bash +./launch-all.sh +``` + +Prefer these **sessionless top-level launchers** over calling `osh-node-oscar/launch.(sh|bat)` directly unless you are debugging the node itself. + +### 7. Running-instance handling + +The launch and monitor scripts now detect already-running OSCAR JVMs. + +Default behavior: + +- `launch-all` refuses to start if OSCAR is already running +- `monitor-oscar` refuses to start if OSCAR is already running + +Optional behaviors: + +- set `FORCE_RESTART=1` to stop the running OSCAR instance and start fresh +- set `ATTACH_TO_EXISTING=1` when using `monitor-oscar` to monitor the running instance instead of replacing it + +### 8. Generate a status report after startup + +After the system has been up long enough to settle, generate a one-file report. + +Windows PowerShell: + +```powershell +powershell -ExecutionPolicy Bypass -File .\check-oscar-status.ps1 +``` + +Linux: + +```bash +./check-oscar-status.sh +``` + +### 9. Admin access + +The admin username is typically **admin**. Do **not** assume the packaged password is always `admin`. + +For packaged releases, the initial password should be managed through the packaged secret file or environment-driven password initialization flow. Verify the package contents, then change the password before production use. + +## Building from source + +Clone the repository and update all submodules recursively: ```bash git clone git@github.com:Botts-Innovative-Research/osh-oakridge-buildnode.git --recursive ``` -If you've already cloned without `--recursive`, run: + +If you already cloned without `--recursive`, run: + ```bash cd path/to/osh-oakridge-buildnode git submodule update --init --recursive ``` -## Build + +## Build + Navigate to the project directory: ```bash cd path/to/osh-oakridge-buildnode ``` -Run the build script (macOS/Linux): +Run the build script. + +Linux/macOS: ```bash ./build-all.sh ``` -Run the build script (Windows): +Windows: -```bash -./build-all.bat -``` - -After the build completes, it can be located in `build/distributions/` - -## Deploy and Start OSH Node -1. Unzip the distribution using the command line or File Explorer: - - Option 1: Command Line - ```bash - unzip build/distributions/osh-node-oscar-1.0.zip - cd osh-node-oscar-1.0/osh-node-oscar-1.0 - ``` - ```bash - tar -xf build/distributions/osh-node-oscar-1.0.zip - cd osh-node-oscar-1.0/osh-node-oscar-1.0 - ``` - Option 2: Use File Explorer - 1. Navigate to `path/to/osh-oakridge-buildnode/build/distributions/` - 2. Right-click `osh-node-oscar-1.0.zip`. - 3. Select **Extract All..** - 4. Choose your destination, (or leave the default) and extract. -1. Launch the OSH node: - Run the launch script, "launch.sh" for linux/mac and "launch.bat" for windows. -2. Access the OSH Node -- Remote: **[ip-address]:8282/sensorhub/admin** -- Locally: **http://localhost:8282/sensorhub/admin** - -The default credentials to access the OSH Node are admin:admin. This can be changed in the Security section of the admin page. - -For documentation on configuring a Lane System on the OSH Admin panel, please refer to the OSCAR Documentation provided in the Google Drive documentation folder. - -## Deploy the Client -After configuring the Lanes on the OSH Admin Panel, you can navigate to the Clients endpoint: -- Remote: **[ip-address]:8282** -- Local: **http://localhost:8282/** - -For documentation on configuring a server on the OSCAR Client refer to the OSCAR Documentation provided in the Google Drive documentation folder. - -# Releasing a New Version - -## Release Checklist -Before releasing, ensure the following on the `dev` branch: -1. Update `version` in `build.gradle` to match the release version (e.g. `"3.2.0"`) -2. Update `deploymentName` in `dist/config/standard/config.json` to `"OSCAR "` (e.g. `"OSCAR 3.2.0"`) -3. Ensure there is no `pgdata` directory in `dist/release/postgis` -4. Verify the build succeeds locally with `./build-all.sh` or `./build-all.bat` - -## Release Steps -1. **Merge `dev` into `main`:** - ```bash - git checkout main - git pull origin main - git merge dev - git push origin main - ``` - Alternatively, create a pull request from `dev` → `main` on GitHub and merge it. - -2. **Tag the release on `main`:** - ```bash - git checkout main - git pull origin main - git tag v # e.g. git tag v3.2.0 - git push origin v - ``` - -3. **The release workflow runs automatically.** It will: - - Validate that the tag is on the `main` branch - - Verify version numbers match the tag in `build.gradle` and `config.json` - - Check that `pgdata` does not exist in the release directory - - Build the project (Gradle + oscar-viewer) - - Package the source code with all submodules included - - Create a GitHub Release with the build artifact and source archive - -# PostgreSQL Configuration -There are some tweaks that can be made to the PostgreSQL configuration to make it perform better. -Below is a list of suggested configuration parameters at varying levels of maximum system RAM. - -`shared_buffers` - Should be around 25% of maximum RAM -`effective_cache_size` - Should be around 70-75% of maximum RAM -`work_mem` - 16MB to 64MB. Depends on maximum system memory and size of the load -`maintenance_work_mem` - 512MB to 2GB. Depends on the load, but it's OK to try high numbers - -# Secure Node Over TLS (HTTPS) -In order to secure the OSH node over TLS, you must generate a Java keystore with an SSL certificate. - -Below is the command to generate a keystore with a self-signed certificate. - -`keytool -genkeypair -alias -keyalg RSA -keysize 2048 -validity -keystore .jks -storepass -keypass -dname "CN=, OU=, O=, L=, ST=, C=" -ext "SAN="` - -Then, in your OSH config (`config.json`), or in the Admin Panel under `Network` -> `HTTP Server`, you must specify the key store path, password, key alias, and HTTPS port. - -An example of the `config.json`'s HTTP Server config is shown below: - -```json -{ - "objClass": "org.sensorhub.impl.service.HttpServerConfig", - "httpPort": 8282, - "httpsPort": 8443, - "servletsRootUrl": "/sensorhub", - "authMethod": "BASIC", - "keyStorePath": "osh-keystore.jks", - "keyStorePassword": "changeit", - "keyAlias": "oscar-key", - "trustStorePath": ".keystore/ssl_trust", - "enableCORS": true, - "id": "5cb05c9c-9e08-4fa1-8731-ffaa5846bdc1", - "autoStart": true, - "moduleClass": "org.sensorhub.impl.service.HttpServer", - "name": "HTTP Server" -} -``` - -You can also edit this information in the OSH launch scripts at `osh-node-oscar/launch.(sh|bat)` - -```shell -java -Xms6g -Xmx6g -Xss256k -XX:ReservedCodeCacheSize=512m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError \ - -Dlogback.configurationFile=./logback.xml \ - -cp "lib/*" \ - -Djava.system.class.loader="org.sensorhub.utils.NativeClassLoader" \ - -Djavax.net.ssl.keyStore="./osh-keystore.jks" \ - -Djavax.net.ssl.keyStorePassword="changeit" \ - -Djavax.net.ssl.trustStore="$SCRIPT_DIR/trustStore.jks" \ - -Djavax.net.ssl.trustStorePassword="changeit" \ - -Djava.library.path="./nativelibs" \ - com.botts.impl.security.SensorHubWrapper ./config.json ./db - -``` \ No newline at end of file +```bat +build-all.bat +``` + +After the build completes, the output is written under `build/distributions/`. + +## Source-tree deployment + +If you are testing from a source checkout instead of a packaged release: + +1. create `.env` from `env.template` +2. verify Java 21 and Docker +3. launch with `monitor-oscar` for first-run validation +4. use `check-oscar-status` after the system reaches steady state + +## MediaMTX for larger camera deployments + +For test systems and larger multi-lane deployments, consider placing **MediaMTX** in front of camera streams so OSCAR connects to stable local RTSP proxy paths instead of directly to every camera. See the updated MediaMTX guide in `dist/documentation/MediaMTX_OSCAR_camera_proxy_guide.md`. + +## PostgreSQL tuning + +The packaged launch scripts now size PostgreSQL by `SYSTEM_PROFILE`. + +Representative values: + +- `RPI4` -> max_connections 75 +- `8GB` -> max_connections 125 +- `16GB` -> max_connections 200 +- `32GB` -> max_connections 300 + +The launchers also set: + +- `superuser_reserved_connections=10` +- `idle_session_timeout=600000` +- connection and disconnection logging + +## Secure node over TLS + +To secure the OSH node over TLS, generate a Java keystore with an SSL certificate. + +```text +keytool -genkeypair -alias -keyalg RSA -keysize 2048 -validity -keystore .jks -storepass -keypass -dname "CN=, OU=, O=, L=, ST=, C=" -ext "SAN=" +``` + +Then configure the keystore path, password, alias, and HTTPS port in `config.json` or in the Admin Panel under **Network -> HTTP Server**. + +## Releasing a new version + +### Release checklist + +Before releasing from `dev`: + +1. update `version` in `build.gradle` +2. update `deploymentName` in `dist/config/standard/config.json` +3. ensure `dist/release/postgis/pgdata` is not packaged +4. verify the release ZIP name matches the intended version, such as `oscar-3.5.1.zip` +5. verify the release root directory name also matches the intended version +6. verify `env.template`, release notes, README, and launch documentation all reflect the same version + +### Release steps + +1. merge `dev` into `main` +2. tag the release on `main` +3. push the release tag and allow the workflow to build and publish the release artifacts diff --git a/build-all.sh b/build-all.sh index 9d635bb..9f0a1a7 100755 --- a/build-all.sh +++ b/build-all.sh @@ -1,10 +1,21 @@ #!/bin/bash +set -euo pipefail -cd web/oscar-viewer || exit +PROJECT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)" + +cd "$PROJECT_DIR/web/oscar-viewer" || exit 1 npm install npm run build -cd ../.. || exit +cd "$PROJECT_DIR" || exit 1 + +./gradlew build -x test -x osgi + +echo "Making shell scripts executable..." + +find "$PROJECT_DIR" -maxdepth 1 -type f -name "*.sh" -exec chmod +x {} \; -./gradlew build -x test -x osgi \ No newline at end of file +if [ -d "$PROJECT_DIR/osh-node-oscar" ]; then + find "$PROJECT_DIR/osh-node-oscar" -maxdepth 1 -type f -name "*.sh" -exec chmod +x {} \; +fi diff --git a/changelog.md b/changelog.md index 0be2957..43a6a42 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,29 @@ # OSCAR Build Node Change Log -All notable changes to this project will be documented in this file. + +All notable changes to this project will be documented in this file. + +## 3.5.1 2026-05-05 + +### Added +- Added profile-based launch sizing for packaged deployments. +- Added Linux and Windows monitoring workflows for first-run validation and field burn-in. +- Added one-file status report scripts for Linux and Windows. +- Added support for attach-or-restart monitor behavior through environment settings. +- Added MediaMTX deployment guidance for camera proxy use in larger test and field systems. + +### Changed +- Updated launch scripts to validate Java and Docker before startup. +- Updated launch scripts to detect an already-running OSCAR instance and fail fast unless `FORCE_RESTART=1` is set. +- Updated monitor scripts to attach to an existing OSCAR instance when `ATTACH_TO_EXISTING=1` is set. +- Updated PostGIS launch flow so an existing named container is replaced cleanly before startup settings are reapplied. +- Updated Windows launch and monitoring logic to use PowerShell and CIM-based process detection instead of WMIC. +- Updated release documentation for the prebuilt 3.5.1 unzip-and-run workflow. + +### Fixed +- Fixed PostgreSQL session over-allocation caused by oversized Hikari idle pooling. +- Fixed startup and monitoring friction on Windows around Java detection, trust store creation, and process discovery. +- Fixed release packaging/documentation drift around launch steps, cleanup expectations, and version naming. + ## 3.5.0 2026-04-24 ### Changes - Updated LaneSystem README @@ -163,8 +187,8 @@ This is the official first release of 3.0.0 ### Changed - Restructured repository, moving most directories that are unused in development under `dist` -## [2.3.0] -Release 2.3.0 +## [2.3.0] +Release 2.3.0 ### Added - Added Deployment version to config.json @@ -177,15 +201,13 @@ Removed dependency to log4j - [#90](https://github.com/Botts-Innovative-Research/osh-oakridge-buildnode/issues/90) Aspect Charts:The prior issue mentioned the Aspect RPMs and the Admin Panel, but this encompasses Aspects issues on the client as well. - [#]() -Update charts in client to display Rapiscan and Aspect charts +Update charts in client to display Rapiscan and Aspect charts - [#]() Node Form Fix: Updated NodeForm to check if node is reachable before adding it to the list of Nodes, so when configuring a node it will ensure that you can access that node before it continues processing and updating the UI. - ## [2.2] - 2025-07-30 Release 2.2 request, no updates since 1.3.7. - ## [1.3.7] 2025-07-18 ### Added diff --git a/dist/documentation/MediaMTX_OSCAR_camera_proxy_guide.md b/dist/documentation/MediaMTX_OSCAR_camera_proxy_guide.md new file mode 100644 index 0000000..e98fc48 --- /dev/null +++ b/dist/documentation/MediaMTX_OSCAR_camera_proxy_guide.md @@ -0,0 +1,389 @@ +# Using MediaMTX to Reduce OSCAR Camera Stream Load + +This guide explains how to use **MediaMTX** as a local RTSP proxy so OSCAR does not have to open large numbers of direct camera connections. + +For **testing and side-by-side field deployment**, the recommended operational flow is: + +1. prepare a fresh OSCAR 3.5.1 deployment +2. create `.env` +3. launch OSCAR with the monitoring script +4. proxy camera streams through MediaMTX +5. run the status-check script after warm-up +6. compare reconnect, thread, and database behavior with and without the proxy + +--- + +## Why MediaMTX helps + +Without a proxy, OSCAR may open many direct RTSP sessions to cameras. If you have many lanes, reconnect activity and repeated stream setup can create unnecessary load on the cameras and on the OSCAR host. + +MediaMTX sits between OSCAR and the cameras: + +- cameras stream to MediaMTX only when needed +- OSCAR connects to MediaMTX on the local machine instead of directly to every camera +- multiple lane definitions can reuse the same proxied path +- reconnects are handled against the local proxy instead of repeatedly hammering the physical cameras + +This is especially useful when: + +- many OSCAR lanes reuse a smaller number of real camera streams +- emulator lanes are used for testing or demonstrations +- direct camera sessions are expensive or unstable +- you want a simple local point to change, test, or swap stream sources + +--- + +## Typical architecture + +A common layout is: + +1. physical cameras publish RTSP streams +2. MediaMTX runs on the same machine as OSCAR or on a nearby local host +3. MediaMTX exposes local RTSP paths such as `/lane03_cam` +4. the OSCAR lane CSV points camera hosts to the local MediaMTX service +5. the SRLS emulator or detector-side service is addressed separately from the same CSV + +In this setup, OSCAR talks to: + +- the SRLS emulator or RPM service for detector data +- MediaMTX for camera video + +--- + +## Recommended OSCAR workflow when using MediaMTX + +### 1. Verify dependencies + +Make sure **Java 21+** and **Docker** are ready for OSCAR, and that MediaMTX is installed separately on the host where you plan to run the proxy. + +### 2. Start OSCAR with monitoring + +Linux: + +```bash +./monitor-oscar.sh +``` + +Windows: + +```bat +monitor-oscar.bat +``` + +### 3. Start MediaMTX + +Linux: + +```bash +./mediamtx mediamtx.yml +``` + +Windows PowerShell: + +```powershell +.\mediamtx.exe mediamtx.yml +``` + +### 4. Let the system warm up + +Allow enough time for lane startup, first client connections, and any reconnect behavior to appear. + +### 5. Generate a status report + +Linux: + +```bash +./check-oscar-status.sh +``` + +Windows: + +```powershell +powershell -ExecutionPolicy Bypass -File .\check-oscar-status.ps1 +``` + +Review: + +- thread count +- reconnect warnings +- camera-related log churn +- PostgreSQL session plateau behavior + +--- + +## Why the provided MediaMTX settings are efficient + +The configuration below keeps MediaMTX focused on lightweight RTSP proxying: + +- `rtmp: no` +- `hls: no` +- `webrtc: no` +- `srt: no` + +Disabling unused protocols avoids extra listeners and extra processing. + +These options are also important: + +- `sourceOnDemand: yes` means MediaMTX only pulls the upstream camera when a client requests the path +- `sourceProtocol: tcp` makes RTSP transport use TCP, which is often more reliable on LANs and across NAT or firewall boundaries +- `api: yes` with `apiAddress: :9997` gives you a simple status API for checking paths and clients + +--- + +## Example MediaMTX configuration for Axis cameras + +Use placeholders for credentials in documentation and shared files. + +```yaml +api: yes +apiAddress: :9997 +rtmp: no +hls: no +webrtc: no +srt: no +paths: + lane03_cam: + source: "rtsp://:@192.168.8.73/axis-media/media.amp?adjustablelivestream=1&resolution=640x480&videocodec=h264&videokeyframeinterval=15" + sourceOnDemand: yes + sourceProtocol: tcp + + lane04_cam: + source: "rtsp://:@192.168.8.229/axis-media/media.amp?adjustablelivestream=1&resolution=640x480&videocodec=h264&videokeyframeinterval=15" + sourceOnDemand: yes + sourceProtocol: tcp + + lane05_cam: + source: "rtsp://:@192.168.8.111/axis-media/media.amp?adjustablelivestream=1&resolution=640x480&videocodec=h264&videokeyframeinterval=15" + sourceOnDemand: yes + sourceProtocol: tcp + + lane06_cam: + source: "rtsp://:@192.168.8.167/axis-media/media.amp?adjustablelivestream=1&resolution=640x480&videocodec=h264&videokeyframeinterval=15" + sourceOnDemand: yes + sourceProtocol: tcp +``` + +--- + +## How this supports many lanes with fewer real cameras + +If your OSCAR deployment has 50 lanes with 2 cameras per lane, that is 100 camera references. Those 100 references do **not** need to be 100 unique physical camera connections. + +With MediaMTX, many lane rows can point back to the same proxied path, such as: + +- `rtsp://192.168.8.77:8554/lane04_cam` +- `rtsp://192.168.8.77:8554/lane06_cam` + +That means the CSV can model many lane-camera assignments while MediaMTX proxies only a small set of real upstream streams. + +--- + +## Example OSCAR service CSV entries + +Upload the CSV through the **Services** tab in the OSCAR admin page. + +```csv +Name,UniqueID,AutoStart,Latitude,Longitude,RPMConfigType,RPMHost,RPMPort,AspectAddressStart,AspectAddressEnd,EMLEnabled,EMLCollimated,LaneWidth,CameraType0,CameraHost0,CameraPath0,Codec0,Username0,Password0,CameraType1,CameraHost1,CameraPath1,Codec1,Username1,Password1 +sim-0,simu-0,FALSE,35.89,-84.19,Rapiscan,192.168.8.77,1601,,,FALSE,FALSE,4.820000172,Custom,192.168.8.77:8554,/lane04_cam,,,,Custom,192.168.8.77:8554,/lane06_cam,,, +sim-2,simu-1,FALSE,35.883,-84.19,Rapiscan,192.168.8.77,1602,,,FALSE,FALSE,4.820000172,Custom,192.168.8.77:8554,/lane06_cam,,,,Custom,192.168.8.77:8554,/lane04_cam,,, +``` + +### Important CSV fields + +- `RPMHost` and `RPMPort` point to the SRLS or Rapiscan emulator/service +- `CameraType0` and `CameraType1` are set to `Custom` so OSCAR uses the host/path directly +- `CameraHost0` and `CameraHost1` point to the machine running MediaMTX, usually `:8554` +- `CameraPath0` and `CameraPath1` point to the MediaMTX path, such as `/lane04_cam` +- `Username` and `Password` fields are left blank because authentication is handled upstream by MediaMTX when it pulls from the real camera + +--- + +## Sony camera settings to use + +For Sony cameras, use the following secondary-stream settings: + +- `H.264` +- `640x480` +- `15 fps` +- `1 s` keyframe interval when possible +- `high` H.264 profile +- `CBR` +- about `4000 kbps` + +These settings are a good match for lightweight proxying and testing because they keep the stream modest while still providing stable H.264 output. + +Example Sony-based MediaMTX path: + +```yaml +api: yes +apiAddress: :9997 +rtmp: no +hls: no +webrtc: no +srt: no +paths: + lane03_cam: + source: "rtsp://:@192.168.8.4:554/media/video2" + sourceOnDemand: yes + sourceProtocol: tcp +``` + +--- + +## Step-by-step setup + +### 1. Install MediaMTX + +Download MediaMTX for your platform and extract it onto the host that will run the proxy. + +Typical contents include: + +- `mediamtx` or `mediamtx.exe` +- `mediamtx.yml` + +### 2. Create `mediamtx.yml` + +Start with one of the examples above and add one path per real upstream camera stream. + +### 3. Start MediaMTX + +Linux: + +```bash +./mediamtx mediamtx.yml +``` + +Windows PowerShell: + +```powershell +.\mediamtx.exe mediamtx.yml +``` + +### 4. Verify MediaMTX is listening + +Linux: + +```bash +ss -ltnp | grep -E '8554|9997' +``` + +Windows PowerShell: + +```powershell +Get-NetTCPConnection -LocalPort 8554,9997 -State Listen +``` + +### 5. Verify the API + +Linux or macOS: + +```bash +curl http://127.0.0.1:9997/v3/paths/list +``` + +Windows PowerShell: + +```powershell +Invoke-RestMethod http://127.0.0.1:9997/v3/paths/list +``` + +### 6. Update the OSCAR CSV + +Point lane camera hosts and paths to MediaMTX instead of the physical cameras. + +### 7. Upload the CSV in OSCAR + +In the OSCAR admin UI: + +1. open the **Services** tab +2. upload the CSV +3. confirm the lanes import successfully +4. start the desired lanes or services + +--- + +## Best practices + +### Keep streams modest + +Use modest stream settings for test environments: + +- H.264 +- 640x480 +- 15 fps +- CBR +- 1-second keyframe interval when possible + +### Use `sourceOnDemand` + +This avoids pulling from upstream cameras when no OSCAR consumer is actually using the stream. + +### Use local proxy paths in OSCAR + +Do not point every lane directly at physical cameras if the same few streams can be reused. + +### Keep protocols disabled unless needed + +If you only need RTSP, keep HLS, WebRTC, RTMP, and SRT disabled. + +### Use monitoring during evaluation + +When comparing direct-camera versus proxied-camera behavior, always launch OSCAR with the monitoring script and collect a status report after warm-up. + +--- + +## Troubleshooting + +### OSCAR cannot open the stream + +Check: + +- MediaMTX is running +- the lane CSV points to the correct host and path +- port `8554` is open locally +- the MediaMTX path name matches exactly + +Direct test example: + +```bash +ffplay rtsp://127.0.0.1:8554/lane03_cam +``` + +### MediaMTX path exists but does not pull video + +Check: + +- upstream camera IP and credentials +- the source URL path +- whether the camera is configured for the requested stream format +- whether the camera accepts TCP RTSP transport + +### Too many reconnects + +MediaMTX can localize reconnect activity, but also check: + +- camera network stability +- camera encoder settings +- duplicate or unstable stream consumers +- bad payloads or emulator-side issues +- OSCAR thread and reconnect logs from the monitor directory + +### API works but paths do not appear active + +With `sourceOnDemand: yes`, a path may stay idle until a client requests it. That is normal. + +--- + +## Summary + +MediaMTX reduces the burden of large camera configurations by turning many direct camera connections into a smaller number of local RTSP proxy paths. + +In practice, that means: + +- less direct pressure on physical cameras +- fewer repeated direct sessions from OSCAR +- easier CSV-based lane provisioning +- easier testing with emulator lanes +- simpler troubleshooting and stream replacement + +For field evaluation, pair MediaMTX with the OSCAR monitoring and status-check scripts so you can compare reconnect behavior, thread growth, and system steadiness under realistic load. diff --git a/dist/documentation/Node_Administration_3.5.1_addendum.md b/dist/documentation/Node_Administration_3.5.1_addendum.md new file mode 100644 index 0000000..863905b --- /dev/null +++ b/dist/documentation/Node_Administration_3.5.1_addendum.md @@ -0,0 +1,25 @@ +# Node Administration 3.5.1 addendum + +The existing **Node Administration** PDF remains useful for Admin UI tasks such as: + +- starting and stopping modules +- adding users and roles +- configuring sensors, storage, and SOS services + +No major Admin UI workflow changes were required for the 3.5.1 launch, monitoring, and packaging updates. + +The operational changes for 3.5.1 are outside that PDF and are now covered in these updated deployment documents: + +- `README.updated.md` +- `OSCAR_launch_monitoring_guide.updated.md` +- `MediaMTX_OSCAR_camera_proxy_guide.updated.md` +- `OSCAR_System_Documentation_Manual_3.5.updated.md` + +Use the PDF for Admin Panel behavior, and use the updated deployment documents for: + +- Java 21 and Docker prerequisites +- `.env` setup +- already-running OSCAR handling +- monitoring and status scripts +- fresh-install cleanup of older OSCAR releases +- MediaMTX-assisted camera deployment guidance diff --git a/dist/documentation/OSCAR_System_Documentation_Manual_3.5.md b/dist/documentation/OSCAR_System_Documentation_Manual_3.5.md index aff79d1..74043be 100644 --- a/dist/documentation/OSCAR_System_Documentation_Manual_3.5.md +++ b/dist/documentation/OSCAR_System_Documentation_Manual_3.5.md @@ -16,8 +16,8 @@ | **Field** | **Value** | |-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | Document type | Technical reference and operations guide | -| Baseline | OSCAR v3.3.2 / 3.5 pause-point feature line | -| Version context | Centered on v3.3.2, the feature-complete build expected to become the 3.5 pause point after an internal test cycle | +| Baseline | OSCAR v3.5.1 packaged deployment guidance with legacy architectural background | +| Version context | Updated with OSCAR 3.5.1 deployment, monitoring, and prebuilt-release operational guidance while retaining broader architecture and feature context | | Audience | Deployment engineers, operators, testers, and maintainers | | Scope | Administrative configuration, viewer/PWA, database, file handling, Web ID integration, offline conversion, reporting, scaling, and upgrade guidance | @@ -53,6 +53,22 @@ Operationally, OSCAR supports two practical deployment patterns: a default singl | Evidence model | Events can combine spectra, N42 files, uploaded evidence, QR code data, videos, Web ID results, adjudication history, and generated reports. | | Major integrations | Rapiscan, Aspect, and RSI lanes, FFmpeg-recognized camera streams, Web ID, Cambio, MQTT, and a role-aware file API. | + +## 3.5.1 packaged release note + +This manual now includes **OSCAR 3.5.1** packaged deployment guidance for the current prebuilt release workflow. + +The most important operational changes in this release line are: + +- Java 21 and Docker are required prerequisites for packaged launchers +- the launchers now validate dependencies before startup +- the top-level launch and monitor scripts detect already-running OSCAR instances +- the monitor scripts can either attach to or replace an already-running OSCAR process +- profile-based Java and PostgreSQL sizing is now part of the standard deployment flow +- MediaMTX is recommended for larger test or field deployments that reuse a smaller number of real camera streams + +For first-run field validation, the preferred workflow is to start with the monitoring wrapper and then generate a status report after the system reaches steady state. + # 2. System architecture OSCAR is best understood as a node-centric application host that serves both configuration and operator workflows while coordinating external devices and services. Lanes are the operational units. Each lane can have detector inputs, camera feeds, event history, and associated evidence. Events and statistics are persisted in PostgreSQL, while files such as videos, reports, site diagrams, CSV imports, and adjudication artifacts remain on the application host file system. @@ -86,32 +102,40 @@ The diagram below condenses the system relationships. It is not a source-code cl # 3. Installation and initial startup -Installation is intentionally simple: download a release archive, extract it, ensure Docker is installed and running, and launch the platform with the OS-specific launch-all script (`launch-all.bat` for Windows, `launch-all.sh` for Linux/macOS, or `launch-all-arm.sh` for ARM systems). The database and application come up together in the default deployment path. +Installation for the current packaged release is intentionally simple: download the **OSCAR 3.5.1** release archive, extract it into a fresh directory, verify **Java 21+** and **Docker**, create `.env`, and start the system with the OS-specific top-level launcher or monitoring wrapper. The database and application come up together in the default deployment path. + +For test and side-by-side field deployment, the preferred first start is the monitoring wrapper rather than a direct node launch. ## Prerequisites | **Item** | **Why it matters** | **Notes** | |---------------------------------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------| -| Supported host OS | The release package contains OS-specific launch scripts. | Windows has the strongest support, although the packaging also includes scripts for other operating systems. | -| Docker | Required for the default local PostgreSQL deployment. | PostgreSQL replaces the older embedded H2 option in the current release line. | +| Supported host OS | The release package contains OS-specific launch scripts. | Windows and Linux are the primary packaged deployment paths discussed in the 3.5.1 workflow. | +| Java 21 or newer | Required by the packaged launch and monitoring scripts. | The launchers validate the Java version at startup and fail fast if Java is missing or too old. | +| Docker | Required for the default local PostgreSQL deployment. | Docker must already be running before `launch-all` or `monitor-oscar` is started. | | Browser with PWA support | Needed for installed desktop/mobile viewer experiences. | Chrome-like browsers support the install flow; notifications depend on secure hosting. | | Optional SSL keystore | Needed to host the application over HTTPS. | The HTTPS configuration expects a keystore path, password, alias, and selected port. | | Optional remote PostgreSQL host | Useful for larger sites or more robust infrastructure. | Remote database connection parameters can be entered in the admin configuration. | | Optional Web ID endpoint | Enables automated isotope analysis for evidence and RS350 data. | A remote default root can be used, but the root URL is configurable for local or offline hosting. | +| Optional MediaMTX host | Recommended for larger camera-heavy systems. | MediaMTX can proxy a smaller set of upstream camera streams to many logical lane assignments. | ## Startup sequence -> 1\. Download the desired release from the repository releases page and extract it to a working directory. +> 1\. Download the desired release from the repository releases page and extract it to a **fresh working directory**. If an older extracted OSCAR release is already present on the same host, stop the old OSCAR JVM, remove the old PostGIS container and any old OSCAR-specific Docker network, and delete the old extracted folder before proceeding. > -> 2\. Install Docker and verify that the Docker service is running before starting OSCAR. +> 2\. Verify **Java 21 or newer** and **Docker** before starting OSCAR. > -> 3\. Run the launch-all script (`launch-all.bat`, `launch-all.sh`, or `launch-all-arm.sh`) for the operating system in use. In the default path, the script starts PostgreSQL locally in Docker and then starts the Java application. +> 3\. Create `.env`. In packaged releases this may mean renaming `env.txt` to `.env`. In source-style layouts this means copying `env.template` to `.env`. > -> 4\. Open the application on the configured port. Port 8282 is the baseline HTTP application port, and 8443 is a representative HTTPS configuration. +> 4\. For test and side-by-side field deployment, start with the monitoring wrapper (`monitor-oscar.bat` or `monitor-oscar.sh`). For routine startup without monitoring, use the top-level `launch-all` script. Avoid direct use of the node-level launch script unless you are debugging. > -> 5\. Sign in with the initial admin account. The initial password is configurable before launch, but the exact shipped username and password should be verified against the release README or package contents. +> 5\. The launchers now validate dependencies, detect already-running OSCAR instances, and either refuse to proceed or replace the running instance depending on `FORCE_RESTART`. The monitor wrappers can also attach to an already-running OSCAR process when `ATTACH_TO_EXISTING=1` is set. > -> 6\. Before production use, change the initial admin password using the dot-prefixed settings or environment file and run the set-initial-admin-password script so the password is written in the hashed form expected by the system. +> 6\. Open the application on the configured port. Port 8282 is the baseline HTTP application port, and 8443 is a representative HTTPS configuration. +> +> 7\. Sign in with the initial admin account. The initial username is typically `admin`, but the exact packaged password should be verified from the release package or release notes rather than assuming a universal default. +> +> 8\. Before production use, change the initial admin password and then generate a first-run status report with `check-oscar-status` after the system reaches steady state. @@ -120,7 +144,7 @@ Installation is intentionally simple: download a release archive, extract it, en +

If multiple extracted versions exist on the same machine, explicitly stop the old OSCAR JVM and remove the old `oscar-postgis-container` before launching the new package. Reusing an old container or old extracted directory can populate data in the wrong place and create confusion during configuration or tuning.

@@ -380,6 +404,18 @@ Configuration of video retention lives in the “OSCAR Service Module.” Configuration of additional users / permissions exists under the “Security” tab. Users can be configured with fine-grained permissions to access / write to the system. + +## 3.5.1 deployment operations guidance + +For packaged 3.5.1 deployments, the most practical first-run validation sequence is: + +1. launch with the monitoring wrapper +2. let the system warm up long enough to reach a steady operating state +3. generate the one-file status report +4. confirm that Java memory, thread count, and PostgreSQL session counts plateau rather than climb continuously + +This deployment workflow is especially important when evaluating reconnect churn, thread growth, or database session growth in larger simulated or camera-heavy systems. + # 9. Performance, scale, and deployment guidance Default deployment begins to strain as lane counts, event volume, and camera counts increase. The following guidance is useful for planning, even though it is not a formal support limit. @@ -483,3 +519,25 @@ This section captures known gaps and enhancement ideas so they are not confused | PWA | Progressive web app. The installable version of the viewer for desktop or mobile use. | | RS350 | A supported backpack-style mobile detector lane type. | + +# Appendix C. Packaged 3.5.1 field-deployment checklist + +> 1\. Verify Java 21+ and Docker. +> +> 2\. If an older OSCAR release was previously used on the host, stop the old OSCAR JVM, remove the old PostGIS container and old OSCAR-specific Docker network, and delete the old extracted release folder. +> +> 3\. Extract the 3.5.1 release into a fresh directory. +> +> 4\. Create `.env` from the packaged environment file and confirm the correct `SYSTEM_PROFILE`. +> +> 5\. Prefer the monitoring wrapper for first-run validation. +> +> 6\. If OSCAR is already running, decide whether to stop it, set `FORCE_RESTART=1`, or use `ATTACH_TO_EXISTING=1` for the monitor wrapper. +> +> 7\. If the deployment uses many camera references, put MediaMTX in front of the cameras and point the OSCAR lane CSV to MediaMTX paths instead of directly to every physical camera. +> +> 8\. After warm-up, generate the one-file status report and confirm memory, threads, and PostgreSQL sessions have stabilized. +> +> 9\. Change the initial admin password before production use. +> +> 10\. Keep the launch/monitoring guide and MediaMTX guide with the release package so operators follow the same tested startup path. diff --git a/dist/documentation/OSCAR_launch_monitoring_guide.md b/dist/documentation/OSCAR_launch_monitoring_guide.md new file mode 100644 index 0000000..b799b2f --- /dev/null +++ b/dist/documentation/OSCAR_launch_monitoring_guide.md @@ -0,0 +1,511 @@ +# OSCAR launch, monitoring, and database diagnostics guide + +This guide covers the current **OSCAR 3.5.1 packaged deployment workflow** for Linux and Windows. + +It explains: + +- how to prepare a fresh prebuilt release +- how to create `.env` +- how the updated `launch-all`, `launch`, `monitor-oscar`, and `check-oscar-status` scripts behave +- how already-running OSCAR instances are handled +- how to validate Java, Docker, memory, and database health +- how to use MediaMTX and status reports during testing and side-by-side field deployment + +--- + +## 1. Recommended operating model + +For **testing, burn-in, and side-by-side field deployment**, the preferred workflow is: + +1. unzip the prebuilt release into a fresh folder +2. create `.env` +3. verify **Java 21+** and **Docker** +4. start with the **monitoring script** +5. let the system warm up +6. run the **status-check script** +7. review JVM, thread, and PostgreSQL behavior before wider deployment + +Use the top-level **sessionless launchers** when possible: + +- `launch-all.sh` / `launch-all.bat` +- `monitor-oscar.sh` / `monitor-oscar.bat` + +Avoid launching `osh-node-oscar/launch.(sh|bat)` directly unless you are debugging the node itself. + +--- + +## 2. Fresh install and upgrade cleanup + +If the machine has previously run OSCAR, clean up the old deployment before extracting **OSCAR 3.5.1**. + +### Linux + +```bash +pgrep -af 'com.botts.impl.security.SensorHubWrapper' +kill + +docker rm -f oscar-postgis-container +docker network rm oscar-postgis-network || true +rm -rf /path/to/old/oscar-3.5.0 +``` + +### Windows PowerShell + +```powershell +Get-CimInstance Win32_Process | + Where-Object { + $_.Name -match '^java(\.exe)?$' -and + $_.CommandLine -like '*com.botts.impl.security.SensorHubWrapper*' + } | + Select-Object ProcessId, CommandLine + +Stop-Process -Id -Force + +docker rm -f oscar-postgis-container +docker network rm oscar-postgis-network +Remove-Item -Recurse -Force .\oscar-3.5.0 +``` + +If the Docker network does not exist, that is fine. The goal is to avoid carrying old container state or an old extracted release folder into the new test run. + +--- + +## 3. Required dependencies + +### Linux + +Required: + +- Bash +- Java 21 or newer +- `keytool` +- Docker + +Recommended: + +- `jcmd` +- `pmap` +- `free` +- `vmstat` + +Ubuntu example: + +```bash +sudo apt update +sudo apt install openjdk-21-jdk docker.io procps psmisc +``` + +Verify: + +```bash +java -version +which keytool +which docker +which jcmd +``` + +### Windows + +Required: + +- PowerShell +- Java 21 or newer +- Docker Desktop or Docker Engine + +Recommended: + +- `jcmd.exe` + +Verify: + +```powershell +java -version +docker version +Get-Command java +Get-Command docker +Get-Command jcmd +``` + +The Windows launchers now use **PowerShell/CIM**-based process discovery. They do not depend on `wmic`. + +--- + +## 4. Environment file setup + +Create `.env` before launch. + +- if the packaged release ships **env.txt**, rename it to **.env** +- if the source tree ships **env.template**, copy it to **.env** + +Linux: + +```bash +cp env.template .env +``` + +Windows PowerShell: + +```powershell +Copy-Item .\env.template .\.env +``` + +### Core settings + +```dotenv +SYSTEM_PROFILE=16GB +DB_NAME=gis +DB_USER=postgres +DB_PASSWORD=postgres +DB_PORT=5432 +DB_HOST=localhost +CONTAINER_NAME=oscar-postgis-container +``` + +### Process and monitor behavior settings + +```dotenv +FORCE_RESTART=0 +ATTACH_TO_EXISTING=0 +MAX_WAIT_SECONDS=300 +RETRY_MAX=120 +RETRY_INTERVAL=2 +POSTGIS_READY_DELAY=5 +``` + +### What these mean + +- `FORCE_RESTART=0` -> refuse to start if OSCAR is already running +- `FORCE_RESTART=1` -> stop the running OSCAR instance and start fresh +- `ATTACH_TO_EXISTING=0` -> monitor script refuses to attach to a running OSCAR process +- `ATTACH_TO_EXISTING=1` -> monitor script attaches to the running OSCAR process instead of replacing it +- `MAX_WAIT_SECONDS` -> how long monitor scripts wait for the Java process to appear +- `RETRY_MAX`, `RETRY_INTERVAL`, `POSTGIS_READY_DELAY` -> PostGIS readiness timing + +--- + +## 5. Profile-based sizing + +The launchers size Java and PostgreSQL by `SYSTEM_PROFILE`. + +Supported profiles: + +- `RPI4` +- `8GB` +- `16GB` +- `32GB` + +Representative PostgreSQL `max_connections` values: + +- `RPI4` -> 75 +- `8GB` -> 125 +- `16GB` -> 200 +- `32GB` -> 300 + +The launchers also set: + +- `superuser_reserved_connections=10` +- `idle_session_timeout=600000` +- connection and disconnection logging + +--- + +## 6. Launch scripts and what they do + +### `launch-all.sh` / `launch-all.bat` + +These are the supported top-level launchers. + +They: + +- load `.env` +- validate Java and Docker +- detect an already-running OSCAR instance +- size PostgreSQL for the selected profile +- rebuild or reuse the PostGIS image +- remove the existing named PostGIS container if necessary +- start a new PostGIS container with the current settings +- wait for PostgreSQL readiness +- call `osh-node-oscar/launch.(sh|bat)` + +### `osh-node-oscar/launch.sh` / `osh-node-oscar/launch.bat` + +These launch only the OSCAR Java node. + +They: + +- load `.env` +- validate Java and keytool +- detect an already-running OSCAR instance +- choose heap and JavaCPP settings for the selected profile +- build or refresh the Java trust store +- initialize the packaged admin password flow +- start the Java process with Native Memory Tracking enabled + +Use these direct node launchers mainly for debugging. + +--- + +## 7. How already-running OSCAR instances are handled + +### Launch behavior + +By default, if an OSCAR JVM is already running, `launch-all` and `launch` stop and print an error instead of silently starting another instance. + +Typical message: + +```text +OSCAR is already running with PID(s): ... +Stop the running instance first, or set FORCE_RESTART=1 to replace it. +``` + +### Force replacement + +Set: + +```dotenv +FORCE_RESTART=1 +``` + +Then the scripts attempt to stop the running OSCAR process before starting a new one. + +### Monitor attach behavior + +For monitor wrappers, you have two supported choices: + +- `FORCE_RESTART=1` -> replace the running OSCAR instance and monitor the new one +- `ATTACH_TO_EXISTING=1` -> keep the running OSCAR instance and attach monitoring to it + +--- + +## 8. Starting OSCAR + +### Recommended first-run start with monitoring + +#### Linux + +```bash +chmod +x launch-all.sh osh-node-oscar/launch.sh monitor-oscar.sh check-oscar-status.sh +./monitor-oscar.sh +``` + +#### Windows + +```bat +monitor-oscar.bat +``` + +This creates an output directory such as: + +```text +oscar-monitor-20260505-032622 +``` + +and captures: + +- launch stdout and stderr +- JVM PID information +- JFR status +- GC heap information +- native memory summaries +- thread dumps +- Docker status +- PostgreSQL session and activity data +- trend CSV files for database sessions + +### Routine start without monitoring + +#### Linux + +```bash +./launch-all.sh +``` + +#### Windows + +```bat +launch-all.bat +``` + +--- + +## 9. Stopping OSCAR + +### Linux + +If you started with `monitor-oscar.sh`, stop the monitor shell or the Java PID it discovered. + +To stop a running OSCAR JVM manually: + +```bash +pgrep -af 'com.botts.impl.security.SensorHubWrapper' +kill +``` + +To stop PostGIS: + +```bash +docker stop oscar-postgis-container +``` + +### Windows PowerShell + +```powershell +Get-CimInstance Win32_Process | + Where-Object { + $_.Name -match '^java(\.exe)?$' -and + $_.CommandLine -like '*com.botts.impl.security.SensorHubWrapper*' + } | + Select-Object ProcessId, CommandLine + +Stop-Process -Id -Force +docker stop oscar-postgis-container +``` + +The Windows monitor script also supports: + +```bat +monitor-oscar.bat stop +``` + +--- + +## 10. Status reports + +### Linux + +```bash +./check-oscar-status.sh +``` + +### Windows + +```powershell +powershell -ExecutionPolicy Bypass -File .\check-oscar-status.ps1 +``` + +These scripts summarize the latest monitor run into a single text report that includes: + +- process status +- live JVM information +- heap and native memory summaries +- current container status +- PostgreSQL activity snapshots +- first-versus-latest trend comparison +- recent log tails + +--- + +## 11. What healthy startup looks like + +A healthy first-run profile typically looks like this: + +- Java RSS rises during startup and then levels out +- thread count rises during startup and then stabilizes +- PostgreSQL sessions rise during startup and then plateau well below usable client slots +- swap or pagefile usage stays low +- `db-error` remains empty + +### Important PostgreSQL rule + +```text +usable client slots = max_connections - superuser_reserved_connections +``` + +If total sessions keep climbing toward that number, PostgreSQL is nearing saturation. + +--- + +## 12. Interpreting the monitor output + +Key files in a monitor directory: + +- `launch.stdout.log` +- `launch.stderr.log` +- `jvm-pid.txt` +- `db-connection-trend.csv` +- one timestamped snapshot directory per interval + +Key per-snapshot files: + +- `nmt-summary.txt` +- `gc-heap-info.txt` +- `thread-print.txt` +- `db-max-connections.txt` +- `db-total-sessions.txt` +- `db-by-state.txt` +- `db-by-app.txt` +- `db-activity-detail.txt` +- `docker-logs-tail.txt` + +Use `db-connection-trend.csv` as the fastest way to spot connection growth, plateauing, or saturation. + +--- + +## 13. MediaMTX during field testing + +For larger camera configurations or side-by-side test deployments: + +1. start OSCAR with `monitor-oscar` +2. route camera streams through MediaMTX +3. let the system run long enough to capture reconnect and thread behavior +4. run `check-oscar-status` +5. compare JVM threads, reconnect logs, and PostgreSQL sessions before and after enabling MediaMTX + +MediaMTX is especially helpful when many logical lane-camera assignments reuse a smaller number of real camera streams. + +--- + +## 14. Troubleshooting checklist + +### Launch fails before Java starts + +Check: + +- `.env` exists +- Java 21+ is installed +- Docker is running +- required directories such as `osh-node-oscar/lib` and `osh-node-oscar/nativelibs` exist +- the trust store and keystore files exist where the launch script expects them + +### Monitor hangs waiting for Java + +Check `launch.stdout.log` and `launch.stderr.log` inside the newest monitor directory. + +Common causes: + +- PostGIS container startup failed +- the OSCAR Java process exited immediately after launch +- a required runtime path is missing +- a certificate, trust store, or password-initialization step failed + +### PostgreSQL sessions keep climbing + +Inspect: + +- `db-total-sessions.txt` +- `db-by-state.txt` +- `db-by-app.txt` +- `db-activity-detail.txt` +- `db-connection-trend.csv` + +### Thread count keeps climbing + +Inspect: + +- `thread-print.txt` +- reconnect-related warnings in `launch.stdout.log` +- MediaMTX versus direct-camera behavior under the same test workload + +--- + +## 15. Files you normally should not delete + +Do not delete these unless you intentionally want to reset state: + +- `.env` +- `osh-node-oscar/db/` +- `pgdata/` +- `osh-node-oscar/osh-keystore.p12` +- `osh-node-oscar/truststore.jks` + +It is safe to delete old monitor directories and generated status reports once you have kept the reports you need. diff --git a/dist/documentation/Release_Notes_3.5.1.md b/dist/documentation/Release_Notes_3.5.1.md new file mode 100644 index 0000000..2d6f9dc --- /dev/null +++ b/dist/documentation/Release_Notes_3.5.1.md @@ -0,0 +1,533 @@ +# Release Notes + +## Overview + +OSCAR **3.5.1** improves deployment stability, observability, and scalability for larger multi-lane systems. This release focuses on: + +* reducing memory pressure +* preventing PostgreSQL connection exhaustion +* improving runtime diagnostics +* simplifying deployment on Linux and Windows +* improving support for MediaMTX-based camera proxy deployments +* making launch and monitoring behavior safer when OSCAR is already running +* improving first-run dependency and startup validation + +These changes were validated against a high-load configuration monitoring **50 radiation portal monitors and 100 camera streams**. + +This is a **prebuilt release**. Users should **unzip OSCAR 3.5.1 into a fresh directory** and start it with the included **monitoring script**, preferably using the **sessionless launch** when possible. + +--- + +## Before you start + +### Required dependencies + +Install these before running OSCAR 3.5.1: + +* **OpenJDK 21** +* **Docker** + +### Recommended deployment model + +For testing, side-by-side field deployment, and first-run validation: + +* unzip the release into a **new clean folder** +* rename `env.txt` to `.env` if needed +* select the correct system profile in `.env` +* use **MediaMTX** for camera-heavy deployments +* start OSCAR with the **sessionless monitoring launch** when possible +* use the **check/status script** to review performance + +--- + +## What is new + +### Profile-based system sizing + +Deployment now supports profile-based resource tuning instead of using one fixed memory configuration for every machine. + +Supported profiles: + +* `RPI4` +* `8GB` +* `16GB` +* `32GB` + +These profiles allow the JVM and PostgreSQL configuration to be matched to the host hardware through the `.env` file and updated launch scripts. + +### Updated launch flow + +Launch scripts were updated for both Linux and Windows so they can: + +* load the selected system profile +* size Java heap appropriately for the machine +* size PostgreSQL more appropriately for the machine +* start the PostGIS container with tuned settings +* provide a more consistent startup path across environments +* check for required dependencies before launch +* stop or refuse duplicate OSCAR launches based on script settings + +### Safer process handling + +The launch and monitoring scripts now better handle cases where OSCAR is already running. + +Improvements include: + +* detection of already running OSCAR processes +* clearer behavior when a prior instance is found +* support for stopping and relaunching cleanly when configured to do so +* monitor behavior aligned with launch behavior end to end +* reduced risk of duplicate Java processes and conflicting monitor sessions + +This makes startup behavior safer during testing, upgrades, and repeated field launches. + +### Dependency and environment validation + +Deployment scripts now better validate startup prerequisites and packaged paths before launch. + +Improvements include: + +* dependency checks for **Java 21** and **Docker** +* clearer startup errors when required tools are missing +* improved trust store handling on Windows +* better validation of expected runtime directories and packaged files +* updated environment template support for launch and monitor behavior + +These changes make prebuilt deployment more reliable, especially on fresh Windows systems. + +### PostgreSQL tuning improvements + +PostgreSQL startup settings were updated to better support larger deployments. + +Improvements include: + +* increased connection limits by profile +* reduced per-connection memory pressure +* reserved superuser or admin connection slots +* idle session timeout support +* connection and disconnection logging for diagnostics + +For the 16 GB profile, PostgreSQL was raised from the earlier 100-connection ceiling to a higher-capacity configuration, resolving the immediate `too many clients already` failure mode during large-scale operation. + +### Hikari connection pool fix + +The main cause of database session over-allocation was identified and corrected in the PostGIS datastore connection manager. + +#### Root cause + +* each Hikari pool was configured with `maximumPoolSize(20)` +* `minimumIdle` was not set +* Hikari therefore defaulted `minimumIdle` to the same value as `maximumPoolSize` +* with multiple pools active, the system held a very large number of idle PostgreSQL sessions open at all times + +#### Fix + +* reduced per-pool size +* explicitly set `minimumIdle(0)` +* shortened idle timeout behavior +* preserved sufficient active connection capacity while eliminating unnecessary idle connection hoarding + +#### Result observed in testing + +* PostgreSQL steady-state sessions dropped from about **186** to about **21** +* idle JDBC sessions dropped from about **180** to about **15** +* database headroom increased substantially +* the immediate Postgres connection saturation problem was eliminated + +This is the most important backend stability improvement in this release. + +### Monitoring and status scripts + +New monitoring and status-check scripts were added for both Linux and Windows. + +These scripts can now: + +* launch OSCAR under monitoring +* support sessionless launch for normal deployment use +* capture JVM memory status +* capture native memory tracking summaries +* capture JFR status +* capture OS memory and swap usage +* capture PostgreSQL session counts and saturation state +* capture database activity detail +* produce a single-file health and status report for rapid review + +### Improved database diagnostics + +Monitoring now includes PostgreSQL visibility such as: + +* `max_connections` +* `superuser_reserved_connections` +* total active sessions +* session state counts +* connection trend logging over time +* recent PostgreSQL log activity + +This makes it much easier to distinguish between: + +* memory pressure +* connection pool over-allocation +* true connection leaks +* normal steady-state pool behavior + +### MediaMTX deployment guidance + +Documentation was added for using **MediaMTX** as a local RTSP proxy layer to reduce the resource burden of handling many camera streams directly in OSCAR. + +This supports a deployment model where: + +* a smaller number of upstream camera streams are proxied locally +* multiple lanes can reuse proxied feeds +* OSCAR connects to stable local endpoints instead of managing a large number of direct camera connections + +This architecture is recommended for larger systems and appears to reduce camera-related reconnect burden. + +### Documentation updates + +Documentation was added or expanded for: + +* `.env` usage +* launch scripts +* monitoring scripts +* check or status scripts +* profile selection +* dependency installation and verification +* startup and shutdown procedures +* data interpretation and troubleshooting +* MediaMTX camera proxy setup +* already-running instance handling +* environment template settings for restart and attach behavior + +--- + +## Problems addressed + +### Memory pressure from fixed JVM sizing + +Previous deployments used a one-size-fits-all Java memory model. On smaller or moderately sized machines, this could reserve too much memory for the JVM and reduce operating system and PostgreSQL headroom, increasing swap or pagefile pressure. + +### PostgreSQL connection exhaustion + +Large deployments were exhausting PostgreSQL connection capacity because multiple Hikari pools were keeping too many idle connections open. This caused: + +* `too many clients already` +* Hikari connection timeouts +* database degradation under load + +### Duplicate launch and monitoring confusion + +Repeated test starts could leave users uncertain whether OSCAR was already running, whether a second Java process had been created, or whether the monitor had attached to the correct instance. + +The updated scripts address this by making existing-instance behavior more explicit and consistent. + +### Limited visibility into failure mode + +Earlier logs were often noisy or incomplete during failures. The new monitoring and status scripts make it easier to determine whether the bottleneck is: + +* Java heap +* native memory +* swap usage +* PostgreSQL session pressure +* query activity +* startup or reconnect churn + +--- + +## Behavior observed after the fixes + +After applying the connection pool fix and updated deployment tuning: + +* PostgreSQL sessions dropped from about **186** to about **21** in testing +* idle JDBC sessions dropped from about **180** to about **15** +* JVM swap usage dropped to **0** in the improved test run +* the system remained stable during startup and warm-up +* database headroom improved dramatically + +--- + +## If you previously ran OSCAR + +If this machine was already running an older OSCAR release, do **not** install OSCAR 3.5.1 over the top of the old directory. + +Before starting OSCAR 3.5.1, stop and remove the older deployment components: + +* stop the old PostGIS container +* remove the old PostGIS container +* remove the old Docker network used by the previous OSCAR deployment, **if one exists** +* stop any old OSCAR Java process that is still running +* delete the old `oscar-3.5.0` directory +* unzip OSCAR **3.5.1** into a fresh folder + +### Linux cleanup example + +Stop and remove the old PostGIS container: + +```bash +docker stop oscar-postgis-container 2>/dev/null || true +docker rm oscar-postgis-container 2>/dev/null || true +``` + +If the previous deployment created a dedicated Docker network, remove it after the container is gone: + +```bash +docker network ls +docker network rm +``` + +Stop any running OSCAR Java process if needed: + +```bash +pkill -f 'com.botts.impl.security.SensorHubWrapper' || true +``` + +Remove the previous OSCAR folder: + +```bash +rm -rf ~/oscar-3.5.0 +``` + +### Windows cleanup example + +Stop and remove the old PostGIS container: + +```powershell +docker stop oscar-postgis-container +docker rm oscar-postgis-container +``` + +If the previous deployment created a dedicated Docker network, remove it after the container is gone: + +```powershell +docker network ls +docker network rm +``` + +Stop any running OSCAR Java process if needed: + +```powershell +Get-CimInstance Win32_Process | + Where-Object { + $_.Name -match '^java(\.exe)?$' -and + $_.CommandLine -like '*com.botts.impl.security.SensorHubWrapper*' + } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force } +``` + +Delete the previous OSCAR folder: + +```powershell +Remove-Item -Recurse -Force .\oscar-3.5.0 +``` + +If you are unsure whether a dedicated OSCAR Docker network exists, list networks first and remove only the one associated with the old OSCAR deployment. + +--- + +## Fresh install workflow for OSCAR 3.5.1 + +### Step 1: unzip the release + +Extract OSCAR **3.5.1** into a new folder. + +Example: + +```text +oscar-3.5.1/ +``` + +### Step 2: confirm dependencies + +Make sure the machine has: + +* **OpenJDK 21** +* **Docker** + +### Step 3: configure the environment file + +The release may include the environment file as: + +```text +env.txt +``` + +Rename it to: + +```text +.env +``` + +Then edit the file and select the correct hardware profile: + +* `RPI4` +* `8GB` +* `16GB` +* `32GB` + +The environment template also supports launch and monitoring behavior such as restart and attach settings. + +### Step 4: start with the monitoring script + +For OSCAR 3.5.1, users should launch with the monitoring script so diagnostics begin immediately. + +Use the **sessionless launch** when possible so OSCAR keeps running without requiring an attached terminal session. + +#### Linux + +Preferred: + +```bash +./monitor-oscar.sh --daemon +``` + +If your script version starts sessionless by default, use: + +```bash +./monitor-oscar.sh +``` + +Use an attached launch only for interactive troubleshooting. + +#### Windows + +Preferred: + +```bat +monitor-oscar.bat +``` + +Use the sessionless option if your Windows wrapper provides both attached and detached modes. + +Use an attached launch only for interactive troubleshooting. + +### Step 5: check performance with the included status script + +After startup, and again after the system has been running for a while, generate a status report. + +#### Linux + +```bash +./check-oscar-status.sh +``` + +#### Windows + +```powershell +powershell -ExecutionPolicy Bypass -File .\check-oscar-status.ps1 +``` + +This report helps verify that memory, swap, and PostgreSQL usage remain healthy. + +--- + +## Recommended field test workflow + +For testing and side-by-side field deployment, users should: + +1. stop and remove any old OSCAR PostGIS container +2. remove the old OSCAR Docker network **if one exists** +3. stop any old OSCAR Java process if one is still running +4. delete the old `oscar-3.5.0` folder +5. unzip OSCAR **3.5.1** into a fresh folder +6. install or verify **OpenJDK 21** and **Docker** +7. rename `env.txt` to `.env` if needed +8. select the correct profile in `.env` +9. configure and use **MediaMTX** for camera-heavy systems +10. start OSCAR with the **sessionless monitoring launch** when possible +11. use the check or status script to compare system behavior and performance + +This is the preferred workflow for: + +* first-time deployment on a machine +* side-by-side comparison with another build +* validating memory behavior +* validating PostgreSQL behavior +* validating MediaMTX camera proxy performance + +--- + +## Included updates + +### Linux + +* `.env`-based configuration +* `launch-all.sh` +* `launch.sh` +* `monitor-oscar.sh` +* `check-oscar-status.sh` + +### Windows + +* `.env`-based configuration +* `launch-all.bat` +* `launch.bat` +* `monitor-oscar.bat` +* `check-oscar-status.ps1` + +--- + +## Recommended operating model + +### Deployment + +* select the correct hardware profile in `.env` +* use the updated launch scripts +* use the **sessionless monitoring launch** for initial validation and normal field deployment when possible +* use the attached launch only for interactive troubleshooting +* let the scripts manage already-running instances instead of manually launching duplicates +* use MediaMTX where many camera streams are involved +* review generated status reports during early burn-in testing + +### Validation after upgrade + +After upgrading, confirm that: + +* PostgreSQL sessions plateau well below the configured connection limit +* swap usage remains low or zero +* JVM RSS stabilizes after startup +* thread count does not continuously climb over long runs +* database status reports do not show saturation errors + +--- + +## Known issues still under observation + +These changes significantly improve stability, but a few items are still worth monitoring: + +* `RapiscanSensor` parse errors such as `For input string: "000NaN"` +* repeated MQTT `Broken pipe` errors +* high thread counts in some runs +* reconnect churn on certain devices or services + +These do not appear to be the primary cause of the major stability issue addressed in this release, but they remain candidates for future cleanup. + +--- + +## Upgrade notes + +1. Stop and remove any previous OSCAR PostGIS container. +2. Remove the previous OSCAR Docker network **if one exists**. +3. Stop any previous OSCAR Java process that is still running. +4. Delete the old `oscar-3.5.0` directory. +5. Unzip OSCAR **3.5.1** into a fresh directory. +6. Install **OpenJDK 21** and **Docker**. +7. Rename `env.txt` to `.env` if needed. +8. Edit `.env` and select the correct hardware profile. +9. For camera-heavy deployments, configure MediaMTX. +10. Start the system with the **sessionless monitoring launch** when possible. +11. Use the check or status script after startup and again after runtime burn-in. + +--- + +## Summary + +This release materially improves OSCAR behavior on larger systems by: + +* matching resource use to host hardware +* reducing unnecessary database connection retention +* improving monitoring and diagnostics +* increasing deployment consistency across Linux and Windows +* supporting MediaMTX-based camera proxy architectures +* validating dependencies and packaged startup requirements earlier +* handling already-running OSCAR instances more safely + +The biggest backend improvement is the correction of oversized Hikari idle pooling, which reduced PostgreSQL session usage from approximately **186** to approximately **21** in testing. diff --git a/dist/documentation/Standard_PostgreSQL_Setup.md b/dist/documentation/Standard_PostgreSQL_Setup.md new file mode 100644 index 0000000..c6f2a10 --- /dev/null +++ b/dist/documentation/Standard_PostgreSQL_Setup.md @@ -0,0 +1,101 @@ +# Standard PostgreSQL Database Setup + +If you are deploying OSCAR with a standard, standalone PostgreSQL database (rather than the default Dockerized option), follow these steps to initialize and configure the database properly. + +## Prerequisites + +- PostgreSQL (version 16 recommended, matching the Docker image) +- PostGIS extensions installed on the database server + +## Step 1: Create the Database + +Connect to your PostgreSQL instance as a superuser (e.g., `postgres`) and create the `gis` database: + +```sql +CREATE DATABASE gis; +``` + +## Step 2: Configure System Parameters + +Set the required `max_connections` limit: + +```sql +ALTER SYSTEM SET max_connections = 1024; +``` + +_Note: You will need to reload or restart the PostgreSQL service for system-level parameter changes to take effect._ + +## Step 3: Enable PostGIS and Required Extensions + +Connect to the newly created `gis` database. If using `psql`, you can do this by running: + +```sql +\connect gis; +``` + +Then, run the following SQL commands to enable the necessary extensions required by the OSCAR system: + +```sql +CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE EXTENSION IF NOT EXISTS btree_gist; +CREATE EXTENSION IF NOT EXISTS btree_gin; +CREATE EXTENSION IF NOT EXISTS fuzzystrmatch; +CREATE EXTENSION IF NOT EXISTS postgis; +CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder; +CREATE EXTENSION IF NOT EXISTS postgis_topology; +``` + +## Step 4: Configure User Credentials and Access + +Ensure your database is accessible to the OSCAR application: + +1. Configure appropriate host-based authentication in your `pg_hba.conf` file to allow the OSCAR server to connect. +2. Ensure the user connecting to the database has sufficient privileges on the `gis` database. + +## Step 5: Configure OSCAR Database Connection + +Once the database is set up, you must configure OSCAR to connect to it. This can be done in one of two ways: + +### Option A: Edit `config.json` Directly (Pre-launch) + +Before starting the OSCAR application, you can edit the `dist/config/standard/config.json` file. Locate the configuration module for `SystemDriverDatabaseConfig` containing the `PostgisObsSystemDatabaseConfig` and update the connection details. + +Find the block that looks similar to this: + +```json +{ + "objClass": "org.sensorhub.impl.database.system.SystemDriverDatabaseConfig", + "dbConfig": { + "objClass": "org.sensorhub.impl.datastore.postgis.database.PostgisObsSystemDatabaseConfig", + "url": "localhost:5432", + "dbName": "gis", + "login": "postgres", + "password": "postgres", + "idProviderType": "SEQUENTIAL", + "autoCommitPeriod": 10, + "useBatch": false, + "id": "bfbd6d58-1a4a-40b4-999d-381a1489cbb5", + "autoStart": false, + "moduleClass": "org.sensorhub.impl.datastore.postgis.database.PostgisObsSystemDatabase" + }, + // ... other fields + "name": "PostGIS Database" +} +``` + +Update the following fields to match your standalone database configuration: + +- `url`: The hostname or IP address of your PostgreSQL server and the port (e.g., `db.example.com:5432`). +- `dbName`: The database name (should be `gis` if you followed Step 1). +- `login`: The username for the database. +- `password`: The password for the database user. + +### Option B: Use the OSCAR Admin Panel GUI (Post-launch) + +If OSCAR is already running (and potentially failing to connect to its default database), you can update the settings through the web administration interface: + +1. Log in to the OSCAR Admin Panel (e.g., `http://localhost:8282/sensorhub/admin`). +2. Navigate to the **Databases** tab. +3. Click on the **PostGIS Database** module. +4. Update the **URL**, **Database Name**, **Login**, and **Password** fields with your standalone database details. +5. Save the configuration and restart the module. diff --git a/dist/release/check-oscar-status.ps1 b/dist/release/check-oscar-status.ps1 new file mode 100644 index 0000000..f9f667f --- /dev/null +++ b/dist/release/check-oscar-status.ps1 @@ -0,0 +1,136 @@ +param( + [string]$BaseDir = ".", + [string]$MonitorDir = "", + [string]$OutFile = "" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'SilentlyContinue' +Set-Location $BaseDir + +if ([string]::IsNullOrWhiteSpace($MonitorDir)) { + $MonitorDir = Get-ChildItem -Directory -Filter 'oscar-monitor-*' | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName +} +if (-not $MonitorDir -or -not (Test-Path $MonitorDir)) { throw 'No oscar-monitor-* directory found.' } +if ([string]::IsNullOrWhiteSpace($OutFile)) { $OutFile = Join-Path (Get-Location) ("oscar-status-{0}.txt" -f (Get-Date -Format 'yyyyMMdd-HHmmss')) } + +$snaps = Get-ChildItem -Path $MonitorDir -Directory | Sort-Object Name +$first = $snaps | Select-Object -First 1 +$last = $snaps | Select-Object -Last 1 +$jvmPidFile = Join-Path $MonitorDir 'jvm-pid.txt' +$pidFromMonitor = if (Test-Path $jvmPidFile) { (Get-Content $jvmPidFile | Select-Object -First 1).Trim() } else { '' } +$javaProc = Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'java.exe' -and $_.CommandLine -match 'SensorHubWrapper' } | Select-Object -First 1 + +function Read-Text([string]$Path) { if (Test-Path $Path) { Get-Content $Path -Raw } else { '' } } +function Read-First([string]$Path) { if (Test-Path $Path) { (Get-Content $Path | Select-Object -First 1).Trim() } else { '' } } +function Extract-DbCount([string]$Path, [string]$State) { + if (-not (Test-Path $Path)) { return '' } + foreach ($line in Get-Content $Path) { + $parts = $line -split '\|' + if ($parts.Count -ge 2 -and $parts[0].Trim() -eq $State) { return $parts[1].Trim() } + } + '' +} +function Calc-Slots($MaxConn, $Reserved) { + if ($MaxConn -match '^\d+$' -and $Reserved -match '^\d+$') { return [int]$MaxConn - [int]$Reserved } + '' +} + +$sb = [System.Text.StringBuilder]::new() +$null = $sb.AppendLine('OSCAR STATUS REPORT') +$null = $sb.AppendLine("Generated: $(Get-Date -Format o)") +$null = $sb.AppendLine("Base directory: $(Get-Location)") +$null = $sb.AppendLine("Monitor directory: $MonitorDir") +$null = $sb.AppendLine("Output file: $OutFile") +$null = $sb.AppendLine() +$null = $sb.AppendLine('=== PROCESS STATUS ===') +$null = $sb.AppendLine("PID from monitor: $pidFromMonitor") +$null = $sb.AppendLine(("Live OSCAR PID: {0}" -f ($(if ($javaProc) { $javaProc.ProcessId } else { '' })))) +$null = $sb.AppendLine() +$null = $sb.AppendLine((Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'monitor-oscar' } | Select-Object ProcessId, Name, CommandLine | Format-Table -AutoSize | Out-String)) +$null = $sb.AppendLine((Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'java.exe' -and $_.CommandLine -match 'SensorHubWrapper' } | Select-Object ProcessId, Name, CommandLine | Format-Table -AutoSize | Out-String)) +$null = $sb.AppendLine((& docker ps --filter name=oscar-postgis-container | Out-String)) +$null = $sb.AppendLine('=== SYSTEM MEMORY AND PAGEFILE ===') +$null = $sb.AppendLine((Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize,FreePhysicalMemory,TotalVirtualMemorySize,FreeVirtualMemory | Format-List | Out-String)) +$null = $sb.AppendLine((Get-Counter '\Memory\Committed Bytes','\Memory\Commit Limit','\Paging File(_Total)\% Usage' | Out-String)) + +if ($javaProc) { + $null = $sb.AppendLine('=== LIVE JVM PROCESS ===') + $null = $sb.AppendLine((Get-Process -Id $javaProc.ProcessId | Select-Object Id,ProcessName,Threads,VirtualMemorySize64,WorkingSet64,PrivateMemorySize64,CPU,StartTime | Format-List | Out-String)) + $null = $sb.AppendLine('=== LIVE JVM JFR STATUS ===') + $null = $sb.AppendLine((& jcmd $javaProc.ProcessId JFR.check | Out-String)) + $null = $sb.AppendLine('=== LIVE JVM GC HEAP INFO ===') + $null = $sb.AppendLine((& jcmd $javaProc.ProcessId GC.heap_info | Out-String)) + $null = $sb.AppendLine('=== LIVE JVM NATIVE MEMORY SUMMARY ===') + $null = $sb.AppendLine((& jcmd $javaProc.ProcessId VM.native_memory summary | Out-String)) +} + +$lastMax = Read-First (Join-Path $last.FullName 'db-max-connections.txt') +$lastReserved = Read-First (Join-Path $last.FullName 'db-superuser-reserved-connections.txt') +$lastTotal = Read-First (Join-Path $last.FullName 'db-total-sessions.txt') +$null = $sb.AppendLine('=== LIVE POSTGRES STATUS (FROM LAST SNAPSHOT) ===') +$null = $sb.AppendLine("max_connections: $lastMax") +$null = $sb.AppendLine("superuser_reserved_connections: $lastReserved") +$null = $sb.AppendLine("usable_client_slots: $(Calc-Slots $lastMax $lastReserved)") +$null = $sb.AppendLine("total_sessions: $lastTotal") +$null = $sb.AppendLine("active: $(Extract-DbCount (Join-Path $last.FullName 'db-by-state.txt') 'active')") +$null = $sb.AppendLine("idle: $(Extract-DbCount (Join-Path $last.FullName 'db-by-state.txt') 'idle')") +$null = $sb.AppendLine("idle in transaction: $(Extract-DbCount (Join-Path $last.FullName 'db-by-state.txt') 'idle in transaction')") +$null = $sb.AppendLine() +$null = $sb.AppendLine('--- db-by-state ---') +$null = $sb.AppendLine((Read-Text (Join-Path $last.FullName 'db-by-state.txt'))) +$null = $sb.AppendLine('--- db-by-app ---') +$null = $sb.AppendLine((Read-Text (Join-Path $last.FullName 'db-by-app.txt'))) +$null = $sb.AppendLine('--- db-error ---') +$null = $sb.AppendLine((Read-Text (Join-Path $last.FullName 'db-error.txt'))) + +$null = $sb.AppendLine('=== FIRST SNAPSHOT SUMMARY ===') +$null = $sb.AppendLine("First snapshot: $($first.FullName)") +$null = $sb.AppendLine((Read-Text (Join-Path $first.FullName 'powershell-process.txt'))) +$null = $sb.AppendLine("db total sessions: $(Read-First (Join-Path $first.FullName 'db-total-sessions.txt'))") +$null = $sb.AppendLine('=== LATEST SNAPSHOT SUMMARY ===') +$null = $sb.AppendLine("Latest snapshot: $($last.FullName)") +$null = $sb.AppendLine((Read-Text (Join-Path $last.FullName 'powershell-process.txt'))) +$null = $sb.AppendLine("db total sessions: $(Read-First (Join-Path $last.FullName 'db-total-sessions.txt'))") + +$null = $sb.AppendLine('=== RECENT TREND (LAST 20 SNAPSHOTS) ===') +foreach ($d in ($snaps | Select-Object -Last 20)) { + $dbTotal = Read-First (Join-Path $d.FullName 'db-total-sessions.txt') + $dbMax = Read-First (Join-Path $d.FullName 'db-max-connections.txt') + $dbReserved = Read-First (Join-Path $d.FullName 'db-superuser-reserved-connections.txt') + $dbActive = Extract-DbCount (Join-Path $d.FullName 'db-by-state.txt') 'active' + $dbIdle = Extract-DbCount (Join-Path $d.FullName 'db-by-state.txt') 'idle' + $proc = (Read-Text (Join-Path $d.FullName 'powershell-process.txt')) -replace '\r?\n',' ' + $null = $sb.AppendLine("$($d.Name) $proc db_total=$dbTotal db_active=$dbActive db_idle=$dbIdle db_slots=$(Calc-Slots $dbMax $dbReserved)") +} +$null = $sb.AppendLine() + +$dbCsv = Join-Path $MonitorDir 'db-connection-trend.csv' +if (Test-Path $dbCsv) { + $null = $sb.AppendLine('=== DB CONNECTION TREND CSV (LAST 40 LINES) ===') + $null = $sb.AppendLine(((Get-Content $dbCsv | Select-Object -Last 40) -join [Environment]::NewLine)) + $null = $sb.AppendLine() +} + +$null = $sb.AppendLine('=== LOG TAILS ===') +$null = $sb.AppendLine('--- launch.stdout.log (last 50 lines) ---') +$null = $sb.AppendLine(((Get-Content (Join-Path $MonitorDir 'launch.stdout.log') -Tail 50) -join [Environment]::NewLine)) +$null = $sb.AppendLine() +$null = $sb.AppendLine('--- launch.stderr.log (last 50 lines) ---') +$null = $sb.AppendLine(((Get-Content (Join-Path $MonitorDir 'launch.stderr.log') -Tail 50) -join [Environment]::NewLine)) +$null = $sb.AppendLine() +$null = $sb.AppendLine('--- postgres docker logs (last captured 100 lines) ---') +$null = $sb.AppendLine(((Get-Content (Join-Path $last.FullName 'docker-logs-tail.txt') -Tail 100) -join [Environment]::NewLine)) +$null = $sb.AppendLine() + +$null = $sb.AppendLine('=== QUICK READ ===') +$null = $sb.AppendLine("First DB total sessions: $(Read-First (Join-Path $first.FullName 'db-total-sessions.txt'))") +$null = $sb.AppendLine("Latest DB total sessions: $(Read-First (Join-Path $last.FullName 'db-total-sessions.txt'))") +$null = $sb.AppendLine("Latest DB usable client slots: $(Calc-Slots $lastMax $lastReserved)") +$null = $sb.AppendLine('Interpretation guide:') +$null = $sb.AppendLine('- Healthy memory: process memory and JVM native memory plateau.') +$null = $sb.AppendLine('- Healthy DB: total sessions rise at startup and then plateau well below usable client slots.') +$null = $sb.AppendLine('- Suspicious DB: total sessions keep climbing, idle sessions pile up, or db-error shows too many clients already.') + +[System.IO.File]::WriteAllText($OutFile, $sb.ToString()) +Write-Host "Wrote report to: $OutFile" diff --git a/dist/release/check-oscar-status.sh b/dist/release/check-oscar-status.sh new file mode 100644 index 0000000..885bca8 --- /dev/null +++ b/dist/release/check-oscar-status.sh @@ -0,0 +1,225 @@ +#!/bin/bash +set -euo pipefail + +BASE_DIR="${1:-.}" +LATEST_DIR="${2:-}" +OUT_FILE="${3:-}" + +cd "$BASE_DIR" + +if [ -z "$LATEST_DIR" ]; then + LATEST_DIR="$(ls -td oscar-monitor-* 2>/dev/null | head -n 1 || true)" +fi + +if [ -z "$LATEST_DIR" ] || [ ! -d "$LATEST_DIR" ]; then + echo "Error: no oscar-monitor-* directory found." + exit 1 +fi + +if [ -z "$OUT_FILE" ]; then + OUT_FILE="oscar-status-$(date +%Y%m%d-%H%M%S).txt" +fi + +FIRST_SNAP="$(find "$LATEST_DIR" -maxdepth 1 -mindepth 1 -type d | sort | head -n 1 || true)" +LAST_SNAP="$(find "$LATEST_DIR" -maxdepth 1 -mindepth 1 -type d | sort | tail -n 1 || true)" +PID="" +[ -f "$LATEST_DIR/jvm-pid.txt" ] && PID="$(cat "$LATEST_DIR/jvm-pid.txt" 2>/dev/null || true)" +LIVE_PID="$(pgrep -f 'com.botts.impl.security.SensorHubWrapper' | head -n 1 || true)" + +extract_db_metric() { + local file="$1" default="$2" + if [ -f "$file" ]; then + tr -d '[:space:]' < "$file" | tail -n 1 + else + echo "$default" + fi +} + +calc_slots() { + local max="$1" reserved="$2" + if [[ "$max" =~ ^[0-9]+$ ]] && [[ "$reserved" =~ ^[0-9]+$ ]]; then + echo $((max - reserved)) + fi +} + +{ + echo "OSCAR STATUS REPORT" + echo "Generated: $(date -Is)" + echo "Base directory: $(pwd)" + echo "Monitor directory: $LATEST_DIR" + echo "Output file: $OUT_FILE" + echo + + echo "=== PROCESS STATUS ===" + echo "PID from monitor: ${PID:-}" + echo "Live OSCAR PID: ${LIVE_PID:-}" + echo + pgrep -af monitor-oscar.sh || true + pgrep -af 'com.botts.impl.security.SensorHubWrapper' || true + echo + docker ps --filter name=oscar-postgis-container || true + echo + + echo "=== SYSTEM MEMORY ===" + free -h || true + echo + echo "--- vmstat (5 samples) ---" + vmstat 1 5 || true + echo + + if [ -n "${LIVE_PID:-}" ] && [ -r "/proc/$LIVE_PID/status" ]; then + echo "=== LIVE JVM /proc STATUS ===" + grep -E 'Name|State|VmSize|VmRSS|VmSwap|Threads' "/proc/$LIVE_PID/status" || true + echo + fi + + if [ -n "${LIVE_PID:-}" ] && [ -r "/proc/$LIVE_PID/smaps_rollup" ]; then + echo "=== LIVE JVM SMAPS ROLLUP ===" + cat "/proc/$LIVE_PID/smaps_rollup" || true + echo + fi + + if [ -n "${LIVE_PID:-}" ] && command -v jcmd >/dev/null 2>&1; then + echo "=== LIVE JVM JFR STATUS ===" + jcmd "$LIVE_PID" JFR.check || true + echo + echo "=== LIVE JVM GC HEAP INFO ===" + jcmd "$LIVE_PID" GC.heap_info || true + echo + echo "=== LIVE JVM NATIVE MEMORY SUMMARY ===" + jcmd "$LIVE_PID" VM.native_memory summary || true + echo + fi + + echo "=== LIVE POSTGRES STATUS ===" + if docker ps --format '{{.Names}}' | grep -Eq '^oscar-postgis-container$'; then + if [ -f "$LAST_SNAP/db-max-connections.txt" ]; then + echo "max_connections: $(extract_db_metric "$LAST_SNAP/db-max-connections.txt" n/a)" + fi + if [ -f "$LAST_SNAP/db-superuser-reserved-connections.txt" ]; then + echo "superuser_reserved_connections: $(extract_db_metric "$LAST_SNAP/db-superuser-reserved-connections.txt" n/a)" + fi + if [ -f "$LAST_SNAP/db-total-sessions.txt" ]; then + echo "total_sessions: $(extract_db_metric "$LAST_SNAP/db-total-sessions.txt" n/a)" + fi + if [ -f "$LAST_SNAP/db-by-state.txt" ]; then + echo + echo "--- db-by-state ---" + cat "$LAST_SNAP/db-by-state.txt" || true + fi + if [ -f "$LAST_SNAP/db-by-app.txt" ]; then + echo + echo "--- db-by-app ---" + cat "$LAST_SNAP/db-by-app.txt" || true + fi + if [ -f "$LAST_SNAP/db-activity-detail.txt" ]; then + echo + echo "--- db-activity-detail (first 40 lines) ---" + head -n 40 "$LAST_SNAP/db-activity-detail.txt" || true + fi + if [ -f "$LAST_SNAP/db-error.txt" ]; then + echo + echo "--- db-error ---" + cat "$LAST_SNAP/db-error.txt" || true + fi + else + echo "Postgres container is not running." + fi + echo + + echo "=== FIRST SNAPSHOT SUMMARY ===" + echo "First snapshot: ${FIRST_SNAP:-}" + if [ -n "${FIRST_SNAP:-}" ] && [ -f "$FIRST_SNAP/proc-status.txt" ]; then + grep -E 'VmRSS|VmSwap|Threads' "$FIRST_SNAP/proc-status.txt" || true + fi + if [ -n "${FIRST_SNAP:-}" ] && [ -f "$FIRST_SNAP/nmt-summary.txt" ]; then + grep '^Total:' "$FIRST_SNAP/nmt-summary.txt" || true + fi + if [ -n "${FIRST_SNAP:-}" ] && [ -f "$FIRST_SNAP/db-total-sessions.txt" ]; then + echo "db total sessions: $(extract_db_metric "$FIRST_SNAP/db-total-sessions.txt" n/a)" + fi + echo + + echo "=== LATEST SNAPSHOT SUMMARY ===" + echo "Latest snapshot: ${LAST_SNAP:-}" + if [ -n "${LAST_SNAP:-}" ] && [ -f "$LAST_SNAP/proc-status.txt" ]; then + grep -E 'VmRSS|VmSwap|Threads' "$LAST_SNAP/proc-status.txt" || true + fi + if [ -n "${LAST_SNAP:-}" ] && [ -f "$LAST_SNAP/nmt-summary.txt" ]; then + grep '^Total:' "$LAST_SNAP/nmt-summary.txt" || true + fi + if [ -n "${LAST_SNAP:-}" ] && [ -f "$LAST_SNAP/db-total-sessions.txt" ]; then + echo "db total sessions: $(extract_db_metric "$LAST_SNAP/db-total-sessions.txt" n/a)" + fi + echo + + echo "=== RECENT TREND (LAST 20 SNAPSHOTS) ===" + for d in $(find "$LATEST_DIR" -maxdepth 1 -mindepth 1 -type d | sort | tail -n 20); do + printf "%s " "$(basename "$d")" + [ -f "$d/proc-status.txt" ] && grep -E 'VmRSS|VmSwap|Threads' "$d/proc-status.txt" | tr '\n' ' ' + [ -f "$d/nmt-summary.txt" ] && grep '^Total:' "$d/nmt-summary.txt" | tr '\n' ' ' + if [ -f "$d/db-total-sessions.txt" ]; then + printf "db_total=%s " "$(extract_db_metric "$d/db-total-sessions.txt" n/a)" + fi + if [ -f "$d/db-by-state.txt" ]; then + printf "db_active=%s " "$(awk -F'|' '$1=="active" {gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' "$d/db-by-state.txt" | tail -n 1)" + printf "db_idle=%s " "$(awk -F'|' '$1=="idle" {gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' "$d/db-by-state.txt" | tail -n 1)" + printf "db_idle_tx=%s " "$(awk -F'|' '$1=="idle in transaction" {gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' "$d/db-by-state.txt" | tail -n 1)" + fi + if [ -f "$d/db-error.txt" ] && [ -s "$d/db-error.txt" ]; then + printf "db_error=yes " + fi + echo + done + echo + + if [ -f "$LATEST_DIR/db-connection-trend.csv" ]; then + echo "=== DB CONNECTION TREND CSV (LAST 40 LINES) ===" + tail -n 40 "$LATEST_DIR/db-connection-trend.csv" || true + echo + fi + + echo "=== LOG TAILS ===" + [ -f "$LATEST_DIR/launch.stdout.log" ] && { echo '--- launch.stdout.log (last 50 lines) ---'; tail -n 50 "$LATEST_DIR/launch.stdout.log"; echo; } + [ -f "$LATEST_DIR/launch.stderr.log" ] && { echo '--- launch.stderr.log (last 50 lines) ---'; tail -n 50 "$LATEST_DIR/launch.stderr.log"; echo; } + [ -f "$LAST_SNAP/docker-logs-tail.txt" ] && { echo '--- postgres docker logs (last captured 100 lines) ---'; tail -n 100 "$LAST_SNAP/docker-logs-tail.txt"; echo; } + + echo "=== QUICK READ ===" + FIRST_RSS=""; LAST_RSS=""; FIRST_SWAP=""; LAST_SWAP=""; FIRST_THREADS=""; LAST_THREADS="" + FIRST_DB_TOTAL=""; LAST_DB_TOTAL=""; FIRST_MAX=""; LAST_MAX=""; FIRST_RESERVED=""; LAST_RESERVED="" + + if [ -n "${FIRST_SNAP:-}" ] && [ -f "$FIRST_SNAP/proc-status.txt" ]; then + FIRST_RSS="$(grep '^VmRSS:' "$FIRST_SNAP/proc-status.txt" | awk '{print $2 " " $3}' || true)" + FIRST_SWAP="$(grep '^VmSwap:' "$FIRST_SNAP/proc-status.txt" | awk '{print $2 " " $3}' || true)" + FIRST_THREADS="$(grep '^Threads:' "$FIRST_SNAP/proc-status.txt" | awk '{print $2}' || true)" + fi + if [ -n "${LAST_SNAP:-}" ] && [ -f "$LAST_SNAP/proc-status.txt" ]; then + LAST_RSS="$(grep '^VmRSS:' "$LAST_SNAP/proc-status.txt" | awk '{print $2 " " $3}' || true)" + LAST_SWAP="$(grep '^VmSwap:' "$LAST_SNAP/proc-status.txt" | awk '{print $2 " " $3}' || true)" + LAST_THREADS="$(grep '^Threads:' "$LAST_SNAP/proc-status.txt" | awk '{print $2}' || true)" + fi + [ -n "${FIRST_SNAP:-}" ] && FIRST_DB_TOTAL="$(extract_db_metric "$FIRST_SNAP/db-total-sessions.txt" n/a)" + [ -n "${LAST_SNAP:-}" ] && LAST_DB_TOTAL="$(extract_db_metric "$LAST_SNAP/db-total-sessions.txt" n/a)" + [ -n "${FIRST_SNAP:-}" ] && FIRST_MAX="$(extract_db_metric "$FIRST_SNAP/db-max-connections.txt" n/a)" + [ -n "${LAST_SNAP:-}" ] && LAST_MAX="$(extract_db_metric "$LAST_SNAP/db-max-connections.txt" n/a)" + [ -n "${FIRST_SNAP:-}" ] && FIRST_RESERVED="$(extract_db_metric "$FIRST_SNAP/db-superuser-reserved-connections.txt" n/a)" + [ -n "${LAST_SNAP:-}" ] && LAST_RESERVED="$(extract_db_metric "$LAST_SNAP/db-superuser-reserved-connections.txt" n/a)" + + echo "First RSS: ${FIRST_RSS:-n/a}" + echo "Latest RSS: ${LAST_RSS:-n/a}" + echo "First VmSwap: ${FIRST_SWAP:-n/a}" + echo "Latest VmSwap: ${LAST_SWAP:-n/a}" + echo "First Threads: ${FIRST_THREADS:-n/a}" + echo "Latest Threads:${LAST_THREADS:-n/a}" + echo "First DB total sessions: ${FIRST_DB_TOTAL:-n/a}" + echo "Latest DB total sessions: ${LAST_DB_TOTAL:-n/a}" + echo "First DB usable client slots: $(calc_slots "$FIRST_MAX" "$FIRST_RESERVED")" + echo "Latest DB usable client slots: $(calc_slots "$LAST_MAX" "$LAST_RESERVED")" + echo + echo "Interpretation guide:" + echo "- Healthy memory: RSS, VmSwap, and thread count rise at startup and then flatten." + echo "- Healthy DB: total sessions rise at startup and then plateau well below usable client slots." + echo "- Suspicious DB: total sessions keep climbing, idle sessions pile up, or db_error shows too many clients already." +} > "$OUT_FILE" + +echo "Wrote report to: $OUT_FILE" diff --git a/dist/release/env.template b/dist/release/env.template new file mode 100644 index 0000000..f1c4058 --- /dev/null +++ b/dist/release/env.template @@ -0,0 +1,55 @@ +# --- SYSTEM PROFILE --- +# Options: RPI4, 8GB, 16GB, 32GB +SYSTEM_PROFILE=16GB + +# --- DATABASE SETTINGS --- +DB_NAME=gis +DB_USER=postgres +DB_PASSWORD=postgres +DB_PORT=5432 +DB_HOST=localhost +CONTAINER_NAME=oscar-postgis-container + +# --- SECURITY --- +# Replace before production use +KEYSTORE_PASSWORD=atakatak +TRUSTSTORE_PASSWORD=changeit + +# Optional: +# If set, launch scripts can use this instead of profile defaults/helper files. +# INITIAL_ADMIN_PASSWORD=admin + +# --- PROCESS HANDLING --- +# 0 = refuse to start if OSCAR is already running +# 1 = stop the running OSCAR instance and start fresh +FORCE_RESTART=0 + +# --- MONITOR BEHAVIOR --- +# 0 = refuse to attach if OSCAR is already running +# 1 = attach monitoring to an already running OSCAR instance +ATTACH_TO_EXISTING=0 + +# Maximum time to wait for OSCAR JVM startup in monitor scripts +MAX_WAIT_SECONDS=300 + +# --- POSTGIS STARTUP / READINESS --- +# Number of readiness retries for PostGIS startup checks +RETRY_MAX=120 + +# Seconds between readiness retries +RETRY_INTERVAL=2 + +# Extra delay after PostGIS reports ready +POSTGIS_READY_DELAY=5 + +# --- OPTIONAL MEMORY / DIAGNOSTICS OVERRIDES --- +# Leave blank to use profile defaults from launch scripts +JAVACPP_MAX_BYTES= +JAVACPP_MAX_PHYSICAL_BYTES= +JFR_FILENAME= + +# --- OPTIONAL ARM BUILD OVERRIDES --- +# Only needed when using ARM-specific launch/build paths +# POSTGIS_IMAGE_NAME=oscar-postgis-arm +# POSTGIS_DOCKERFILE=Dockerfile-arm64 +# POSTGIS_PLATFORM=linux/arm64 \ No newline at end of file diff --git a/dist/release/launch-all-arm.sh b/dist/release/launch-all-arm.sh old mode 100755 new mode 100644 index 96c1cc5..488215b --- a/dist/release/launch-all-arm.sh +++ b/dist/release/launch-all-arm.sh @@ -1,76 +1,308 @@ -#!/bin/bash - -HOST=localhost -DB_NAME=gis -DB_USER=postgres -RETRY_MAX=20 -RETRY_INTERVAL=5 -PROJECT_DIR="$(pwd)" # Store the original directory -CONTAINER_NAME=oscar-postgis-container - -#sudo docker rm -f "$CONTAINER_NAME" 2>/dev/null || true - -# Create pgdata directory if needed -if [ ! -d "${PROJECT_DIR}/pgdata" ]; then - echo "Creating pgdata folder..." - mkdir -p "${PROJECT_DIR}/pgdata" -fi +#!/usr/bin/env bash +set -euo pipefail + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do + SOURCE_DIR="$(CDPATH= cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)" + SOURCE="$(readlink "$SOURCE")" + case "$SOURCE" in + /*) ;; + *) SOURCE="${SOURCE_DIR}/${SOURCE}" ;; + esac +done +PROJECT_DIR="$(CDPATH= cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)" +ENV_FILE="$PROJECT_DIR/.env" +MATCH_EXPR='com.botts.impl.security.SensorHubWrapper' +FORCE_RESTART="${FORCE_RESTART:-0}" +RETRY_MAX="${RETRY_MAX:-120}" +RETRY_INTERVAL="${RETRY_INTERVAL:-2}" +POSTGIS_READY_DELAY="${POSTGIS_READY_DELAY:-5}" +IMAGE_NAME="${POSTGIS_IMAGE_NAME:-${IMAGE_NAME:-oscar-postgis-arm}}" +POSTGIS_DOCKERFILE="${POSTGIS_DOCKERFILE:-Dockerfile-arm64}" +POSTGIS_PLATFORM="${POSTGIS_PLATFORM:-linux/arm64}" + +load_env() { + local env_file="$1" + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ""|"#"*) continue ;; + export\ *) line="${line#export }" ;; + esac + local name="${line%%=*}" + local value="${line#*=}" + value="${value%$'\r'}" + export "${name}=${value}" + done < "$env_file" +} + +require_cmd() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Error: required command not found: $cmd" + exit 1 + fi +} + +get_java_major() { + java -version 2>&1 | awk -F'"' '/version/ { split($2, v, "."); print v[1]; exit }' +} + +check_dependencies() { + require_cmd bash + require_cmd java + require_cmd keytool + require_cmd docker + + if ! docker info >/dev/null 2>&1; then + echo "Error: Docker is installed, but the Docker daemon is not running." + exit 1 + fi + + local java_major + java_major="$(get_java_major || true)" + if [[ -z "$java_major" || ! "$java_major" =~ ^[0-9]+$ ]]; then + echo "Error: could not determine Java version. Java 21 or newer is required." + exit 1 + fi + if [ "$java_major" -lt 21 ]; then + echo "Error: Java 21 or newer is required. Found Java $java_major." + exit 1 + fi +} + +find_existing_oscar_pids() { + pgrep -f "$MATCH_EXPR" || true +} + +stop_existing_oscar() { + local pids="$1" + if [ -z "$pids" ]; then + return 0 + fi + + echo "Stopping existing OSCAR instance(s): $pids" + kill $pids 2>/dev/null || true + + local waited=0 + while [ "$waited" -lt 15 ]; do + sleep 1 + waited=$((waited + 1)) + if [ -z "$(find_existing_oscar_pids)" ]; then + return 0 + fi + done + + echo "Existing OSCAR instance still running after graceful stop. Forcing stop." + kill -9 $pids 2>/dev/null || true + sleep 1 + + if [ -n "$(find_existing_oscar_pids)" ]; then + echo "Error: unable to stop the existing OSCAR instance." + exit 1 + fi +} + +check_existing_oscar() { + local pids + pids="$(find_existing_oscar_pids)" + + if [ -z "$pids" ]; then + return 0 + fi -# Check Docker -if ! command -v docker >/dev/null 2>&1; then - echo "Error: Docker is not installed. Please install Docker first." + if [ "$FORCE_RESTART" = "1" ]; then + echo "Existing OSCAR instance found with PID(s): $pids. Replacing because FORCE_RESTART=1." + stop_existing_oscar "$pids" + return 0 + fi + + echo "OSCAR is already running with PID(s): $pids." + echo "Stop the running instance first, or set FORCE_RESTART=1 to replace it." + exit 1 +} + +require_env() { + local name="$1" + local value="${!name:-}" + if [ -z "$value" ]; then + echo "Error: ${name} is not set in .env." + exit 1 + fi +} + +require_number() { + local name="$1" + local value="${!name:-}" + case "$value" in + ''|*[!0-9]*) + echo "Error: ${name} must be a number, got '${value}'." + exit 1 + ;; + esac +} + +ensure_project_layout() { + if [ ! -d "$PROJECT_DIR/postgis" ]; then + echo "Error: postgis directory not found in $PROJECT_DIR" + exit 1 + fi + + if [ ! -f "$PROJECT_DIR/postgis/$POSTGIS_DOCKERFILE" ]; then + echo "Error: $POSTGIS_DOCKERFILE not found in $PROJECT_DIR/postgis" + exit 1 + fi + + if [ ! -d "$PROJECT_DIR/osh-node-oscar" ]; then + echo "Error: osh-node-oscar directory not found in $PROJECT_DIR" + exit 1 + fi + + if [ ! -f "$PROJECT_DIR/osh-node-oscar/launch.sh" ]; then + echo "Error: launch.sh not found in $PROJECT_DIR/osh-node-oscar" + exit 1 + fi + + mkdir -p "$PROJECT_DIR/pgdata" +} + +if [ ! -f "$ENV_FILE" ]; then + echo "Error: .env file not found in $PROJECT_DIR" + echo "Create it by copying env.template to .env and editing the values." exit 1 fi -echo "Building PostGIS Docker image..." +load_env "$ENV_FILE" +check_dependencies +check_existing_oscar +ensure_project_layout + +SYSTEM_PROFILE="${SYSTEM_PROFILE:-8GB}" +CONTAINER_NAME="${CONTAINER_NAME:-oscar-postgis-container}" +DB_HOST="${DB_HOST:-localhost}" +export SYSTEM_PROFILE CONTAINER_NAME DB_HOST RETRY_MAX RETRY_INTERVAL POSTGIS_READY_DELAY IMAGE_NAME POSTGIS_DOCKERFILE POSTGIS_PLATFORM + +require_env DB_NAME +require_env DB_USER +require_env DB_PASSWORD +require_env DB_PORT -cd postgis || { echo "Error: postgis directory not found"; exit 1; } +require_number DB_PORT +require_number RETRY_MAX +require_number RETRY_INTERVAL +require_number POSTGIS_READY_DELAY -# Build PostGIS -sudo docker build . \ - --file=Dockerfile-arm64 \ - --tag=oscar-postgis-arm +case "${SYSTEM_PROFILE^^}" in + RPI4) + SYSTEM_PROFILE="RPI4" + PG_SHARED="256MB" + PG_CACHE="1GB" + PG_WORK_MEM="2MB" + PG_MAINT="64MB" + PG_MAX_CONN="75" + ;; + 8GB) + SYSTEM_PROFILE="8GB" + PG_SHARED="512MB" + PG_CACHE="2GB" + PG_WORK_MEM="4MB" + PG_MAINT="128MB" + PG_MAX_CONN="125" + ;; + 16GB) + SYSTEM_PROFILE="16GB" + PG_SHARED="1GB" + PG_CACHE="4GB" + PG_WORK_MEM="8MB" + PG_MAINT="256MB" + PG_MAX_CONN="200" + ;; + 32GB) + SYSTEM_PROFILE="32GB" + PG_SHARED="2GB" + PG_CACHE="8GB" + PG_WORK_MEM="16MB" + PG_MAINT="512MB" + PG_MAX_CONN="300" + ;; + *) + echo "Unknown profile '${SYSTEM_PROFILE}', using 8GB defaults." + SYSTEM_PROFILE="8GB" + PG_SHARED="512MB" + PG_CACHE="2GB" + PG_WORK_MEM="4MB" + PG_MAINT="128MB" + PG_MAX_CONN="125" + ;; + esac -echo "Starting PostGIS container..." -echo "PROJECT_DIR is set to: ${PROJECT_DIR}" +export SYSTEM_PROFILE CONTAINER_NAME DB_NAME DB_USER DB_PASSWORD DB_PORT -if docker ps -a --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}$"; then - # The container exists - if docker ps --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}$"; then - echo "Container already running: ${CONTAINER_NAME}" +echo "Building PostGIS Docker image for Apple Silicon / ARM64..." +( + cd "$PROJECT_DIR/postgis" + if [ -n "$POSTGIS_PLATFORM" ]; then + docker build --platform "$POSTGIS_PLATFORM" . --file="$POSTGIS_DOCKERFILE" --tag="$IMAGE_NAME" else - echo "Starting existing container: ${CONTAINER_NAME}" - docker start "${CONTAINER_NAME}" + docker build . --file="$POSTGIS_DOCKERFILE" --tag="$IMAGE_NAME" fi -else - echo "Creating new container: ${CONTAINER_NAME}" - docker run \ - --name $CONTAINER_NAME \ - -e POSTGRES_DB=$DB_NAME \ - -e POSTGRES_USER=$DB_USER \ - -e POSTGRES_PASSWORD=postgres \ - -e DATADIR=/var/lib/postgresql/data \ - -p 5432:5432 \ - -v "$(pwd)/pgdata:/var/lib/postgresql/data" \ - -d \ - oscar-postgis-arm || { echo "Failed to start PostGIS container"; exit 1; } -fi +) + +echo "Preparing PostGIS container for profile: $SYSTEM_PROFILE" +echo " Image: $IMAGE_NAME" +echo " Dockerfile: $POSTGIS_DOCKERFILE" +echo " Port: ${DB_PORT}:5432" +echo " Data: $PROJECT_DIR/pgdata" -# Wait for PostgreSQL/PostGIS to become ready -echo "Waiting for PostGIS ARM64 (PostgreSQL) to be ready..." +if docker container inspect "$CONTAINER_NAME" >/dev/null 2>&1; then + echo "Removing existing container '$CONTAINER_NAME' so updated settings take effect..." + docker rm -f "$CONTAINER_NAME" >/dev/null +fi -RETRY_COUNT=0 -export PGPASSWORD=postgres # Needed for pg_isready with password +echo "Creating new PostGIS container..." +docker run \ + --name "$CONTAINER_NAME" \ + -e POSTGRES_DB="$DB_NAME" \ + -e POSTGRES_USER="$DB_USER" \ + -e POSTGRES_PASSWORD="$DB_PASSWORD" \ + -e DATADIR=/var/lib/postgresql/data \ + -p "${DB_PORT}:5432" \ + -v "$PROJECT_DIR/pgdata:/var/lib/postgresql/data" \ + -d \ + "$IMAGE_NAME" \ + -c shared_buffers="$PG_SHARED" \ + -c effective_cache_size="$PG_CACHE" \ + -c work_mem="$PG_WORK_MEM" \ + -c maintenance_work_mem="$PG_MAINT" \ + -c max_connections="$PG_MAX_CONN" \ + -c superuser_reserved_connections=10 \ + -c idle_session_timeout=600000 \ + -c log_connections=on \ + -c log_disconnections=on \ + -c wal_buffers=16MB \ + -c random_page_cost=1.1 \ + -c effective_io_concurrency=200 -until docker exec "$CONTAINER_NAME" pg_isready -U "$DB_USER" -d "$DB_NAME" > /dev/null 2>&1; do - echo "PostGIS not ready yet, retrying..." - sleep "${RETRY_INTERVAL}" +echo "Waiting for PostGIS ARM64 to be ready..." +export PGPASSWORD="$DB_PASSWORD" +retry_count=0 +until docker exec "$CONTAINER_NAME" pg_isready -U "$DB_USER" -d "$DB_NAME" >/dev/null 2>&1; do + retry_count=$((retry_count + 1)) + if [ "$retry_count" -ge "$RETRY_MAX" ]; then + echo "Error: PostGIS did not become ready after $((RETRY_MAX * RETRY_INTERVAL)) seconds." + echo "Last container logs:" + docker logs --tail 50 "$CONTAINER_NAME" || true + exit 1 + fi + echo "PostGIS not ready yet, retrying..." + sleep "$RETRY_INTERVAL" done -echo "PostGIS (PostgreSQL) is ready! Please wait for OpenSensorHub to start..." +echo "PostGIS is ready. Starting OpenSensorHub..." +sleep "$POSTGIS_READY_DELAY" -sleep 10 +cd "$PROJECT_DIR/osh-node-oscar" +if [ ! -x ./launch.sh ]; then + chmod +x ./launch.sh +fi -# Launch osh-node-oscar -cd "$PROJECT_DIR/osh-node-oscar" || { echo "Error: osh-node-oscar not found"; exit 1; } -./launch.sh \ No newline at end of file +exec ./launch.sh diff --git a/dist/release/launch-all-arm_old.sh b/dist/release/launch-all-arm_old.sh new file mode 100644 index 0000000..358cdd0 --- /dev/null +++ b/dist/release/launch-all-arm_old.sh @@ -0,0 +1,247 @@ +#!/bin/bash +set -euo pipefail + +# Apple Silicon / ARM64 launcher for the full OSH + PostGIS stack. +# Resolves paths from this script's location, not from the caller's cwd. + +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do + SOURCE_DIR="$(CDPATH= cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)" + SOURCE="$(readlink "$SOURCE")" + case "$SOURCE" in + /*) ;; + *) SOURCE="${SOURCE_DIR}/${SOURCE}" ;; + esac +done +PROJECT_DIR="$(CDPATH= cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)" +ENV_FILE="${PROJECT_DIR}/.env" + +IMAGE_NAME="${POSTGIS_IMAGE_NAME:-${IMAGE_NAME:-oscar-postgis-arm}}" +POSTGIS_DOCKERFILE="${POSTGIS_DOCKERFILE:-Dockerfile-arm64}" +POSTGIS_PLATFORM="${POSTGIS_PLATFORM:-linux/arm64}" + +if [ ! -f "$ENV_FILE" ]; then + echo "Error: .env file not found in ${PROJECT_DIR}." + echo "Create it by copying env.template to .env and editing the values." + exit 1 +fi + +# Export values from .env so osh-node-oscar/launch.sh can use the same settings. +set -a +# shellcheck disable=SC1090 +. "$ENV_FILE" +set +a + +# Remove a possible CR from values if .env was edited on Windows. +strip_cr_var() { + _name="$1" + eval "_value=\${${_name}-}" + _value="${_value%$'\r'}" + export "${_name}=${_value}" +} + +for _var in \ + SYSTEM_PROFILE DB_NAME DB_USER DB_PASSWORD DB_PORT DB_HOST CONTAINER_NAME \ + KEYSTORE_PASSWORD TRUSTSTORE_PASSWORD JAVACPP_MAX_BYTES \ + JAVACPP_MAX_PHYSICAL_BYTES JFR_FILENAME RETRY_MAX RETRY_INTERVAL \ + POSTGIS_READY_DELAY IMAGE_NAME POSTGIS_IMAGE_NAME POSTGIS_DOCKERFILE POSTGIS_PLATFORM + do + strip_cr_var "$_var" +done +unset _var + +SYSTEM_PROFILE="${SYSTEM_PROFILE:-8GB}" +CONTAINER_NAME="${CONTAINER_NAME:-oscar-postgis-container}" +DB_HOST="${DB_HOST:-localhost}" +RETRY_MAX="${RETRY_MAX:-120}" +RETRY_INTERVAL="${RETRY_INTERVAL:-2}" +POSTGIS_READY_DELAY="${POSTGIS_READY_DELAY:-5}" +IMAGE_NAME="${POSTGIS_IMAGE_NAME:-${IMAGE_NAME:-oscar-postgis-arm}}" +POSTGIS_DOCKERFILE="${POSTGIS_DOCKERFILE:-Dockerfile-arm64}" +POSTGIS_PLATFORM="${POSTGIS_PLATFORM:-linux/arm64}" +export SYSTEM_PROFILE CONTAINER_NAME DB_HOST RETRY_MAX RETRY_INTERVAL POSTGIS_READY_DELAY +export IMAGE_NAME POSTGIS_DOCKERFILE POSTGIS_PLATFORM + +require_env() { + _name="$1" + eval "_value=\${${_name}:-}" + if [ -z "$_value" ]; then + echo "Error: ${_name} is not set in .env." + exit 1 + fi +} + +require_env DB_NAME +require_env DB_USER +require_env DB_PASSWORD +require_env DB_PORT + +require_number() { + _name="$1" + eval "_value=\${${_name}:-}" + case "$_value" in + *[!0-9]*|'') + echo "Error: ${_name} must be a number, got '${_value}'." + exit 1 + ;; + esac +} + +require_number DB_PORT +require_number RETRY_MAX +require_number RETRY_INTERVAL +require_number POSTGIS_READY_DELAY + +PROFILE_UPPER="$(printf '%s' "$SYSTEM_PROFILE" | tr '[:lower:]' '[:upper:]')" +case "$PROFILE_UPPER" in + "RPI4") + SYSTEM_PROFILE="RPI4" + PG_SHARED="256MB" + PG_CACHE="1GB" + PG_WORK_MEM="2MB" + PG_MAINT="64MB" + PG_MAX_CONN="75" + ;; + "8GB") + SYSTEM_PROFILE="8GB" + PG_SHARED="512MB" + PG_CACHE="2GB" + PG_WORK_MEM="4MB" + PG_MAINT="128MB" + PG_MAX_CONN="125" + ;; + "16GB") + SYSTEM_PROFILE="16GB" + PG_SHARED="1GB" + PG_CACHE="4GB" + PG_WORK_MEM="8MB" + PG_MAINT="256MB" + PG_MAX_CONN="200" + ;; + "32GB") + SYSTEM_PROFILE="32GB" + PG_SHARED="2GB" + PG_CACHE="8GB" + PG_WORK_MEM="16MB" + PG_MAINT="512MB" + PG_MAX_CONN="300" + ;; + *) + echo "Unknown profile '${SYSTEM_PROFILE}', using 8GB defaults." + SYSTEM_PROFILE="8GB" + PG_SHARED="512MB" + PG_CACHE="2GB" + PG_WORK_MEM="4MB" + PG_MAINT="128MB" + PG_MAX_CONN="125" + ;; +esac + +# Keep sanitized/defaulted values available to the child launch.sh. +export SYSTEM_PROFILE CONTAINER_NAME DB_NAME DB_USER DB_PASSWORD DB_PORT + +mkdir -p "${PROJECT_DIR}/pgdata" + +if ! command -v docker >/dev/null 2>&1; then + echo "Error: Docker is not installed or is not in PATH." + exit 1 +fi + +if ! docker info >/dev/null 2>&1; then + echo "Error: Docker is installed, but the Docker daemon is not running." + echo "Start Docker Desktop, then run this script again." + exit 1 +fi + +POSTGIS_DIR="${PROJECT_DIR}/postgis" +if [ ! -d "$POSTGIS_DIR" ]; then + echo "Error: postgis directory not found in ${PROJECT_DIR}." + exit 1 +fi + +if [ ! -f "${POSTGIS_DIR}/${POSTGIS_DOCKERFILE}" ]; then + echo "Error: ${POSTGIS_DOCKERFILE} not found in ${POSTGIS_DIR}." + exit 1 +fi + +echo "Building PostGIS Docker image for Apple Silicon / ARM64..." +cd "$POSTGIS_DIR" +if [ -n "${POSTGIS_PLATFORM:-}" ]; then + docker build --platform "$POSTGIS_PLATFORM" . --file="$POSTGIS_DOCKERFILE" --tag="$IMAGE_NAME" +else + docker build . --file="$POSTGIS_DOCKERFILE" --tag="$IMAGE_NAME" +fi + +echo "Preparing PostGIS container for profile: ${SYSTEM_PROFILE}" +echo " Image: ${IMAGE_NAME}" +echo " Dockerfile: ${POSTGIS_DOCKERFILE}" +echo " Port: ${DB_PORT}:5432" +echo " Data: ${PROJECT_DIR}/pgdata" + +# Recreate the container so profile/tuning changes always take effect. +# Data persists because pgdata is mounted from the host. +if docker container inspect "$CONTAINER_NAME" >/dev/null 2>&1; then + echo "Removing existing container '${CONTAINER_NAME}' so updated settings take effect..." + docker rm -f "$CONTAINER_NAME" >/dev/null +fi + +echo "Creating new PostGIS container..." +docker run \ + --name "$CONTAINER_NAME" \ + -e POSTGRES_DB="$DB_NAME" \ + -e POSTGRES_USER="$DB_USER" \ + -e POSTGRES_PASSWORD="$DB_PASSWORD" \ + -e DATADIR=/var/lib/postgresql/data \ + -p "${DB_PORT}:5432" \ + -v "${PROJECT_DIR}/pgdata:/var/lib/postgresql/data" \ + -d \ + "$IMAGE_NAME" \ + -c shared_buffers="$PG_SHARED" \ + -c effective_cache_size="$PG_CACHE" \ + -c work_mem="$PG_WORK_MEM" \ + -c maintenance_work_mem="$PG_MAINT" \ + -c max_connections="$PG_MAX_CONN" \ + -c superuser_reserved_connections=10 \ + -c idle_session_timeout=600000 \ + -c log_connections=on \ + -c log_disconnections=on \ + -c wal_buffers=16MB \ + -c random_page_cost=1.1 \ + -c effective_io_concurrency=200 \ + || { echo "Failed to start PostGIS container"; exit 1; } + +echo "Waiting for PostGIS ARM64 to be ready..." +export PGPASSWORD="$DB_PASSWORD" +RETRY_COUNT=0 +until docker exec "$CONTAINER_NAME" pg_isready -U "$DB_USER" -d "$DB_NAME" >/dev/null 2>&1; do + RETRY_COUNT=$((RETRY_COUNT + 1)) + if [ "$RETRY_COUNT" -ge "$RETRY_MAX" ]; then + echo "Error: PostGIS did not become ready after $((RETRY_MAX * RETRY_INTERVAL)) seconds." + echo "Last container logs:" + docker logs --tail 50 "$CONTAINER_NAME" || true + exit 1 + fi + echo "PostGIS not ready yet, retrying..." + sleep "$RETRY_INTERVAL" +done + +echo "PostGIS is ready. Starting OpenSensorHub..." +sleep "$POSTGIS_READY_DELAY" + +OSH_DIR="${PROJECT_DIR}/osh-node-oscar" +if [ ! -d "$OSH_DIR" ]; then + echo "Error: osh-node-oscar directory not found in ${PROJECT_DIR}." + exit 1 +fi + +cd "$OSH_DIR" +if [ ! -f "./launch.sh" ]; then + echo "Error: launch.sh was not found in ${OSH_DIR}." + exit 1 +fi + +if [ ! -x "./launch.sh" ]; then + chmod +x ./launch.sh +fi + +exec ./launch.sh diff --git a/dist/release/launch-all.bat b/dist/release/launch-all.bat old mode 100755 new mode 100644 index 5c93081..bec96ac --- a/dist/release/launch-all.bat +++ b/dist/release/launch-all.bat @@ -1,117 +1,342 @@ @echo off -setlocal enabledelayedexpansion +setlocal EnableExtensions -REM ==== CONFIG ==== -set HOST=localhost -set PORT=5432 -set DB_NAME=gis -set USER=postgres -set RETRY_MAX=20 -set RETRY_INTERVAL=5 -set PROJECT_DIR=%cd% -set CONTAINER_NAME=oscar-postgis-container -set IMAGE_NAME=oscar-postgis +set "PROJECT_DIR=%~dp0" +set "ENV_FILE=%PROJECT_DIR%.env" +set "MATCH_EXPR=com.botts.impl.security.SensorHubWrapper" +set "FORCE_RESTART=%FORCE_RESTART%" +if not defined FORCE_RESTART set "FORCE_RESTART=0" +set "RETRY_MAX=%RETRY_MAX%" +if not defined RETRY_MAX set "RETRY_MAX=120" +set "RETRY_INTERVAL=%RETRY_INTERVAL%" +if not defined RETRY_INTERVAL set "RETRY_INTERVAL=2" +set "POSTGIS_READY_DELAY=%POSTGIS_READY_DELAY%" +if not defined POSTGIS_READY_DELAY set "POSTGIS_READY_DELAY=5" +set "POSTGIS_DOCKERFILE=%POSTGIS_DOCKERFILE%" +if not defined POSTGIS_DOCKERFILE set "POSTGIS_DOCKERFILE=Dockerfile" -echo PROJECT_DIR is: %PROJECT_DIR% +if not exist "%ENV_FILE%" ( + echo Error: .env file not found in "%PROJECT_DIR%". + echo Create it by copying env.template to .env and editing the values. + exit /b 1 +) + +call :load_env "%ENV_FILE%" + +if not defined IMAGE_NAME if defined POSTGIS_IMAGE_NAME set "IMAGE_NAME=%POSTGIS_IMAGE_NAME%" +if not defined IMAGE_NAME set "IMAGE_NAME=oscar-postgis" + +call :check_dependencies +if errorlevel 1 exit /b %ERRORLEVEL% +call :check_existing_oscar +if errorlevel 1 exit /b %ERRORLEVEL% +call :ensure_project_layout +if errorlevel 1 exit /b %ERRORLEVEL% + +if not defined SYSTEM_PROFILE set "SYSTEM_PROFILE=8GB" +if not defined CONTAINER_NAME set "CONTAINER_NAME=oscar-postgis-container" +if not defined DB_HOST set "DB_HOST=localhost" -where docker >nul 2>&1 -if %errorlevel% neq 0 ( - echo ERROR: Docker is not installed or not in PATH. +if not defined DB_NAME ( + echo Error: DB_NAME is not set in .env. exit /b 1 ) +if not defined DB_USER ( + echo Error: DB_USER is not set in .env. + exit /b 1 +) +if not defined DB_PASSWORD ( + echo Error: DB_PASSWORD is not set in .env. + exit /b 1 +) +if not defined DB_PORT ( + echo Error: DB_PORT is not set in .env. + exit /b 1 +) + +call :require_number DB_PORT +if errorlevel 1 exit /b %ERRORLEVEL% +call :require_number RETRY_MAX +if errorlevel 1 exit /b %ERRORLEVEL% +call :require_number RETRY_INTERVAL +if errorlevel 1 exit /b %ERRORLEVEL% +call :require_number POSTGIS_READY_DELAY +if errorlevel 1 exit /b %ERRORLEVEL% + +if /I "%SYSTEM_PROFILE%"=="RPI4" ( + set "SYSTEM_PROFILE=RPI4" + set "PG_SHARED=256MB" + set "PG_CACHE=1GB" + set "PG_WORK_MEM=2MB" + set "PG_MAINT=64MB" + set "PG_MAX_CONN=75" +) else if /I "%SYSTEM_PROFILE%"=="8GB" ( + set "SYSTEM_PROFILE=8GB" + set "PG_SHARED=512MB" + set "PG_CACHE=2GB" + set "PG_WORK_MEM=4MB" + set "PG_MAINT=128MB" + set "PG_MAX_CONN=125" +) else if /I "%SYSTEM_PROFILE%"=="16GB" ( + set "SYSTEM_PROFILE=16GB" + set "PG_SHARED=1GB" + set "PG_CACHE=4GB" + set "PG_WORK_MEM=8MB" + set "PG_MAINT=256MB" + set "PG_MAX_CONN=200" +) else if /I "%SYSTEM_PROFILE%"=="32GB" ( + set "SYSTEM_PROFILE=32GB" + set "PG_SHARED=2GB" + set "PG_CACHE=8GB" + set "PG_WORK_MEM=16MB" + set "PG_MAINT=512MB" + set "PG_MAX_CONN=300" +) else ( + echo Unknown profile '%SYSTEM_PROFILE%', using 8GB defaults. + set "SYSTEM_PROFILE=8GB" + set "PG_SHARED=512MB" + set "PG_CACHE=2GB" + set "PG_WORK_MEM=4MB" + set "PG_MAINT=128MB" + set "PG_MAX_CONN=125" +) -if not exist "%PROJECT_DIR%\pgdata" ( - echo Creating pgdata directory... - mkdir "%PROJECT_DIR%\pgdata" +set "PATH=%JAVA_HOME_DETECTED%\bin;%PATH%" + +if not exist "%PROJECT_DIR%pgdata" mkdir "%PROJECT_DIR%pgdata" >nul 2>nul +if not exist "%PROJECT_DIR%pgdata" ( + echo Error: failed to create pgdata directory. + exit /b 1 ) echo Building PostGIS Docker image... -pushd postgis -docker build . -f Dockerfile -t %IMAGE_NAME% -if %errorlevel% neq 0 ( - echo ERROR: Docker build failed. +pushd "%PROJECT_DIR%postgis" +docker build . --file="%POSTGIS_DOCKERFILE%" --tag="%IMAGE_NAME%" +if errorlevel 1 ( + echo Error: Docker build failed. + popd exit /b 1 ) popd -echo Starting PostGIS container... +echo Preparing PostGIS container for profile: %SYSTEM_PROFILE% +echo Image: %IMAGE_NAME% +echo Port: %DB_PORT%:5432 +echo Data: %PROJECT_DIR%pgdata -for /f "tokens=*" %%i in ('docker ps -a --format "{{.Names}}"') do ( - if "%%i"=="%CONTAINER_NAME%" ( - set CONTAINER_EXISTS=1 +docker container inspect "%CONTAINER_NAME%" >nul 2>&1 +if not errorlevel 1 ( + echo Removing existing container '%CONTAINER_NAME%' so updated settings take effect... + docker rm -f "%CONTAINER_NAME%" >nul + if errorlevel 1 ( + echo Error: failed to remove existing container '%CONTAINER_NAME%'. + exit /b 1 ) ) -for /f "tokens=*" %%i in ('docker ps --format "{{.Names}}"') do ( - if "%%i"=="%CONTAINER_NAME%" ( - set CONTAINER_RUNNING=1 - ) +echo Creating new container... +docker run ^ + --name "%CONTAINER_NAME%" ^ + -e "POSTGRES_DB=%DB_NAME%" ^ + -e "POSTGRES_USER=%DB_USER%" ^ + -e "POSTGRES_PASSWORD=%DB_PASSWORD%" ^ + -p "%DB_PORT%:5432" ^ + -v "%PROJECT_DIR%pgdata:/var/lib/postgresql/data" ^ + -d ^ + "%IMAGE_NAME%" ^ + -c "shared_buffers=%PG_SHARED%" ^ + -c "effective_cache_size=%PG_CACHE%" ^ + -c "work_mem=%PG_WORK_MEM%" ^ + -c "maintenance_work_mem=%PG_MAINT%" ^ + -c "max_connections=%PG_MAX_CONN%" ^ + -c superuser_reserved_connections=10 ^ + -c idle_session_timeout=600000 ^ + -c log_connections=on ^ + -c log_disconnections=on ^ + -c wal_buffers=16MB ^ + -c random_page_cost=1.1 ^ + -c effective_io_concurrency=200 +if errorlevel 1 ( + echo Error: failed to start PostGIS container. + exit /b 1 ) -if defined CONTAINER_EXISTS ( - if defined CONTAINER_RUNNING ( - echo Container already running: %CONTAINER_NAME% - ) else ( - echo Starting existing container: %CONTAINER_NAME% - docker start %CONTAINER_NAME% - ) -) else ( - echo Creating new container: %CONTAINER_NAME% - docker run ^ - --name %CONTAINER_NAME% ^ - -e POSTGRES_DB=%DB_NAME% ^ - -e POSTGRES_USER=%USER% ^ - -e POSTGRES_PASSWORD=postgres ^ - -p %PORT%:5432 ^ - -v "%PROJECT_DIR%\pgdata:/var/lib/postgresql/data" ^ - -d ^ - %IMAGE_NAME% - - if %errorlevel% neq 0 ( - echo ERROR: Failed to start PostGIS container. - exit /b 1 - ) +echo Waiting for PostGIS to be ready... +set "PGPASSWORD=%DB_PASSWORD%" +set /a RETRY_COUNT=0 + +:wait_loop +docker exec "%CONTAINER_NAME%" pg_isready -U "%DB_USER%" -d "%DB_NAME%" >nul 2>&1 +if not errorlevel 1 goto after_wait +set /a RETRY_COUNT+=1 +if %RETRY_COUNT% GEQ %RETRY_MAX% ( + echo Error: PostGIS did not become ready after %RETRY_MAX% attempts. + echo Last container logs: + docker logs --tail 50 "%CONTAINER_NAME%" + exit /b 1 ) +timeout /t %RETRY_INTERVAL% /nobreak >nul +goto wait_loop -echo Waiting for PostGIS database to become ready... +:after_wait +echo PostGIS is ready. +timeout /t %POSTGIS_READY_DELAY% /nobreak >nul -set RETRY_COUNT=0 +cd /d "%PROJECT_DIR%osh-node-oscar" +if errorlevel 1 ( + echo Error: osh-node-oscar directory not found in "%PROJECT_DIR%". + exit /b 1 +) -:wait_loop -docker exec %CONTAINER_NAME% pg_isready -U %USER% -d %DB_NAME% >nul 2>&1 -if %errorlevel% equ 0 ( - echo Received OK from PostGIS. Please wait for initialization... - goto after_wait +if not exist "launch.bat" ( + echo Error: launch.bat not found in "%CD%". + exit /b 1 ) -echo PostGIS not ready yet, retrying... -set /a RETRY_COUNT+=1 +call "launch.bat" +set "LAUNCH_EXIT_CODE=%ERRORLEVEL%" +endlocal & exit /b %LAUNCH_EXIT_CODE% -if %RETRY_COUNT% geq %RETRY_MAX% ( - echo ERROR: PostGIS did not become ready in time. +:check_dependencies +where powershell >nul 2>nul +if errorlevel 1 ( + echo Error: PowerShell is required but was not found on PATH. exit /b 1 ) -timeout /t %RETRY_INTERVAL% >nul -goto wait_loop +where java >nul 2>nul +if errorlevel 1 ( + echo Error: java was not found on PATH. Install OpenJDK 21 or newer. + exit /b 1 +) -:after_wait +where docker >nul 2>nul +if errorlevel 1 ( + echo Error: docker was not found on PATH. Install Docker Desktop and make sure it is running. + exit /b 1 +) -timeout /t 10 >nul +docker info >nul 2>nul +if errorlevel 1 ( + echo Error: Docker is installed, but the Docker daemon is not running. + exit /b 1 +) -echo PostGIS database is ready! +set "JAVA_HOME_LINE=" +for /f "delims=" %%A in ('java -XshowSettings:properties -version 2^>^&1 ^| findstr /c:"java.home ="') do ( + set "JAVA_HOME_LINE=%%A" + goto :deps_java_home_line +) -cd "%PROJECT_DIR%\osh-node-oscar" -if %errorlevel% neq 0 ( - echo ERROR: osh-node-oscar directory not found. +:deps_java_home_line +if not defined JAVA_HOME_LINE ( + echo Error: could not determine java.home from the installed Java runtime. exit /b 1 ) -if exist launch.bat ( - call launch.bat -) else ( - echo WARNING: launch.bat not found. Trying launch.sh through Git Bash... - bash launch.sh +for /f "tokens=1,* delims==" %%A in ("%JAVA_HOME_LINE%") do set "JAVA_HOME_DETECTED=%%B" +for /f "tokens=* delims= " %%A in ("%JAVA_HOME_DETECTED%") do set "JAVA_HOME_DETECTED=%%A" + +if not exist "%JAVA_HOME_DETECTED%\bin\java.exe" ( + echo Error: Java executable not found under "%JAVA_HOME_DETECTED%\bin\java.exe". + exit /b 1 +) +if not exist "%JAVA_HOME_DETECTED%\bin\keytool.exe" ( + echo Error: keytool.exe not found under "%JAVA_HOME_DETECTED%\bin\keytool.exe". + exit /b 1 +) + +set "JAVA_VERSION_LINE=" +for /f "delims=" %%A in ('"%JAVA_HOME_DETECTED%\bin\java.exe" -version 2^>^&1 ^| findstr /r /c:"version \""') do ( + set "JAVA_VERSION_LINE=%%A" + goto :deps_java_version_line +) + +:deps_java_version_line +if not defined JAVA_VERSION_LINE ( + echo Error: could not determine Java version. OpenJDK 21 or newer is required. + exit /b 1 +) + +for /f "tokens=2 delims=\"" %%A in ("%JAVA_VERSION_LINE%") do set "JAVA_VERSION_RAW=%%A" +for /f "tokens=1 delims=." %%A in ("%JAVA_VERSION_RAW%") do set "JAVA_MAJOR=%%A" +if not defined JAVA_MAJOR ( + echo Error: could not parse Java version from "%JAVA_VERSION_LINE%". + exit /b 1 +) +if %JAVA_MAJOR% LSS 21 ( + echo Error: Java 21 or newer is required. Found Java %JAVA_MAJOR%. + exit /b 1 +) +exit /b 0 + +:find_existing_oscar +set "OSCAR_PID=" +for /f %%P in ('powershell -NoProfile -Command "$p = Get-CimInstance Win32_Process ^| Where-Object { $_.Name -match ''^java(\.exe)?$'' -and $_.CommandLine -like ''*com.botts.impl.security.SensorHubWrapper*'' } ^| Select-Object -ExpandProperty ProcessId -First 1; if ($p) { Write-Output $p }"') do set "OSCAR_PID=%%P" +exit /b 0 + +:check_existing_oscar +call :find_existing_oscar +if not defined OSCAR_PID exit /b 0 + +if "%FORCE_RESTART%"=="1" ( + echo Existing OSCAR instance found with PID %OSCAR_PID%. Replacing because FORCE_RESTART=1. + taskkill /PID %OSCAR_PID% /T /F >nul 2>nul + timeout /t 2 /nobreak >nul + call :find_existing_oscar + if defined OSCAR_PID ( + echo Error: could not stop the existing OSCAR instance. + exit /b 1 + ) + exit /b 0 +) + +echo OSCAR is already running with PID %OSCAR_PID%. +echo Stop the running instance first, or set FORCE_RESTART=1 to replace it. +exit /b 1 + +:ensure_project_layout +if not exist "%PROJECT_DIR%postgis" ( + echo Error: postgis directory not found in "%PROJECT_DIR%". + exit /b 1 +) +if not exist "%PROJECT_DIR%postgis\%POSTGIS_DOCKERFILE%" ( + echo Error: %POSTGIS_DOCKERFILE% not found in "%PROJECT_DIR%postgis". + exit /b 1 +) +if not exist "%PROJECT_DIR%osh-node-oscar" ( + echo Error: osh-node-oscar directory not found in "%PROJECT_DIR%". + exit /b 1 +) +if not exist "%PROJECT_DIR%osh-node-oscar\launch.bat" ( + echo Error: launch.bat not found in "%PROJECT_DIR%osh-node-oscar". + exit /b 1 +) +exit /b 0 + +:require_number +call set "VALUE=%%%~1%%" +if not defined VALUE ( + echo Error: %~1 must be a number, got ''. + exit /b 1 +) +for /f "delims=0123456789" %%A in ("%VALUE%") do ( + echo Error: %~1 must be a number, got '%VALUE%'. + exit /b 1 +) +exit /b 0 + +:load_env +for /f "usebackq tokens=1,* delims==" %%A in ("%~1") do ( + set "ENV_NAME=%%A" + set "ENV_VALUE=%%B" + call :set_env_var ) +exit /b 0 -endlocal +:set_env_var +if not defined ENV_NAME exit /b 0 +if "%ENV_NAME:~0,1%"=="#" exit /b 0 +if /I "%ENV_NAME:~0,7%"=="export " set "ENV_NAME=%ENV_NAME:~7%" +set "%ENV_NAME%=%ENV_VALUE%" +exit /b 0 diff --git a/dist/release/launch-all.sh b/dist/release/launch-all.sh old mode 100755 new mode 100644 index 5716c7c..29639c5 --- a/dist/release/launch-all.sh +++ b/dist/release/launch-all.sh @@ -1,78 +1,291 @@ -#!/bin/bash - -HOST="localhost" -PORT="5432" -DB_NAME="gis" -DB_USER="postgres" -RETRY_MAX=20 -RETRY_INTERVAL=5 -PROJECT_DIR="$(pwd)" # Store the original directory -CONTAINER_NAME="oscar-postgis-container" - -#docker rm -f "$CONTAINER_NAME" 2>/dev/null || true - -# Create pgdata directory if needed -if [ ! -d "${PROJECT_DIR}/pgdata" ]; then - echo "Creating pgdata folder..." - mkdir -p "${PROJECT_DIR}/pgdata" -fi +#!/usr/bin/env bash +set -euo pipefail -# Check Docker -if ! command -v docker >/dev/null 2>&1; then - echo "Error: Docker is not installed. Please install Docker first." - exit 1 -fi +PROJECT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="$PROJECT_DIR/.env" +MATCH_EXPR='com.botts.impl.security.SensorHubWrapper' +FORCE_RESTART="${FORCE_RESTART:-0}" +RETRY_MAX="${RETRY_MAX:-120}" +RETRY_INTERVAL="${RETRY_INTERVAL:-2}" +POSTGIS_READY_DELAY="${POSTGIS_READY_DELAY:-5}" +IMAGE_NAME="${POSTGIS_IMAGE_NAME:-${IMAGE_NAME:-oscar-postgis}}" +POSTGIS_DOCKERFILE="${POSTGIS_DOCKERFILE:-Dockerfile}" -echo "Building PostGIS Docker image..." +load_env() { + local env_file="$1" + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ""|"#"*) continue ;; + export\ *) line="${line#export }" ;; + esac + local name="${line%%=*}" + local value="${line#*=}" + value="${value%$'\r'}" + export "${name}=${value}" + done < "$env_file" +} + +require_cmd() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Error: required command not found: $cmd" + exit 1 + fi +} + +get_java_major() { + java -version 2>&1 | awk -F'"' '/version/ { split($2, v, "."); print v[1]; exit }' +} + +check_dependencies() { + require_cmd bash + require_cmd java + require_cmd keytool + require_cmd docker + + if ! docker info >/dev/null 2>&1; then + echo "Error: Docker is installed, but the Docker daemon is not running." + exit 1 + fi + + local java_major + java_major="$(get_java_major || true)" + if [[ -z "$java_major" || ! "$java_major" =~ ^[0-9]+$ ]]; then + echo "Error: could not determine Java version. Java 21 or newer is required." + exit 1 + fi + if [ "$java_major" -lt 21 ]; then + echo "Error: Java 21 or newer is required. Found Java $java_major." + exit 1 + fi +} + +find_existing_oscar_pids() { + pgrep -f "$MATCH_EXPR" || true +} + +stop_existing_oscar() { + local pids="$1" + if [ -z "$pids" ]; then + return 0 + fi + + echo "Stopping existing OSCAR instance(s): $pids" + kill $pids 2>/dev/null || true + + local waited=0 + while [ "$waited" -lt 15 ]; do + sleep 1 + waited=$((waited + 1)) + if [ -z "$(find_existing_oscar_pids)" ]; then + return 0 + fi + done -cd postgis || { echo "Error: postgis directory not found"; exit 1; } + echo "Existing OSCAR instance still running after graceful stop. Forcing stop." + kill -9 $pids 2>/dev/null || true + sleep 1 -# Build PostGIS -docker build . \ - --file=Dockerfile \ - --tag=oscar-postgis + if [ -n "$(find_existing_oscar_pids)" ]; then + echo "Error: unable to stop the existing OSCAR instance." + exit 1 + fi +} + +check_existing_oscar() { + local pids + pids="$(find_existing_oscar_pids)" -echo "Starting PostGIS container..." + if [ -z "$pids" ]; then + return 0 + fi + if [ "$FORCE_RESTART" = "1" ]; then + echo "Existing OSCAR instance found with PID(s): $pids. Replacing because FORCE_RESTART=1." + stop_existing_oscar "$pids" + return 0 + fi -echo "PROJECT_DIR is set to: ${PROJECT_DIR}" + echo "OSCAR is already running with PID(s): $pids." + echo "Stop the running instance first, or set FORCE_RESTART=1 to replace it." + exit 1 +} -if docker ps -a --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}$"; then - # The container exists - if docker ps --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}$"; then - echo "Container already running: ${CONTAINER_NAME}" - else - echo "Starting existing container: ${CONTAINER_NAME}" - docker start "${CONTAINER_NAME}" +require_env() { + local name="$1" + local value="${!name:-}" + if [ -z "$value" ]; then + echo "Error: ${name} is not set in .env." + exit 1 fi -else - echo "Creating new container: ${CONTAINER_NAME}" - docker run \ - --name "$CONTAINER_NAME" \ - -e POSTGRES_DB="$DB_NAME" \ - -e POSTGRES_USER="$DB_USER" \ - -e POSTGRES_PASSWORD="postgres" \ - -p $PORT:5432 \ - -v "${PROJECT_DIR}/pgdata:/var/lib/postgresql/data" \ - -d \ - oscar-postgis || { echo "Failed to start PostGIS container"; exit 1; } +} + +require_number() { + local name="$1" + local value="${!name:-}" + case "$value" in + ''|*[!0-9]*) + echo "Error: ${name} must be a number, got '${value}'." + exit 1 + ;; + esac +} + +ensure_project_layout() { + if [ ! -d "$PROJECT_DIR/postgis" ]; then + echo "Error: postgis directory not found in $PROJECT_DIR" + exit 1 + fi + + if [ ! -f "$PROJECT_DIR/postgis/$POSTGIS_DOCKERFILE" ]; then + echo "Error: $POSTGIS_DOCKERFILE not found in $PROJECT_DIR/postgis" + exit 1 + fi + + if [ ! -d "$PROJECT_DIR/osh-node-oscar" ]; then + echo "Error: osh-node-oscar directory not found in $PROJECT_DIR" + exit 1 + fi + + if [ ! -f "$PROJECT_DIR/osh-node-oscar/launch.sh" ]; then + echo "Error: launch.sh not found in $PROJECT_DIR/osh-node-oscar" + exit 1 + fi + + mkdir -p "$PROJECT_DIR/pgdata" +} + +if [ ! -f "$ENV_FILE" ]; then + echo "Error: .env file not found in $PROJECT_DIR" + echo "Create it by copying env.template to .env and editing the values." + exit 1 fi -# Wait for PostgreSQL/PostGIS to become ready -echo "Waiting for PostGIS (PostgreSQL) to be ready..." +load_env "$ENV_FILE" +check_dependencies +check_existing_oscar +ensure_project_layout + +SYSTEM_PROFILE="${SYSTEM_PROFILE:-8GB}" +CONTAINER_NAME="${CONTAINER_NAME:-oscar-postgis-container}" +DB_HOST="${DB_HOST:-localhost}" +export SYSTEM_PROFILE CONTAINER_NAME DB_HOST RETRY_MAX RETRY_INTERVAL POSTGIS_READY_DELAY IMAGE_NAME POSTGIS_DOCKERFILE -RETRY_COUNT=0 -export PGPASSWORD=postgres # Needed for pg_isready with password +require_env DB_NAME +require_env DB_USER +require_env DB_PASSWORD +require_env DB_PORT -until docker exec "$CONTAINER_NAME" pg_isready -U "$DB_USER" -d "$DB_NAME" > /dev/null 2>&1; do - echo "PostGIS not ready yet, retrying..." - sleep "${RETRY_INTERVAL}" +require_number DB_PORT +require_number RETRY_MAX +require_number RETRY_INTERVAL +require_number POSTGIS_READY_DELAY + +case "${SYSTEM_PROFILE^^}" in + RPI4) + SYSTEM_PROFILE="RPI4" + PG_SHARED="256MB" + PG_CACHE="1GB" + PG_WORK_MEM="2MB" + PG_MAINT="64MB" + PG_MAX_CONN="75" + ;; + 8GB) + SYSTEM_PROFILE="8GB" + PG_SHARED="512MB" + PG_CACHE="2GB" + PG_WORK_MEM="4MB" + PG_MAINT="128MB" + PG_MAX_CONN="125" + ;; + 16GB) + SYSTEM_PROFILE="16GB" + PG_SHARED="1GB" + PG_CACHE="4GB" + PG_WORK_MEM="8MB" + PG_MAINT="256MB" + PG_MAX_CONN="200" + ;; + 32GB) + SYSTEM_PROFILE="32GB" + PG_SHARED="2GB" + PG_CACHE="8GB" + PG_WORK_MEM="16MB" + PG_MAINT="512MB" + PG_MAX_CONN="300" + ;; + *) + echo "Unknown profile '${SYSTEM_PROFILE}', using 8GB defaults." + SYSTEM_PROFILE="8GB" + PG_SHARED="512MB" + PG_CACHE="2GB" + PG_WORK_MEM="4MB" + PG_MAINT="128MB" + PG_MAX_CONN="125" + ;; + esac + +export SYSTEM_PROFILE CONTAINER_NAME DB_NAME DB_USER DB_PASSWORD DB_PORT + +echo "Building PostGIS Docker image..." +( + cd "$PROJECT_DIR/postgis" + docker build . --file="$POSTGIS_DOCKERFILE" --tag="$IMAGE_NAME" +) + +echo "Preparing PostGIS container for profile: $SYSTEM_PROFILE" +echo " Image: $IMAGE_NAME" +echo " Port: ${DB_PORT}:5432" +echo " Data: $PROJECT_DIR/pgdata" + +if docker container inspect "$CONTAINER_NAME" >/dev/null 2>&1; then + echo "Removing existing container '$CONTAINER_NAME' so updated settings take effect..." + docker rm -f "$CONTAINER_NAME" >/dev/null +fi + +echo "Creating new container..." +docker run \ + --name "$CONTAINER_NAME" \ + -e POSTGRES_DB="$DB_NAME" \ + -e POSTGRES_USER="$DB_USER" \ + -e POSTGRES_PASSWORD="$DB_PASSWORD" \ + -p "${DB_PORT}:5432" \ + -v "$PROJECT_DIR/pgdata:/var/lib/postgresql/data" \ + -d \ + "$IMAGE_NAME" \ + -c shared_buffers="$PG_SHARED" \ + -c effective_cache_size="$PG_CACHE" \ + -c work_mem="$PG_WORK_MEM" \ + -c maintenance_work_mem="$PG_MAINT" \ + -c max_connections="$PG_MAX_CONN" \ + -c superuser_reserved_connections=10 \ + -c idle_session_timeout=600000 \ + -c log_connections=on \ + -c log_disconnections=on \ + -c wal_buffers=16MB \ + -c random_page_cost=1.1 \ + -c effective_io_concurrency=200 + +echo "Waiting for PostGIS to be ready..." +export PGPASSWORD="$DB_PASSWORD" +retry_count=0 +until docker exec "$CONTAINER_NAME" pg_isready -U "$DB_USER" -d "$DB_NAME" >/dev/null 2>&1; do + retry_count=$((retry_count + 1)) + if [ "$retry_count" -ge "$RETRY_MAX" ]; then + echo "Error: PostGIS did not become ready after $((RETRY_MAX * RETRY_INTERVAL)) seconds." + echo "Last container logs:" + docker logs --tail 50 "$CONTAINER_NAME" || true + exit 1 + fi + sleep "$RETRY_INTERVAL" done -echo "PostGIS (PostgreSQL) is ready! Please wait for OpenSensorHub to start..." +echo "PostGIS is ready." +sleep "$POSTGIS_READY_DELAY" -sleep 10 +cd "$PROJECT_DIR/osh-node-oscar" +if [ ! -x ./launch.sh ]; then + chmod +x ./launch.sh +fi -# Launch osh-node-oscar -cd "$PROJECT_DIR/osh-node-oscar" || { echo "Error: osh-node-oscar not found"; exit 1; } -./launch.sh \ No newline at end of file +exec ./launch.sh diff --git a/dist/release/launch-all_old.bat b/dist/release/launch-all_old.bat new file mode 100644 index 0000000..646c7ce --- /dev/null +++ b/dist/release/launch-all_old.bat @@ -0,0 +1,192 @@ +@echo off +setlocal EnableExtensions + +rem Resolve project root from this script's location instead of the caller's cwd. +set "PROJECT_DIR=%~dp0" +set "ENV_FILE=%PROJECT_DIR%.env" +set "IMAGE_NAME=oscar-postgis" + +if not exist "%ENV_FILE%" ( + echo Error: .env file not found in "%PROJECT_DIR%". + echo Create it by copying env.template to .env and editing the values. + exit /b 1 +) + +call :load_env "%ENV_FILE%" + +if not defined SYSTEM_PROFILE set "SYSTEM_PROFILE=8GB" +if not defined CONTAINER_NAME set "CONTAINER_NAME=oscar-postgis-container" + +if not defined DB_NAME ( + echo Error: DB_NAME is not set in .env. + exit /b 1 +) +if not defined DB_USER ( + echo Error: DB_USER is not set in .env. + exit /b 1 +) +if not defined DB_PASSWORD ( + echo Error: DB_PASSWORD is not set in .env. + exit /b 1 +) +if not defined DB_PORT ( + echo Error: DB_PORT is not set in .env. + exit /b 1 +) + +if /I "%SYSTEM_PROFILE%"=="RPI4" ( + set "PG_SHARED=256MB" + set "PG_CACHE=1GB" + set "PG_WORK_MEM=2MB" + set "PG_MAINT=64MB" + set "PG_MAX_CONN=75" +) else if /I "%SYSTEM_PROFILE%"=="8GB" ( + set "PG_SHARED=512MB" + set "PG_CACHE=2GB" + set "PG_WORK_MEM=4MB" + set "PG_MAINT=128MB" + set "PG_MAX_CONN=125" +) else if /I "%SYSTEM_PROFILE%"=="16GB" ( + set "PG_SHARED=1GB" + set "PG_CACHE=4GB" + set "PG_WORK_MEM=8MB" + set "PG_MAINT=256MB" + set "PG_MAX_CONN=200" +) else if /I "%SYSTEM_PROFILE%"=="32GB" ( + set "PG_SHARED=2GB" + set "PG_CACHE=8GB" + set "PG_WORK_MEM=16MB" + set "PG_MAINT=512MB" + set "PG_MAX_CONN=300" +) else ( + echo Unknown profile '%SYSTEM_PROFILE%', using 8GB defaults. + set "PG_SHARED=512MB" + set "PG_CACHE=2GB" + set "PG_WORK_MEM=4MB" + set "PG_MAINT=128MB" + set "PG_MAX_CONN=125" +) + +if not exist "%PROJECT_DIR%pgdata" ( + mkdir "%PROJECT_DIR%pgdata" + if errorlevel 1 ( + echo Error: failed to create pgdata directory. + exit /b 1 + ) +) + +where docker >nul 2>&1 +if errorlevel 1 ( + echo Error: Docker is not installed or is not in PATH. + exit /b 1 +) + +echo Building PostGIS Docker image... +if not exist "%PROJECT_DIR%postgis" ( + echo Error: postgis directory not found in "%PROJECT_DIR%". + exit /b 1 +) +pushd "%PROJECT_DIR%postgis" +docker build . --file=Dockerfile --tag=%IMAGE_NAME% +if errorlevel 1 ( + echo Error: Docker build failed. + popd + exit /b 1 +) +popd + +echo Preparing PostGIS container for profile: %SYSTEM_PROFILE% + +rem Recreate the container so profile/tuning changes always take effect. +rem Data persists because pgdata is mounted from the host. +docker container inspect "%CONTAINER_NAME%" >nul 2>&1 +if not errorlevel 1 ( + echo Removing existing container '%CONTAINER_NAME%' so updated settings take effect... + docker rm -f "%CONTAINER_NAME%" >nul + if errorlevel 1 ( + echo Error: failed to remove existing container '%CONTAINER_NAME%'. + exit /b 1 + ) +) + +echo Creating new container... +docker run ^ + --name "%CONTAINER_NAME%" ^ + -e "POSTGRES_DB=%DB_NAME%" ^ + -e "POSTGRES_USER=%DB_USER%" ^ + -e "POSTGRES_PASSWORD=%DB_PASSWORD%" ^ + -p "%DB_PORT%:5432" ^ + -v "%PROJECT_DIR%pgdata:/var/lib/postgresql/data" ^ + -d ^ + %IMAGE_NAME% ^ + -c "shared_buffers=%PG_SHARED%" ^ + -c "effective_cache_size=%PG_CACHE%" ^ + -c "work_mem=%PG_WORK_MEM%" ^ + -c "maintenance_work_mem=%PG_MAINT%" ^ + -c "max_connections=%PG_MAX_CONN%" ^ + -c superuser_reserved_connections=10 ^ + -c idle_session_timeout=600000 ^ + -c log_connections=on ^ + -c log_disconnections=on ^ + -c wal_buffers=16MB ^ + -c random_page_cost=1.1 ^ + -c effective_io_concurrency=200 +if errorlevel 1 ( + echo Error: failed to start PostGIS container. + exit /b 1 +) + +echo Waiting for PostGIS to be ready... +set "PGPASSWORD=%DB_PASSWORD%" + +:wait_loop +docker exec "%CONTAINER_NAME%" pg_isready -U "%DB_USER%" -d "%DB_NAME%" >nul 2>&1 +if not errorlevel 1 goto after_wait +timeout /t 2 /nobreak >nul +goto wait_loop + +:after_wait +echo PostGIS is ready. +timeout /t 5 /nobreak >nul + +cd /d "%PROJECT_DIR%osh-node-oscar" +if errorlevel 1 ( + echo Error: osh-node-oscar directory not found in "%PROJECT_DIR%". + exit /b 1 +) + +if exist "launch.bat" goto run_launch_bat +if exist "launch.sh" goto run_launch_sh +echo Error: neither launch.bat nor launch.sh was found in "%CD%". +exit /b 1 + +:run_launch_bat +call "launch.bat" +set "LAUNCH_EXIT_CODE=%ERRORLEVEL%" +goto launch_done + +:run_launch_sh +echo Warning: launch.bat not found. Trying launch.sh through Bash... +bash "launch.sh" +set "LAUNCH_EXIT_CODE=%ERRORLEVEL%" +goto launch_done + +:launch_done +endlocal & exit /b %LAUNCH_EXIT_CODE% + +:load_env +rem Minimal .env loader for KEY=VALUE lines. Blank lines are ignored by for /f. +rem Lines beginning with # are ignored. Empty values clear the variable. +for /f "usebackq tokens=1,* delims==" %%A in ("%~1") do ( + set "ENV_NAME=%%A" + set "ENV_VALUE=%%B" + call :set_env_var +) +exit /b 0 + +:set_env_var +if not defined ENV_NAME exit /b 0 +if "%ENV_NAME:~0,1%"=="#" exit /b 0 +if /I "%ENV_NAME:~0,7%"=="export " set "ENV_NAME=%ENV_NAME:~7%" +set "%ENV_NAME%=%ENV_VALUE%" +exit /b 0 diff --git a/dist/release/launch-all_old.sh b/dist/release/launch-all_old.sh new file mode 100644 index 0000000..7ab5491 --- /dev/null +++ b/dist/release/launch-all_old.sh @@ -0,0 +1,111 @@ +#!/bin/bash +set -euo pipefail + +PROJECT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="${PROJECT_DIR}/.env" + +if [ ! -f "$ENV_FILE" ]; then + echo "Error: .env file not found in $PROJECT_DIR" + exit 1 +fi + +set -a +. "$ENV_FILE" +set +a + +CONTAINER_NAME="${CONTAINER_NAME:-oscar-postgis-container}" + +case "${SYSTEM_PROFILE:-8GB}" in + "RPI4") + PG_SHARED="256MB" + PG_CACHE="1GB" + PG_WORK_MEM="2MB" + PG_MAINT="64MB" + PG_MAX_CONN="75" + ;; + "8GB") + PG_SHARED="512MB" + PG_CACHE="2GB" + PG_WORK_MEM="4MB" + PG_MAINT="128MB" + PG_MAX_CONN="125" + ;; + "16GB") + PG_SHARED="1GB" + PG_CACHE="4GB" + PG_WORK_MEM="8MB" + PG_MAINT="256MB" + PG_MAX_CONN="200" + ;; + "32GB") + PG_SHARED="2GB" + PG_CACHE="8GB" + PG_WORK_MEM="16MB" + PG_MAINT="512MB" + PG_MAX_CONN="300" + ;; + *) + echo "Unknown profile '${SYSTEM_PROFILE}', using 8GB defaults." + PG_SHARED="512MB" + PG_CACHE="2GB" + PG_WORK_MEM="4MB" + PG_MAINT="128MB" + PG_MAX_CONN="125" + ;; +esac + +mkdir -p "${PROJECT_DIR}/pgdata" + +if ! command -v docker >/dev/null 2>&1; then + echo "Error: Docker is not installed." + exit 1 +fi + +echo "Building PostGIS Docker image..." +cd "${PROJECT_DIR}/postgis" || { echo "Error: postgis directory not found"; exit 1; } +docker build . --file=Dockerfile --tag=oscar-postgis + +echo "Preparing PostGIS container for profile: ${SYSTEM_PROFILE}" + +# Recreate the container so new tuning always applies. +# Data persists because pgdata is mounted from the host. +if docker ps -a --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}$"; then + echo "Removing existing container '${CONTAINER_NAME}' so updated settings take effect..." + docker rm -f "${CONTAINER_NAME}" >/dev/null +fi + +echo "Creating new container..." +docker run \ + --name "${CONTAINER_NAME}" \ + -e POSTGRES_DB="${DB_NAME}" \ + -e POSTGRES_USER="${DB_USER}" \ + -e POSTGRES_PASSWORD="${DB_PASSWORD}" \ + -p "${DB_PORT}:5432" \ + -v "${PROJECT_DIR}/pgdata:/var/lib/postgresql/data" \ + -d \ + oscar-postgis \ + -c shared_buffers="${PG_SHARED}" \ + -c effective_cache_size="${PG_CACHE}" \ + -c work_mem="${PG_WORK_MEM}" \ + -c maintenance_work_mem="${PG_MAINT}" \ + -c max_connections="${PG_MAX_CONN}" \ + -c superuser_reserved_connections=10 \ + -c idle_session_timeout=600000 \ + -c log_connections=on \ + -c log_disconnections=on \ + -c wal_buffers=16MB \ + -c random_page_cost=1.1 \ + -c effective_io_concurrency=200 \ + || { echo "Failed to start PostGIS container"; exit 1; } + +echo "Waiting for PostGIS to be ready..." +export PGPASSWORD="${DB_PASSWORD}" +until docker exec "${CONTAINER_NAME}" pg_isready -U "${DB_USER}" -d "${DB_NAME}" >/dev/null 2>&1; do + sleep 2 +done + +echo "PostGIS is ready." +sleep 5 + +cd "${PROJECT_DIR}/osh-node-oscar" || { echo "Error: osh-node-oscar not found"; exit 1; } +exec ./launch.sh diff --git a/dist/release/monitor-oscar.bat b/dist/release/monitor-oscar.bat new file mode 100644 index 0000000..a325b4e --- /dev/null +++ b/dist/release/monitor-oscar.bat @@ -0,0 +1,249 @@ +@echo off +setlocal EnableExtensions + +if /I "%~1"=="stop" goto :stop_mode + +set "SCRIPT_DIR=%~dp0" +for %%I in ("%SCRIPT_DIR%.") do set "PROJECT_DIR=%%~fI" + +call :timestamp OUT_STAMP +set "OUT_DIR=%PROJECT_DIR%\oscar-monitor-%OUT_STAMP%" +set "ENV_FILE=%PROJECT_DIR%\.env" +set "CONTAINER_NAME=oscar-postgis-container" +set "DB_NAME=gis" +set "DB_USER=postgres" +set "DB_PASSWORD=postgres" +set "MATCH_EXPR=com.botts.impl.security.SensorHubWrapper" +set "INTERVAL=%INTERVAL%" +if not defined INTERVAL set "INTERVAL=60" +set "MAX_WAIT_SECONDS=%MAX_WAIT_SECONDS%" +if not defined MAX_WAIT_SECONDS set "MAX_WAIT_SECONDS=300" +set "JFR_NAME=%JFR_NAME%" +if not defined JFR_NAME set "JFR_NAME=oscar" +set "JFR_MAX_AGE=%JFR_MAX_AGE%" +if not defined JFR_MAX_AGE set "JFR_MAX_AGE=4h" +set "JFR_MAX_SIZE=%JFR_MAX_SIZE%" +if not defined JFR_MAX_SIZE set "JFR_MAX_SIZE=1g" +set "LAUNCH_CMD=%PROJECT_DIR%\launch-all.bat" +set "ATTACH_TO_EXISTING=%ATTACH_TO_EXISTING%" +if not defined ATTACH_TO_EXISTING set "ATTACH_TO_EXISTING=0" +set "FORCE_RESTART=%FORCE_RESTART%" +if not defined FORCE_RESTART set "FORCE_RESTART=0" + +if exist "%ENV_FILE%" call :load_env "%ENV_FILE%" + +call :check_dependencies +if errorlevel 1 exit /b %ERRORLEVEL% + +if not exist "%OUT_DIR%" mkdir "%OUT_DIR%" +echo timestamp,total_sessions,active,idle,idle_in_transaction,max_connections,superuser_reserved_connections,failed_psql>"%OUT_DIR%\db-connection-trend.csv" + +echo %DATE% %TIME% Monitor output: %OUT_DIR% +echo %DATE% %TIME% Launch command: %LAUNCH_CMD% + +call :find_existing_oscar +if defined OSCAR_PID ( + if "%ATTACH_TO_EXISTING%"=="1" ( + set "JVM_PID=%OSCAR_PID%" + set "USE_EXISTING=1" + echo %DATE% %TIME% Attaching to existing OSCAR PID %JVM_PID% + ) else if "%FORCE_RESTART%"=="1" ( + echo %DATE% %TIME% Existing OSCAR instance found with PID %OSCAR_PID%. Replacing because FORCE_RESTART=1. + taskkill /PID %OSCAR_PID% /T /F >nul 2>nul + timeout /t 2 /nobreak >nul + call :find_existing_oscar + if defined OSCAR_PID ( + echo Error: could not stop the existing OSCAR instance. + exit /b 1 + ) + ) else ( + echo OSCAR is already running with PID %OSCAR_PID%. + echo Set ATTACH_TO_EXISTING=1 to monitor the running instance, or FORCE_RESTART=1 to replace it. + exit /b 1 + ) +) + +if not defined USE_EXISTING ( + if not exist "%LAUNCH_CMD%" ( + echo Error: launch command not found: "%LAUNCH_CMD%" + exit /b 1 + ) + + powershell -NoProfile -Command "$p = Start-Process -FilePath 'cmd.exe' -ArgumentList '/c ""%LAUNCH_CMD%""' -RedirectStandardOutput '%OUT_DIR%\launch.stdout.log' -RedirectStandardError '%OUT_DIR%\launch.stderr.log' -PassThru; Write-Output $p.Id" > "%OUT_DIR%\launcher-pid.txt" + for /f "usebackq" %%P in ("%OUT_DIR%\launcher-pid.txt") do set "LAUNCH_PID=%%P" + + echo %DATE% %TIME% Waiting for OSCAR Java process... + set /a WAITED=0 + + :wait_for_jvm + call :find_existing_oscar + if defined OSCAR_PID ( + set "JVM_PID=%OSCAR_PID%" + goto :have_jvm + ) + + if %WAITED% GEQ %MAX_WAIT_SECONDS% ( + echo ERROR: Could not find OSCAR Java PID after waiting. + exit /b 1 + ) + + timeout /t 2 /nobreak >nul + set /a WAITED+=2 + goto :wait_for_jvm +) else ( + >"%OUT_DIR%\launch.stdout.log" type nul + >"%OUT_DIR%\launch.stderr.log" type nul +) + +:have_jvm +echo %JVM_PID%>"%OUT_DIR%\jvm-pid.txt" + +powershell -NoProfile -Command "$p = Get-CimInstance Win32_Process -Filter 'ProcessId=%JVM_PID%'; if($p){ 'Timestamp: ' + (Get-Date -Format o); 'Launcher PID: %LAUNCH_PID%'; 'JVM PID: %JVM_PID%'; ''; 'Command line:'; $p.CommandLine }" > "%OUT_DIR%\process-info.txt" + +if defined JCMD_CMD ( + "%JCMD_CMD%" %JVM_PID% JFR.start name=%JFR_NAME% settings=profile disk=true maxage=%JFR_MAX_AGE% maxsize=%JFR_MAX_SIZE% filename="%OUT_DIR%\%JFR_NAME%.jfr" > "%OUT_DIR%\jfr-start.txt" 2>&1 + "%JCMD_CMD%" %JVM_PID% VM.native_memory baseline > "%OUT_DIR%\nmt-baseline.txt" 2>&1 +) else ( + echo jcmd not available; skipping JFR start and NMT baseline. > "%OUT_DIR%\jcmd-warning.txt" +) + +:loop +call :snapshot +call :process_alive %JVM_PID% JVM_ALIVE +if not defined JVM_ALIVE goto :eof_ok +set "JVM_ALIVE=" +timeout /t %INTERVAL% /nobreak >nul +goto :loop + +:snapshot +call :timestamp SNAP_STAMP +set "SNAP=%OUT_DIR%\%SNAP_STAMP%" +if not exist "%SNAP%" mkdir "%SNAP%" + +echo Collecting snapshot at %SNAP_STAMP% for PID %JVM_PID% +powershell -NoProfile -Command "$p = Get-CimInstance Win32_Process -Filter 'ProcessId=%JVM_PID%'; if($p){$p | Select-Object ProcessId,ParentProcessId,Name,CommandLine | Format-List | Out-String}" > "%SNAP%\process.txt" 2>&1 +powershell -NoProfile -Command "$p=Get-Process -Id %JVM_PID% -ErrorAction SilentlyContinue; if($p){$p | Select-Object Id,ProcessName,Threads,VirtualMemorySize64,WorkingSet64,PrivateMemorySize64,CPU,StartTime | Format-List | Out-String}" > "%SNAP%\powershell-process.txt" 2>&1 +powershell -NoProfile -Command "Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize,FreePhysicalMemory,TotalVirtualMemorySize,FreeVirtualMemory | Format-List | Out-String" > "%SNAP%\memory.txt" 2>&1 +powershell -NoProfile -Command "Get-Counter '\Memory\Committed Bytes','\Memory\Commit Limit','\Paging File(_Total)\%% Usage' | Out-String" > "%SNAP%\counters.txt" 2>&1 +if defined JCMD_CMD ( + "%JCMD_CMD%" %JVM_PID% VM.native_memory summary > "%SNAP%\nmt-summary.txt" 2>&1 + "%JCMD_CMD%" %JVM_PID% GC.heap_info > "%SNAP%\gc-heap-info.txt" 2>&1 + "%JCMD_CMD%" %JVM_PID% Thread.print > "%SNAP%\thread-print.txt" 2>&1 + "%JCMD_CMD%" %JVM_PID% JFR.check > "%SNAP%\jfr-check.txt" 2>&1 +) + +docker ps --filter name=%CONTAINER_NAME% > "%SNAP%\docker-ps.txt" 2>&1 +docker logs --tail 100 %CONTAINER_NAME% > "%SNAP%\docker-logs-tail.txt" 2>&1 +call :db_snapshot "%SNAP%" +exit /b 0 + +:db_snapshot +set "SNAP=%~1" +set "DB_ERR=%SNAP%\db-error.txt" +set "FAILED=0" +set "MAX_CONN=" +set "SUPER_RESERVED=" +set "TOTAL_SESSIONS=" +set "ACTIVE_COUNT=0" +set "IDLE_COUNT=0" +set "IDLE_TX_COUNT=0" + +for /f %%T in ('powershell -NoProfile -Command "Get-Date -Format o"') do set "DB_TS=%%T" + +docker ps --format {{.Names}} | findstr /i /x "%CONTAINER_NAME%" >nul 2>&1 +if errorlevel 1 ( + >"%DB_ERR%" echo Container %CONTAINER_NAME% not running + >>"%OUT_DIR%\db-connection-trend.csv" echo %DB_TS%,,,,,,,1 + exit /b 0 +) + +docker exec -e PGPASSWORD=%DB_PASSWORD% %CONTAINER_NAME% psql -U %DB_USER% -d %DB_NAME% -At -c "show max_connections;" > "%SNAP%\db-max-connections.txt" 2> "%DB_ERR%" +if errorlevel 1 set "FAILED=1" +docker exec -e PGPASSWORD=%DB_PASSWORD% %CONTAINER_NAME% psql -U %DB_USER% -d %DB_NAME% -At -c "show superuser_reserved_connections;" > "%SNAP%\db-superuser-reserved-connections.txt" 2>> "%DB_ERR%" +docker exec -e PGPASSWORD=%DB_PASSWORD% %CONTAINER_NAME% psql -U %DB_USER% -d %DB_NAME% -At -c "select count(*) from pg_stat_activity;" > "%SNAP%\db-total-sessions.txt" 2>> "%DB_ERR%" +docker exec -e PGPASSWORD=%DB_PASSWORD% %CONTAINER_NAME% psql -U %DB_USER% -d %DB_NAME% -At -c "select coalesce(state,''), count(*) from pg_stat_activity group by state order by count(*) desc;" > "%SNAP%\db-by-state.txt" 2>> "%DB_ERR%" +docker exec -e PGPASSWORD=%DB_PASSWORD% %CONTAINER_NAME% psql -U %DB_USER% -d %DB_NAME% -At -c "select coalesce(application_name,''), coalesce(usename,''), coalesce(client_addr::text,''), coalesce(state,''), count(*) from pg_stat_activity group by application_name, usename, client_addr, state order by count(*) desc limit 20;" > "%SNAP%\db-by-app.txt" 2>> "%DB_ERR%" +docker exec -e PGPASSWORD=%DB_PASSWORD% %CONTAINER_NAME% psql -U %DB_USER% -d %DB_NAME% -At -c "select pid, usename, application_name, client_addr, state, backend_start, xact_start, query_start, wait_event_type, wait_event, left(query,120) from pg_stat_activity order by backend_start;" > "%SNAP%\db-activity-detail.txt" 2>> "%DB_ERR%" + +for /f "usebackq" %%A in ("%SNAP%\db-max-connections.txt") do set "MAX_CONN=%%A" +for /f "usebackq" %%A in ("%SNAP%\db-superuser-reserved-connections.txt") do set "SUPER_RESERVED=%%A" +for /f "usebackq" %%A in ("%SNAP%\db-total-sessions.txt") do set "TOTAL_SESSIONS=%%A" +for /f "usebackq tokens=1,2 delims=|" %%A in ("%SNAP%\db-by-state.txt") do ( + if /i "%%A"=="active" set "ACTIVE_COUNT=%%B" + if /i "%%A"=="idle" set "IDLE_COUNT=%%B" + if /i "%%A"=="idle in transaction" set "IDLE_TX_COUNT=%%B" +) +>>"%OUT_DIR%\db-connection-trend.csv" echo %DB_TS%,%TOTAL_SESSIONS%,%ACTIVE_COUNT%,%IDLE_COUNT%,%IDLE_TX_COUNT%,%MAX_CONN%,%SUPER_RESERVED%,%FAILED% +exit /b 0 + +:stop_mode +call :find_existing_oscar +if defined OSCAR_PID taskkill /PID %OSCAR_PID% /T /F >nul 2>&1 +for /f %%C in ('docker ps --filter name=oscar-postgis-container --format {{.Names}}') do docker stop %%C >nul 2>&1 +echo OSCAR stop requested. +exit /b 0 + +:check_dependencies +where powershell >nul 2>nul +if errorlevel 1 ( + echo Error: PowerShell is required but was not found on PATH. + exit /b 1 +) + +where java >nul 2>nul +if errorlevel 1 ( + echo Error: java was not found on PATH. Install OpenJDK 21 or newer. + exit /b 1 +) + +where docker >nul 2>nul +if errorlevel 1 ( + echo Error: docker was not found on PATH. Install Docker Desktop and make sure it is running. + exit /b 1 +) + +set "JAVA_HOME_LINE=" +for /f "delims=" %%A in ('java -XshowSettings:properties -version 2^>^&1 ^| findstr /c:"java.home ="') do ( + set "JAVA_HOME_LINE=%%A" + goto :monitor_java_home_line +) + +:monitor_java_home_line +if not defined JAVA_HOME_LINE exit /b 0 +for /f "tokens=1,* delims==" %%A in ("%JAVA_HOME_LINE%") do set "JAVA_HOME_DETECTED=%%B" +for /f "tokens=* delims= " %%A in ("%JAVA_HOME_DETECTED%") do set "JAVA_HOME_DETECTED=%%A" +if exist "%JAVA_HOME_DETECTED%\bin\jcmd.exe" set "JCMD_CMD=%JAVA_HOME_DETECTED%\bin\jcmd.exe" +exit /b 0 + +:find_existing_oscar +set "OSCAR_PID=" +for /f %%P in ('powershell -NoProfile -Command "$p = Get-CimInstance Win32_Process ^| Where-Object { $_.Name -match ''^java(\.exe)?$'' -and $_.CommandLine -like ''*com.botts.impl.security.SensorHubWrapper*'' } ^| Select-Object -ExpandProperty ProcessId -First 1; if ($p) { Write-Output $p }"') do set "OSCAR_PID=%%P" +exit /b 0 + +:process_alive +set "%~2=" +powershell -NoProfile -Command "if (Get-Process -Id %~1 -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }" >nul 2>nul +if not errorlevel 1 set "%~2=1" +exit /b 0 + +:timestamp +for /f %%A in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd-HHmmss"') do set "%~1=%%A" +exit /b 0 + +:load_env +for /f "usebackq tokens=1,* delims==" %%A in ("%~1") do ( + set "ENV_NAME=%%A" + set "ENV_VALUE=%%B" + call :set_env_var +) +exit /b 0 + +:set_env_var +if not defined ENV_NAME exit /b 0 +if "%ENV_NAME:~0,1%"=="#" exit /b 0 +if /I "%ENV_NAME:~0,7%"=="export " set "ENV_NAME=%ENV_NAME:~7%" +set "%ENV_NAME%=%ENV_VALUE%" +exit /b 0 + +:eof_ok +exit /b 0 diff --git a/dist/release/monitor-oscar.sh b/dist/release/monitor-oscar.sh new file mode 100644 index 0000000..0cb78b6 --- /dev/null +++ b/dist/release/monitor-oscar.sh @@ -0,0 +1,342 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="${PROJECT_DIR:-$SCRIPT_DIR}" +LAUNCH_CMD="${LAUNCH_CMD:-$PROJECT_DIR/launch-all.sh}" +MATCH_EXPR="${MATCH_EXPR:-com.botts.impl.security.SensorHubWrapper}" +INTERVAL="${INTERVAL:-60}" +MAX_WAIT_SECONDS="${MAX_WAIT_SECONDS:-300}" +OUT_DIR="${OUT_DIR:-$PROJECT_DIR/oscar-monitor-$(date +%Y%m%d-%H%M%S)}" +JFR_NAME="${JFR_NAME:-oscar}" +JFR_MAX_AGE="${JFR_MAX_AGE:-4h}" +JFR_MAX_SIZE="${JFR_MAX_SIZE:-1g}" +ENV_FILE="${ENV_FILE:-$PROJECT_DIR/.env}" +ATTACH_TO_EXISTING="${ATTACH_TO_EXISTING:-0}" +FORCE_RESTART="${FORCE_RESTART:-0}" + +mkdir -p "$OUT_DIR" + +CONTAINER_NAME="oscar-postgis-container" +DB_NAME="gis" +DB_USER="postgres" +DB_PASSWORD="postgres" +if [ -f "$ENV_FILE" ]; then + set -a + . "$ENV_FILE" + set +a + CONTAINER_NAME="${CONTAINER_NAME:-oscar-postgis-container}" + DB_NAME="${DB_NAME:-gis}" + DB_USER="${DB_USER:-postgres}" + DB_PASSWORD="${DB_PASSWORD:-postgres}" +fi + +DB_CSV="$OUT_DIR/db-connection-trend.csv" +echo 'timestamp,total_sessions,active,idle,idle_in_transaction,max_connections,superuser_reserved_connections,failed_psql' > "$DB_CSV" + +log() { + printf '%s %s\n' "$(date -Is)" "$*" +} + +require_cmd() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Error: required command not found: $cmd" + exit 1 + fi +} + +get_java_major() { + java -version 2>&1 | awk -F'"' '/version/ { split($2, v, "."); print v[1]; exit }' +} + +check_dependencies() { + require_cmd bash + require_cmd java + require_cmd docker + require_cmd pgrep + + local java_major + java_major="$(get_java_major || true)" + if [[ -z "$java_major" || ! "$java_major" =~ ^[0-9]+$ || "$java_major" -lt 21 ]]; then + echo "Error: Java 21 or newer is required to run OSCAR monitoring." + exit 1 + fi + + if ! command -v jcmd >/dev/null 2>&1; then + log "Warning: jcmd not found. JFR/NMT snapshots will be skipped." + fi +} + +find_existing_oscar_pid() { + pgrep -f "$MATCH_EXPR" | head -n 1 || true +} + +find_all_existing_oscar_pids() { + pgrep -f "$MATCH_EXPR" || true +} + +stop_existing_oscar() { + local pids="$1" + if [ -z "$pids" ]; then + return 0 + fi + + log "Stopping existing OSCAR instance(s): $pids" + kill $pids 2>/dev/null || true + + local waited=0 + while [ "$waited" -lt 15 ]; do + sleep 1 + waited=$((waited + 1)) + if [ -z "$(find_all_existing_oscar_pids)" ]; then + return 0 + fi + done + + log "Force killing existing OSCAR instance(s): $pids" + kill -9 $pids 2>/dev/null || true + sleep 1 + + if [ -n "$(find_all_existing_oscar_pids)" ]; then + echo "Error: unable to stop existing OSCAR instance(s)." + exit 1 + fi +} + +run_db_query() { + local sql="$1" + docker exec -e PGPASSWORD="$DB_PASSWORD" "$CONTAINER_NAME" \ + psql -U "$DB_USER" -d "$DB_NAME" -At -c "$sql" +} + +collect_db_snapshot() { + local d="$1" + local ts failed total active idle idle_tx max_conn super_reserved + ts="$(date -Is)" + failed=0 + + if ! docker ps --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}$"; then + echo "Container ${CONTAINER_NAME} not running" > "$d/db-error.txt" + echo "$ts,,,,,,,1" >> "$DB_CSV" + return 0 + fi + + if run_db_query "show max_connections;" > "$d/db-max-connections.txt" 2> "$d/db-error.txt"; then + run_db_query "show superuser_reserved_connections;" > "$d/db-superuser-reserved-connections.txt" 2>> "$d/db-error.txt" || failed=1 + run_db_query "select count(*) from pg_stat_activity;" > "$d/db-total-sessions.txt" 2>> "$d/db-error.txt" || failed=1 + run_db_query "select coalesce(state,''), count(*) from pg_stat_activity group by state order by count(*) desc;" > "$d/db-by-state.txt" 2>> "$d/db-error.txt" || failed=1 + run_db_query "select coalesce(application_name,''), coalesce(usename,''), coalesce(client_addr::text,''), coalesce(state,''), count(*) from pg_stat_activity group by application_name, usename, client_addr, state order by count(*) desc limit 20;" > "$d/db-by-app.txt" 2>> "$d/db-error.txt" || failed=1 + run_db_query "select pid, usename, application_name, client_addr, state, backend_start, xact_start, query_start, wait_event_type, wait_event, left(query,120) from pg_stat_activity order by backend_start;" > "$d/db-activity-detail.txt" 2>> "$d/db-error.txt" || failed=1 + else + failed=1 + fi + + max_conn="" + super_reserved="" + total="" + active="" + idle="" + idle_tx="" + + [ -f "$d/db-max-connections.txt" ] && max_conn="$(tr -d '[:space:]' < "$d/db-max-connections.txt" | tail -n 1)" + [ -f "$d/db-superuser-reserved-connections.txt" ] && super_reserved="$(tr -d '[:space:]' < "$d/db-superuser-reserved-connections.txt" | tail -n 1)" + [ -f "$d/db-total-sessions.txt" ] && total="$(tr -d '[:space:]' < "$d/db-total-sessions.txt" | tail -n 1)" + if [ -f "$d/db-by-state.txt" ]; then + active="$(awk -F'|' '$1=="active" {gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' "$d/db-by-state.txt" | tail -n 1)" + idle="$(awk -F'|' '$1=="idle" {gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' "$d/db-by-state.txt" | tail -n 1)" + idle_tx="$(awk -F'|' '$1=="idle in transaction" {gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' "$d/db-by-state.txt" | tail -n 1)" + fi + echo "$ts,${total:-},${active:-0},${idle:-0},${idle_tx:-0},${max_conn:-},${super_reserved:-},$failed" >> "$DB_CSV" +} + +dump_once() { + if [ -z "${PID:-}" ] || ! kill -0 "$PID" 2>/dev/null; then + return 0 + fi + + local ts d + ts="$(date +%Y%m%d-%H%M%S)" + d="$OUT_DIR/$ts" + mkdir -p "$d" + + log "Collecting snapshot at $ts for PID $PID" + + ps -p "$PID" -o pid,ppid,user,%cpu,%mem,vsz,rss,etimes,cmd > "$d/ps.txt" 2>&1 || true + [ -r "/proc/$PID/status" ] && cat "/proc/$PID/status" > "$d/proc-status.txt" 2>&1 || true + [ -r "/proc/$PID/smaps_rollup" ] && cat "/proc/$PID/smaps_rollup" > "$d/smaps_rollup.txt" 2>&1 || true + + command -v pmap >/dev/null 2>&1 && pmap -x "$PID" > "$d/pmap-x.txt" 2>&1 || true + command -v free >/dev/null 2>&1 && free -h > "$d/free.txt" 2>&1 || true + [ -r /proc/meminfo ] && cat /proc/meminfo > "$d/meminfo.txt" 2>&1 || true + [ -r /proc/swaps ] && cat /proc/swaps > "$d/swaps.txt" 2>&1 || true + vmstat 1 5 > "$d/vmstat.txt" 2>&1 || true + + if command -v jcmd >/dev/null 2>&1; then + jcmd "$PID" VM.native_memory summary > "$d/nmt-summary.txt" 2>&1 || true + jcmd "$PID" GC.heap_info > "$d/gc-heap-info.txt" 2>&1 || true + jcmd "$PID" Thread.print > "$d/thread-print.txt" 2>&1 || true + jcmd "$PID" JFR.check > "$d/jfr-check.txt" 2>&1 || true + fi + + docker ps --filter "name=$CONTAINER_NAME" > "$d/docker-ps.txt" 2>&1 || true + docker logs --tail 100 "$CONTAINER_NAME" > "$d/docker-logs-tail.txt" 2>&1 || true + collect_db_snapshot "$d" +} + +final_dump() { + if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then + dump_once + if command -v jcmd >/dev/null 2>&1; then + jcmd "$PID" JFR.dump name="$JFR_NAME" filename="$OUT_DIR/${JFR_NAME}-final.jfr" \ + > "$OUT_DIR/jfr-dump-final.txt" 2>&1 || true + fi + fi +} + +stop_stack() { + if [ "${STOPPING:-0}" -eq 1 ]; then + return 0 + fi + STOPPING=1 + + log "Stopping OSCAR stack..." + final_dump + + if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then + log "Stopping JVM PID $PID" + kill "$PID" 2>/dev/null || true + for _ in 1 2 3 4 5 6 7 8 9 10; do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 1 + done + if kill -0 "$PID" 2>/dev/null; then + log "Force killing JVM PID $PID" + kill -9 "$PID" 2>/dev/null || true + fi + fi + + if [ -n "${LAUNCH_PID:-}" ] && kill -0 "$LAUNCH_PID" 2>/dev/null; then + log "Stopping launcher PID $LAUNCH_PID" + kill "$LAUNCH_PID" 2>/dev/null || true + fi + + if docker ps --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}$"; then + log "Stopping container ${CONTAINER_NAME}" + docker stop "$CONTAINER_NAME" > "$OUT_DIR/docker-stop.txt" 2>&1 || true + fi +} + +on_signal() { + log "Received stop signal" + stop_stack + exit 0 +} + +on_exit() { + final_dump +} + +trap on_signal INT TERM +trap on_exit EXIT + +check_dependencies + +log "Monitor output: $OUT_DIR" +log "Launch command: $LAUNCH_CMD" +log "JVM match: $MATCH_EXPR" +log "Container name: $CONTAINER_NAME" +log "Database: $DB_NAME user=$DB_USER" + +if [ ! -x "$LAUNCH_CMD" ] && [ "$ATTACH_TO_EXISTING" != "1" ]; then + echo "Error: launch command is not executable: $LAUNCH_CMD" + exit 1 +fi + +LAUNCH_PID="" +PID="" +STOPPING=0 +USE_EXISTING=0 + +existing_pids="$(find_all_existing_oscar_pids)" +if [ -n "$existing_pids" ]; then + if [ "$ATTACH_TO_EXISTING" = "1" ]; then + PID="$(printf '%s\n' "$existing_pids" | head -n 1)" + USE_EXISTING=1 + log "Attaching monitor to existing OSCAR PID $PID" + elif [ "$FORCE_RESTART" = "1" ]; then + log "Existing OSCAR instance found: $existing_pids" + stop_existing_oscar "$existing_pids" + else + echo "OSCAR is already running with PID(s): $existing_pids" + echo "Set ATTACH_TO_EXISTING=1 to monitor the running instance, or FORCE_RESTART=1 to replace it." + exit 1 + fi +fi + +if [ "$USE_EXISTING" = "0" ]; then + log "Starting OSCAR..." + "$LAUNCH_CMD" > "$OUT_DIR/launch.stdout.log" 2> "$OUT_DIR/launch.stderr.log" & + LAUNCH_PID=$! + echo "$LAUNCH_PID" > "$OUT_DIR/launcher-pid.txt" + + log "Waiting for JVM to appear..." + waited=0 + while true; do + PID="$(find_existing_oscar_pid)" + if [ -n "$PID" ]; then + break + fi + if [ "$waited" -ge "$MAX_WAIT_SECONDS" ]; then + log "Timed out waiting for JVM after ${MAX_WAIT_SECONDS}s" + exit 1 + fi + sleep 2 + waited=$((waited + 2)) + done +else + : > "$OUT_DIR/launch.stdout.log" + : > "$OUT_DIR/launch.stderr.log" +fi + +log "Found JVM PID: $PID" +echo "$PID" > "$OUT_DIR/jvm-pid.txt" + +{ + echo "Timestamp: $(date -Is)" + echo "Launcher PID: ${LAUNCH_PID:-}" + echo "JVM PID: $PID" + echo + echo "Command line:" + tr '\0' ' ' < "/proc/$PID/cmdline" + echo +} > "$OUT_DIR/process-info.txt" + +if command -v jcmd >/dev/null 2>&1; then + log "Starting JFR on PID $PID" + jcmd "$PID" JFR.start \ + name="$JFR_NAME" \ + settings=profile \ + disk=true \ + maxage="$JFR_MAX_AGE" \ + maxsize="$JFR_MAX_SIZE" \ + filename="$OUT_DIR/${JFR_NAME}.jfr" \ + > "$OUT_DIR/jfr-start.txt" 2>&1 || true + + jcmd "$PID" VM.native_memory baseline \ + > "$OUT_DIR/nmt-baseline.txt" 2>&1 || true +fi + +dump_once + +while kill -0 "$PID" 2>/dev/null; do + sleep "$INTERVAL" + dump_once +done + +log "JVM exited." +if [ -n "$LAUNCH_PID" ]; then + wait "$LAUNCH_PID" || true +fi diff --git a/dist/release/monitor-oscar_old.bat b/dist/release/monitor-oscar_old.bat new file mode 100644 index 0000000..55eb8af --- /dev/null +++ b/dist/release/monitor-oscar_old.bat @@ -0,0 +1,129 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion + +if "%~1"=="stop" goto :stop_mode + +set "SCRIPT_DIR=%~dp0" +for %%I in ("%SCRIPT_DIR%.") do set "PROJECT_DIR=%%~fI" +set "OUT_DIR=%PROJECT_DIR%\oscar-monitor-%DATE:~10,4%%DATE:~4,2%%DATE:~7,2%-%TIME:~0,2%%TIME:~3,2%%TIME:~6,2%" +set "OUT_DIR=%OUT_DIR: =0%" +set "ENV_FILE=%PROJECT_DIR%\.env" +set "CONTAINER_NAME=oscar-postgis-container" +set "DB_NAME=gis" +set "DB_USER=postgres" +set "DB_PASSWORD=postgres" +set "MATCH_EXPR=com.botts.impl.security.SensorHubWrapper" +set "INTERVAL=60" +set "JFR_NAME=oscar" +set "JFR_MAX_AGE=4h" +set "JFR_MAX_SIZE=1g" +set "LAUNCH_CMD=%PROJECT_DIR%\launch-all.bat" + +if exist "%ENV_FILE%" ( + for /f "usebackq tokens=1,* delims==" %%A in ("%ENV_FILE%") do ( + if not "%%A"=="" if /i not "%%A:~0,1"=="#" set "%%A=%%B" + ) +) + +if not exist "%OUT_DIR%" mkdir "%OUT_DIR%" +echo timestamp,total_sessions,active,idle,idle_in_transaction,max_connections,superuser_reserved_connections,failed_psql>"%OUT_DIR%\db-connection-trend.csv" + +echo %DATE% %TIME% Monitor output: %OUT_DIR% +echo %DATE% %TIME% Launch command: %LAUNCH_CMD% + +where jcmd >nul 2>nul +if errorlevel 1 echo Warning: jcmd not found. JFR and NMT snapshots will be limited. + +start "OSCAR_LAUNCH" /b cmd /c ""%LAUNCH_CMD%" 1>"%OUT_DIR%\launch.stdout.log" 2>"%OUT_DIR%\launch.stderr.log"" + +:wait_for_jvm +timeout /t 2 /nobreak >nul +for /f "tokens=2 delims=," %%P in ('wmic process where "name='java.exe' and commandline like '%%SensorHubWrapper%%'" get processid^,commandline /format:csv ^| findstr /i SensorHubWrapper') do ( + set "JVM_PID=%%P" + goto :have_jvm +) +goto :wait_for_jvm + +:have_jvm +echo %JVM_PID%>"%OUT_DIR%\jvm-pid.txt" +if exist "%PROJECT_DIR%\monitor.pid" del "%PROJECT_DIR%\monitor.pid" +echo %PROCESS_ID%>"%PROJECT_DIR%\monitor.pid" + +jcmd %JVM_PID% JFR.start name=%JFR_NAME% settings=profile disk=true maxage=%JFR_MAX_AGE% maxsize=%JFR_MAX_SIZE% filename="%OUT_DIR%\%JFR_NAME%.jfr" >"%OUT_DIR%\jfr-start.txt" 2>&1 +jcmd %JVM_PID% VM.native_memory baseline >"%OUT_DIR%\nmt-baseline.txt" 2>&1 + +:loop +call :snapshot +for /f "tokens=2 delims=," %%P in ('wmic process where "processid=%JVM_PID%" get processid /format:csv ^| findstr /r ",[0-9][0-9]*$"') do set "ALIVE=%%P" +if not defined ALIVE goto :eof_ok +set "ALIVE=" +timeout /t %INTERVAL% /nobreak >nul +goto :loop + +:snapshot +set "STAMP=%DATE:~10,4%%DATE:~4,2%%DATE:~7,2%-%TIME:~0,2%%TIME:~3,2%%TIME:~6,2%" +set "STAMP=%STAMP: =0%" +set "SNAP=%OUT_DIR%\%STAMP%" +if not exist "%SNAP%" mkdir "%SNAP%" + +echo Collecting snapshot at %STAMP% for PID %JVM_PID% +wmic process where processid=%JVM_PID% get Name,ParentProcessId,ProcessId,ThreadCount,WorkingSetSize,VirtualSize /format:list >"%SNAP%\wmic-process.txt" 2>&1 +powershell -NoProfile -Command "$p=Get-Process -Id %JVM_PID% -ErrorAction SilentlyContinue; if($p){$p|Select-Object Id,ProcessName,Threads,VirtualMemorySize64,WorkingSet64,PrivateMemorySize64,CPU,StartTime|Format-List|Out-String}" >"%SNAP%\powershell-process.txt" 2>&1 +powershell -NoProfile -Command "Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize,FreePhysicalMemory,TotalVirtualMemorySize,FreeVirtualMemory | Format-List | Out-String" >"%SNAP%\memory.txt" 2>&1 +powershell -NoProfile -Command "Get-Counter '\Memory\Committed Bytes','\Memory\Commit Limit','\Paging File(_Total)\%% Usage' | Out-String" >"%SNAP%\counters.txt" 2>&1 +jcmd %JVM_PID% VM.native_memory summary >"%SNAP%\nmt-summary.txt" 2>&1 +jcmd %JVM_PID% GC.heap_info >"%SNAP%\gc-heap-info.txt" 2>&1 +jcmd %JVM_PID% Thread.print >"%SNAP%\thread-print.txt" 2>&1 +jcmd %JVM_PID% JFR.check >"%SNAP%\jfr-check.txt" 2>&1 + +docker ps --filter name=%CONTAINER_NAME% >"%SNAP%\docker-ps.txt" 2>&1 +docker logs --tail 100 %CONTAINER_NAME% >"%SNAP%\docker-logs-tail.txt" 2>&1 +call :db_snapshot "%SNAP%" +exit /b 0 + +:db_snapshot +set "SNAP=%~1" +set "DB_ERR=%SNAP%\db-error.txt" +set "FAILED=0" +set "MAX_CONN=" +set "SUPER_RESERVED=" +set "TOTAL_SESSIONS=" +set "ACTIVE_COUNT=0" +set "IDLE_COUNT=0" +set "IDLE_TX_COUNT=0" + +docker ps --format {{.Names}} | findstr /i /x "%CONTAINER_NAME%" >nul 2>&1 +if errorlevel 1 ( + >"%DB_ERR%" echo Container %CONTAINER_NAME% not running + >>"%OUT_DIR%\db-connection-trend.csv" echo %DATE%T%TIME%,,,,,,,1 + exit /b 0 +) + +docker exec -e PGPASSWORD=%DB_PASSWORD% %CONTAINER_NAME% psql -U %DB_USER% -d %DB_NAME% -At -c "show max_connections;" >"%SNAP%\db-max-connections.txt" 2>"%DB_ERR%" +if errorlevel 1 set "FAILED=1" +docker exec -e PGPASSWORD=%DB_PASSWORD% %CONTAINER_NAME% psql -U %DB_USER% -d %DB_NAME% -At -c "show superuser_reserved_connections;" >"%SNAP%\db-superuser-reserved-connections.txt" 2>>"%DB_ERR%" +docker exec -e PGPASSWORD=%DB_PASSWORD% %CONTAINER_NAME% psql -U %DB_USER% -d %DB_NAME% -At -c "select count(*) from pg_stat_activity;" >"%SNAP%\db-total-sessions.txt" 2>>"%DB_ERR%" +docker exec -e PGPASSWORD=%DB_PASSWORD% %CONTAINER_NAME% psql -U %DB_USER% -d %DB_NAME% -At -c "select coalesce(state,''), count(*) from pg_stat_activity group by state order by count(*) desc;" >"%SNAP%\db-by-state.txt" 2>>"%DB_ERR%" +docker exec -e PGPASSWORD=%DB_PASSWORD% %CONTAINER_NAME% psql -U %DB_USER% -d %DB_NAME% -At -c "select coalesce(application_name,''), coalesce(usename,''), coalesce(client_addr::text,''), coalesce(state,''), count(*) from pg_stat_activity group by application_name, usename, client_addr, state order by count(*) desc limit 20;" >"%SNAP%\db-by-app.txt" 2>>"%DB_ERR%" + +for /f %%A in (%SNAP%\db-max-connections.txt) do set "MAX_CONN=%%A" +for /f %%A in (%SNAP%\db-superuser-reserved-connections.txt) do set "SUPER_RESERVED=%%A" +for /f %%A in (%SNAP%\db-total-sessions.txt) do set "TOTAL_SESSIONS=%%A" +for /f "tokens=1,2 delims=|" %%A in (%SNAP%\db-by-state.txt) do ( + if /i "%%A"=="active" set "ACTIVE_COUNT=%%B" + if /i "%%A"=="idle" set "IDLE_COUNT=%%B" + if /i "%%A"=="idle in transaction" set "IDLE_TX_COUNT=%%B" +) +>>"%OUT_DIR%\db-connection-trend.csv" echo %DATE%T%TIME%,%TOTAL_SESSIONS%,%ACTIVE_COUNT%,%IDLE_COUNT%,%IDLE_TX_COUNT%,%MAX_CONN%,%SUPER_RESERVED%,%FAILED% +exit /b 0 + +:stop_mode +for /f %%P in (%~dp0monitor.pid) do set "MONPID=%%P" +if defined MONPID taskkill /PID %MONPID% /T /F >nul 2>&1 +for /f "tokens=2 delims=," %%P in ('wmic process where "name='java.exe' and commandline like '%%SensorHubWrapper%%'" get processid^,commandline /format:csv ^| findstr /i SensorHubWrapper') do taskkill /PID %%P /T /F >nul 2>&1 +for /f %%C in ('docker ps --filter name=oscar-postgis-container --format {{.Names}}') do docker stop %%C >nul 2>&1 +echo OSCAR stack stop requested. +exit /b 0 + +:eof_ok +exit /b 0 diff --git a/dist/release/monitor-oscar_old.sh b/dist/release/monitor-oscar_old.sh new file mode 100644 index 0000000..29fe1b1 --- /dev/null +++ b/dist/release/monitor-oscar_old.sh @@ -0,0 +1,254 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="${PROJECT_DIR:-$SCRIPT_DIR}" +LAUNCH_CMD="${LAUNCH_CMD:-$PROJECT_DIR/launch-all.sh}" +MATCH_EXPR="${MATCH_EXPR:-com.botts.impl.security.SensorHubWrapper}" +INTERVAL="${INTERVAL:-60}" +OUT_DIR="${OUT_DIR:-$PROJECT_DIR/oscar-monitor-$(date +%Y%m%d-%H%M%S)}" +JFR_NAME="${JFR_NAME:-oscar}" +JFR_MAX_AGE="${JFR_MAX_AGE:-4h}" +JFR_MAX_SIZE="${JFR_MAX_SIZE:-1g}" +ENV_FILE="${ENV_FILE:-$PROJECT_DIR/.env}" + +mkdir -p "$OUT_DIR" + +CONTAINER_NAME="oscar-postgis-container" +DB_NAME="gis" +DB_USER="postgres" +DB_PASSWORD="postgres" +if [ -f "$ENV_FILE" ]; then + set -a + . "$ENV_FILE" + set +a + CONTAINER_NAME="${CONTAINER_NAME:-oscar-postgis-container}" + DB_NAME="${DB_NAME:-gis}" + DB_USER="${DB_USER:-postgres}" + DB_PASSWORD="${DB_PASSWORD:-postgres}" +fi + +DB_CSV="$OUT_DIR/db-connection-trend.csv" +echo 'timestamp,total_sessions,active,idle,idle_in_transaction,max_connections,superuser_reserved_connections,failed_psql' > "$DB_CSV" + +log() { + printf '%s %s\n' "$(date -Is)" "$*" +} + +log "Monitor output: $OUT_DIR" +log "Launch command: $LAUNCH_CMD" +log "JVM match: $MATCH_EXPR" +log "Container name: $CONTAINER_NAME" +log "Database: $DB_NAME user=$DB_USER" + +if [ ! -x "$LAUNCH_CMD" ]; then + echo "Error: launch command is not executable: $LAUNCH_CMD" + exit 1 +fi + +if ! command -v jcmd >/dev/null 2>&1; then + log "Warning: jcmd not found. JFR/NMT snapshots will be skipped." +fi + +LAUNCH_PID="" +PID="" +STOPPING=0 + +run_db_query() { + local sql="$1" + docker exec -e PGPASSWORD="$DB_PASSWORD" "$CONTAINER_NAME" \ + psql -U "$DB_USER" -d "$DB_NAME" -At -c "$sql" +} + +collect_db_snapshot() { + local d="$1" + local ts failed total active idle idle_tx max_conn super_reserved + ts="$(date -Is)" + failed=0 + + if ! command -v docker >/dev/null 2>&1 || ! docker ps --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}$"; then + echo "Container ${CONTAINER_NAME} not running" > "$d/db-error.txt" + echo "$ts,,,,,,,1" >> "$DB_CSV" + return 0 + fi + + if run_db_query "show max_connections;" > "$d/db-max-connections.txt" 2> "$d/db-error.txt"; then + run_db_query "show superuser_reserved_connections;" > "$d/db-superuser-reserved-connections.txt" 2>> "$d/db-error.txt" || failed=1 + run_db_query "select count(*) from pg_stat_activity;" > "$d/db-total-sessions.txt" 2>> "$d/db-error.txt" || failed=1 + run_db_query "select coalesce(state,''), count(*) from pg_stat_activity group by state order by count(*) desc;" > "$d/db-by-state.txt" 2>> "$d/db-error.txt" || failed=1 + run_db_query "select coalesce(application_name,''), coalesce(usename,''), coalesce(client_addr::text,''), coalesce(state,''), count(*) from pg_stat_activity group by application_name, usename, client_addr, state order by count(*) desc limit 20;" > "$d/db-by-app.txt" 2>> "$d/db-error.txt" || failed=1 + run_db_query "select pid, usename, application_name, client_addr, state, backend_start, xact_start, query_start, wait_event_type, wait_event, left(query,120) from pg_stat_activity order by backend_start;" > "$d/db-activity-detail.txt" 2>> "$d/db-error.txt" || failed=1 + else + failed=1 + fi + + max_conn="" + super_reserved="" + total="" + active="" + idle="" + idle_tx="" + + [ -f "$d/db-max-connections.txt" ] && max_conn="$(tr -d '[:space:]' < "$d/db-max-connections.txt" | tail -n 1)" + [ -f "$d/db-superuser-reserved-connections.txt" ] && super_reserved="$(tr -d '[:space:]' < "$d/db-superuser-reserved-connections.txt" | tail -n 1)" + [ -f "$d/db-total-sessions.txt" ] && total="$(tr -d '[:space:]' < "$d/db-total-sessions.txt" | tail -n 1)" + if [ -f "$d/db-by-state.txt" ]; then + active="$(awk -F'|' '$1=="active" {gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' "$d/db-by-state.txt" | tail -n 1)" + idle="$(awk -F'|' '$1=="idle" {gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' "$d/db-by-state.txt" | tail -n 1)" + idle_tx="$(awk -F'|' '$1=="idle in transaction" {gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' "$d/db-by-state.txt" | tail -n 1)" + fi + echo "$ts,${total:-},${active:-0},${idle:-0},${idle_tx:-0},${max_conn:-},${super_reserved:-},$failed" >> "$DB_CSV" +} + +dump_once() { + if [ -z "${PID:-}" ] || ! kill -0 "$PID" 2>/dev/null; then + return 0 + fi + + local ts d + ts="$(date +%Y%m%d-%H%M%S)" + d="$OUT_DIR/$ts" + mkdir -p "$d" + + log "Collecting snapshot at $ts for PID $PID" + + ps -p "$PID" -o pid,ppid,user,%cpu,%mem,vsz,rss,etimes,cmd > "$d/ps.txt" 2>&1 || true + [ -r "/proc/$PID/status" ] && cat "/proc/$PID/status" > "$d/proc-status.txt" 2>&1 || true + [ -r "/proc/$PID/smaps_rollup" ] && cat "/proc/$PID/smaps_rollup" > "$d/smaps_rollup.txt" 2>&1 || true + + command -v pmap >/dev/null 2>&1 && pmap -x "$PID" > "$d/pmap-x.txt" 2>&1 || true + command -v free >/dev/null 2>&1 && free -h > "$d/free.txt" 2>&1 || true + [ -r /proc/meminfo ] && cat /proc/meminfo > "$d/meminfo.txt" 2>&1 || true + [ -r /proc/swaps ] && cat /proc/swaps > "$d/swaps.txt" 2>&1 || true + vmstat 1 5 > "$d/vmstat.txt" 2>&1 || true + + if command -v jcmd >/dev/null 2>&1; then + jcmd "$PID" VM.native_memory summary > "$d/nmt-summary.txt" 2>&1 || true + jcmd "$PID" GC.heap_info > "$d/gc-heap-info.txt" 2>&1 || true + jcmd "$PID" Thread.print > "$d/thread-print.txt" 2>&1 || true + jcmd "$PID" JFR.check > "$d/jfr-check.txt" 2>&1 || true + fi + + if command -v docker >/dev/null 2>&1; then + docker ps --filter "name=$CONTAINER_NAME" > "$d/docker-ps.txt" 2>&1 || true + docker logs --tail 100 "$CONTAINER_NAME" > "$d/docker-logs-tail.txt" 2>&1 || true + collect_db_snapshot "$d" + fi +} + +final_dump() { + if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then + dump_once + if command -v jcmd >/dev/null 2>&1; then + jcmd "$PID" JFR.dump name="$JFR_NAME" filename="$OUT_DIR/${JFR_NAME}-final.jfr" \ + > "$OUT_DIR/jfr-dump-final.txt" 2>&1 || true + fi + fi +} + +stop_stack() { + if [ "$STOPPING" -eq 1 ]; then + return 0 + fi + STOPPING=1 + + log "Stopping OSCAR stack..." + final_dump + + if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then + log "Stopping JVM PID $PID" + kill "$PID" 2>/dev/null || true + for _ in 1 2 3 4 5 6 7 8 9 10; do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 1 + done + if kill -0 "$PID" 2>/dev/null; then + log "Force killing JVM PID $PID" + kill -9 "$PID" 2>/dev/null || true + fi + fi + + if [ -n "${LAUNCH_PID:-}" ] && kill -0 "$LAUNCH_PID" 2>/dev/null; then + log "Stopping launcher PID $LAUNCH_PID" + kill "$LAUNCH_PID" 2>/dev/null || true + fi + + if command -v docker >/dev/null 2>&1; then + if docker ps --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}$"; then + log "Stopping container ${CONTAINER_NAME}" + docker stop "$CONTAINER_NAME" > "$OUT_DIR/docker-stop.txt" 2>&1 || true + fi + fi +} + +on_signal() { + log "Received stop signal" + stop_stack + exit 0 +} + +on_exit() { + final_dump +} + +trap on_signal INT TERM +trap on_exit EXIT + +log "Starting OSCAR..." +"$LAUNCH_CMD" > "$OUT_DIR/launch.stdout.log" 2> "$OUT_DIR/launch.stderr.log" & +LAUNCH_PID=$! +echo "$LAUNCH_PID" > "$OUT_DIR/launcher-pid.txt" + +log "Waiting for JVM to appear..." +while true; do + PID="$(pgrep -f "$MATCH_EXPR" | head -n 1 || true)" + if [ -n "$PID" ]; then + break + fi + if ! kill -0 "$LAUNCH_PID" 2>/dev/null; then + log "Launch process exited before JVM appeared." + wait "$LAUNCH_PID" || true + exit 1 + fi + sleep 2 +done + +log "Found JVM PID: $PID" +echo "$PID" > "$OUT_DIR/jvm-pid.txt" + +{ + echo "Timestamp: $(date -Is)" + echo "Launcher PID: $LAUNCH_PID" + echo "JVM PID: $PID" + echo + echo "Command line:" + tr '\0' ' ' < "/proc/$PID/cmdline" + echo +} > "$OUT_DIR/process-info.txt" + +if command -v jcmd >/dev/null 2>&1; then + log "Starting JFR on PID $PID" + jcmd "$PID" JFR.start \ + name="$JFR_NAME" \ + settings=profile \ + disk=true \ + maxage="$JFR_MAX_AGE" \ + maxsize="$JFR_MAX_SIZE" \ + filename="$OUT_DIR/${JFR_NAME}.jfr" \ + > "$OUT_DIR/jfr-start.txt" 2>&1 || true + + jcmd "$PID" VM.native_memory baseline \ + > "$OUT_DIR/nmt-baseline.txt" 2>&1 || true +fi + +dump_once + +while kill -0 "$PID" 2>/dev/null; do + sleep "$INTERVAL" + dump_once +done + +log "JVM exited." +wait "$LAUNCH_PID" || true diff --git a/dist/scripts/standard/launch.bat b/dist/scripts/standard/launch.bat old mode 100755 new mode 100644 index 4b50578..489dffa --- a/dist/scripts/standard/launch.bat +++ b/dist/scripts/standard/launch.bat @@ -1,41 +1,244 @@ @echo off -setlocal enabledelayedexpansion +setlocal EnableExtensions +set "SCRIPT_DIR=%~dp0" +set "MATCH_EXPR=com.botts.impl.security.SensorHubWrapper" +set "FORCE_RESTART=%FORCE_RESTART%" +if not defined FORCE_RESTART set "FORCE_RESTART=0" -REM Make sure all the necessary certificates are trusted by the system. -CALL %~dp0load_trusted_certs.bat +set "ENV_FILE=" +if exist "%SCRIPT_DIR%.env" ( + set "ENV_FILE=%SCRIPT_DIR%.env" +) else if exist "%SCRIPT_DIR%..\.env" ( + set "ENV_FILE=%SCRIPT_DIR%..\.env" +) + +if defined ENV_FILE call :load_env "%ENV_FILE%" + +call :check_java +if errorlevel 1 exit /b %ERRORLEVEL% + +call :check_existing_oscar +if errorlevel 1 exit /b %ERRORLEVEL% + +call :ensure_runtime_paths +if errorlevel 1 exit /b %ERRORLEVEL% + +if not defined SYSTEM_PROFILE set "SYSTEM_PROFILE=8GB" + +if /I "%SYSTEM_PROFILE%"=="RPI4" ( + set "JAVA_XMS=512m" + set "JAVA_XMX=1536m" + set "JAVACPP_MAX_BYTES_DEFAULT=512m" + set "JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT=2g" +) else if /I "%SYSTEM_PROFILE%"=="8GB" ( + set "JAVA_XMS=1g" + set "JAVA_XMX=2g" + set "JAVACPP_MAX_BYTES_DEFAULT=1g" + set "JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT=4g" +) else if /I "%SYSTEM_PROFILE%"=="16GB" ( + set "JAVA_XMS=1g" + set "JAVA_XMX=3g" + set "JAVACPP_MAX_BYTES_DEFAULT=2g" + set "JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT=8g" +) else if /I "%SYSTEM_PROFILE%"=="32GB" ( + set "JAVA_XMS=2g" + set "JAVA_XMX=6g" + set "JAVACPP_MAX_BYTES_DEFAULT=4g" + set "JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT=16g" +) else ( + echo Unknown profile '%SYSTEM_PROFILE%', using 8GB defaults. + set "JAVA_XMS=1g" + set "JAVA_XMX=2g" + set "JAVACPP_MAX_BYTES_DEFAULT=1g" + set "JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT=4g" +) + +if not defined JAVACPP_MAX_BYTES set "JAVACPP_MAX_BYTES=%JAVACPP_MAX_BYTES_DEFAULT%" +if not defined JAVACPP_MAX_PHYSICAL_BYTES set "JAVACPP_MAX_PHYSICAL_BYTES=%JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT%" +if not defined JFR_FILENAME set "JFR_FILENAME=%SCRIPT_DIR%oscar.jfr" + +if not defined HOME if defined USERPROFILE set "HOME=%USERPROFILE%" + +set "PATH=%JAVA_HOME_DETECTED%\bin;%PATH%" +set "KEYSTORE=%SCRIPT_DIR%osh-keystore.p12" +set "KEYSTORE_TYPE=PKCS12" +if not defined KEYSTORE_PASSWORD set "KEYSTORE_PASSWORD=atakatak" + +set "TRUSTSTORE=%SCRIPT_DIR%truststore.jks" +set "TRUSTSTORE_TYPE=JKS" +if not defined TRUSTSTORE_PASSWORD set "TRUSTSTORE_PASSWORD=changeit" + +set "INITIAL_ADMIN_PASSWORD_FILE=%SCRIPT_DIR%.s" +if not exist "%INITIAL_ADMIN_PASSWORD_FILE%" if not defined INITIAL_ADMIN_PASSWORD set "INITIAL_ADMIN_PASSWORD=admin" + +echo Starting OSH Node with Profile: %SYSTEM_PROFILE% +echo Heap: %JAVA_XMS% / %JAVA_XMX% +echo JavaCPP maxBytes: %JAVACPP_MAX_BYTES% +echo JavaCPP maxPhysicalBytes: %JAVACPP_MAX_PHYSICAL_BYTES% +echo JFR file: %JFR_FILENAME% + +call "%SCRIPT_DIR%load_trusted_certs.bat" +if errorlevel 1 exit /b %ERRORLEVEL% + +call "%SCRIPT_DIR%set-initial-admin-password.bat" +if errorlevel 1 exit /b %ERRORLEVEL% + +java ^ + -Xms%JAVA_XMS% ^ + -Xmx%JAVA_XMX% ^ + -Xss256k ^ + -XX:ReservedCodeCacheSize=256m ^ + -XX:+UseG1GC ^ + -XX:+HeapDumpOnOutOfMemoryError ^ + -XX:+UnlockDiagnosticVMOptions ^ + -XX:NativeMemoryTracking=summary ^ + "-Dorg.bytedeco.javacpp.maxBytes=%JAVACPP_MAX_BYTES%" ^ + "-Dorg.bytedeco.javacpp.maxPhysicalBytes=%JAVACPP_MAX_PHYSICAL_BYTES%" ^ + -Dorg.bytedeco.javacpp.maxRetries=2 ^ + -Dorg.bytedeco.javacpp.mxbean=true ^ + "-Dlogback.configurationFile=%SCRIPT_DIR%logback.xml" ^ + -cp "%SCRIPT_DIR%lib\*" ^ + "-Djava.system.class.loader=org.sensorhub.utils.NativeClassLoader" ^ + "-Djavax.net.ssl.keyStore=%KEYSTORE%" ^ + "-Djavax.net.ssl.keyStorePassword=%KEYSTORE_PASSWORD%" ^ + "-Djavax.net.ssl.trustStore=%TRUSTSTORE%" ^ + "-Djavax.net.ssl.trustStorePassword=%TRUSTSTORE_PASSWORD%" ^ + "-Djava.library.path=%SCRIPT_DIR%nativelibs" ^ + com.botts.impl.security.SensorHubWrapper "%SCRIPT_DIR%config.json" "%SCRIPT_DIR%db" + +set "JAVA_EXIT_CODE=%ERRORLEVEL%" +endlocal & exit /b %JAVA_EXIT_CODE% + +:check_java +where java >nul 2>nul +if errorlevel 1 ( + echo Error: java was not found on PATH. Install OpenJDK 21 or newer. + exit /b 1 +) + +set "JAVA_HOME_LINE=" +for /f "delims=" %%A in ('java -XshowSettings:properties -version 2^>^&1 ^| findstr /c:"java.home ="') do ( + set "JAVA_HOME_LINE=%%A" + goto :check_java_home_line +) + +:check_java_home_line +if not defined JAVA_HOME_LINE ( + echo Error: could not determine java.home from the installed Java runtime. + exit /b 1 +) + +for /f "tokens=1,* delims==" %%A in ("%JAVA_HOME_LINE%") do set "JAVA_HOME_DETECTED=%%B" +for /f "tokens=* delims= " %%A in ("%JAVA_HOME_DETECTED%") do set "JAVA_HOME_DETECTED=%%A" -set KEYSTORE=.\osh-keystore.p12 -set KEYSTORE_TYPE=PKCS12 -set KEYSTORE_PASSWORD=atakatak +if not exist "%JAVA_HOME_DETECTED%\bin\java.exe" ( + echo Error: Java executable not found under "%JAVA_HOME_DETECTED%\bin\java.exe". + exit /b 1 +) -set TRUSTSTORE=.\truststore.jks -set TRUSTSTORE_TYPE=JKS -set TRUSTSTORE_PASSWORD=changeit +if not exist "%JAVA_HOME_DETECTED%\bin\keytool.exe" ( + echo Error: keytool.exe not found under "%JAVA_HOME_DETECTED%\bin\keytool.exe". + exit /b 1 +) -set INITIAL_ADMIN_PASSWORD_FILE=.\.s +set "JAVA_VERSION_LINE=" +for /f "delims=" %%A in ('"%JAVA_HOME_DETECTED%\bin\java.exe" -version 2^>^&1 ^| findstr /r /c:"version \""') do ( + set "JAVA_VERSION_LINE=%%A" + goto :check_java_version_line +) +:check_java_version_line +if not defined JAVA_VERSION_LINE ( + echo Error: could not determine Java version. OpenJDK 21 or newer is required. + exit /b 1 +) + +for /f "tokens=2 delims=\"" %%A in ("%JAVA_VERSION_LINE%") do set "JAVA_VERSION_RAW=%%A" +for /f "tokens=1 delims=." %%A in ("%JAVA_VERSION_RAW%") do set "JAVA_MAJOR=%%A" + +if not defined JAVA_MAJOR ( + echo Error: could not parse Java version from "%JAVA_VERSION_LINE%". + exit /b 1 +) -REM Check if INITIAL_ADMIN_PASSWORD_FILE and INITIAL_ADMIN_PASSWORD are empty -REM Set default password if neither is provided -if "%INITIAL_ADMIN_PASSWORD_FILE%"=="" if "%INITIAL_ADMIN_PASSWORD%"=="" ( - set INITIAL_ADMIN_PASSWORD=admin +if %JAVA_MAJOR% LSS 21 ( + echo Error: Java 21 or newer is required. Found Java %JAVA_MAJOR%. + exit /b 1 ) -REM Call the next batch script to handle setting the initial admin password -CALL "%SCRIPT_DIR%set-initial-admin-password.bat" +exit /b 0 -REM Start the node -java -Xms6g -Xmx6g -Xss256k -XX:ReservedCodeCacheSize=512m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError ^ - -Dlogback.configurationFile=./logback.xml ^ - -cp "lib/*" ^ - -Djava.system.class.loader="org.sensorhub.utils.NativeClassLoader" ^ - -Djavax.net.ssl.keyStore="./osh-keystore.p12" ^ - -Djavax.net.ssl.keyStorePassword="atakatak" ^ - -Djavax.net.ssl.trustStore="%~dp0trustStore.jks" ^ - -Djavax.net.ssl.trustStorePassword="changeit" ^ - -Djava.library.path="./nativelibs" ^ - com.botts.impl.security.SensorHubWrapper config.json db +:find_existing_oscar +set "OSCAR_PID=" +for /f %%P in ('powershell -NoProfile -Command "$p = Get-CimInstance Win32_Process ^| Where-Object { $_.Name -match ''^java(\.exe)?$'' -and $_.CommandLine -like ''*com.botts.impl.security.SensorHubWrapper*'' } ^| Select-Object -ExpandProperty ProcessId -First 1; if ($p) { Write-Output $p }"') do set "OSCAR_PID=%%P" +exit /b 0 + +:check_existing_oscar +call :find_existing_oscar +if not defined OSCAR_PID exit /b 0 + +if "%FORCE_RESTART%"=="1" ( + echo Existing OSCAR instance found with PID %OSCAR_PID%. Replacing because FORCE_RESTART=1. + taskkill /PID %OSCAR_PID% /T /F >nul 2>nul + timeout /t 2 /nobreak >nul + call :find_existing_oscar + if defined OSCAR_PID ( + echo Error: could not stop the existing OSCAR instance. + exit /b 1 + ) + exit /b 0 +) + +echo OSCAR is already running with PID %OSCAR_PID%. +echo Close the existing instance first, or set FORCE_RESTART=1 to replace it. +exit /b 1 + +:ensure_runtime_paths +if not exist "%SCRIPT_DIR%load_trusted_certs.bat" ( + echo Error: load_trusted_certs.bat not found in "%SCRIPT_DIR%". + exit /b 1 +) +if not exist "%SCRIPT_DIR%set-initial-admin-password.bat" ( + echo Error: set-initial-admin-password.bat not found in "%SCRIPT_DIR%". + exit /b 1 +) + +if not exist "%SCRIPT_DIR%config.json" ( + echo Error: missing config file: "%SCRIPT_DIR%config.json". + exit /b 1 +) + +if not exist "%SCRIPT_DIR%lib" ( + echo Error: missing library directory: "%SCRIPT_DIR%lib". + exit /b 1 +) + +if not exist "%SCRIPT_DIR%nativelibs" ( + echo Error: missing native library directory: "%SCRIPT_DIR%nativelibs". + exit /b 1 +) + +if not exist "%SCRIPT_DIR%db" mkdir "%SCRIPT_DIR%db" >nul 2>nul +if not exist "%SCRIPT_DIR%db" ( + echo Error: could not create database directory: "%SCRIPT_DIR%db". + exit /b 1 +) + +exit /b 0 + +:load_env +for /f "usebackq tokens=1,* delims==" %%A in ("%~1") do ( + set "ENV_NAME=%%A" + set "ENV_VALUE=%%B" + call :set_env_var +) +exit /b 0 -endlocal +:set_env_var +if not defined ENV_NAME exit /b 0 +if "%ENV_NAME:~0,1%"=="#" exit /b 0 +if /I "%ENV_NAME:~0,7%"=="export " set "ENV_NAME=%ENV_NAME:~7%" +set "%ENV_NAME%=%ENV_VALUE%" +exit /b 0 diff --git a/dist/scripts/standard/launch.sh b/dist/scripts/standard/launch.sh old mode 100755 new mode 100644 index f9d4094..eadc411 --- a/dist/scripts/standard/launch.sh +++ b/dist/scripts/standard/launch.sh @@ -1,37 +1,229 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -# Make sure all the necessary certificates are trusted by the system. -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -"$SCRIPT_DIR/load_trusted_certs.sh" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MATCH_EXPR='com.botts.impl.security.SensorHubWrapper' - export KEYSTORE="./osh-keystore.p12" - export KEYSTORE_TYPE=PKCS12 - export KEYSTORE_PASSWORD="atakatak" +load_env() { + local env_file="$1" + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ""|"#"*) continue ;; + export\ *) line="${line#export }" ;; + esac + local name="${line%%=*}" + local value="${line#*=}" + export "${name}=${value}" + done < "$env_file" +} - export TRUSTSTORE="./truststore.jks" - export TRUSTSTORE_TYPE=JKS - export TRUSTSTORE_PASSWORD="changeit" - export INITIAL_ADMIN_PASSWORD_FILE="./.s" +require_cmd() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Error: required command not found: $cmd" + exit 1 + fi +} +get_java_major() { + java -version 2>&1 | awk -F'"' '/version/ { split($2, v, "."); print v[1]; exit }' +} -# After copying the default configuration file, also look to see if they -# specified what they want the initial admin user's password to be, either -# as a secret file or by providing it as an environment variable. -if [ -z "$INITIAL_ADMIN_PASSWORD_FILE" ] && [ -z "$INITIAL_ADMIN_PASSWORD" ]; then - export INITIAL_ADMIN_PASSWORD=admin +check_dependencies() { + require_cmd bash + require_cmd java + require_cmd keytool + + local java_major + java_major="$(get_java_major || true)" + if [[ -z "$java_major" || ! "$java_major" =~ ^[0-9]+$ ]]; then + echo "Error: could not determine Java version. Java 21 or newer is required." + exit 1 + fi + if [ "$java_major" -lt 21 ]; then + echo "Error: Java 21 or newer is required. Found Java $java_major." + exit 1 + fi +} + +find_existing_oscar_pids() { + pgrep -f "$MATCH_EXPR" || true +} + +stop_existing_oscar() { + local pids="$1" + if [ -z "$pids" ]; then + return 0 + fi + + echo "Stopping existing OSCAR instance(s): $pids" + kill $pids 2>/dev/null || true + + local waited=0 + while [ "$waited" -lt 15 ]; do + sleep 1 + waited=$((waited + 1)) + if [ -z "$(find_existing_oscar_pids)" ]; then + return 0 + fi + done + + echo "Existing OSCAR instance still running after graceful stop. Forcing stop." + kill -9 $pids 2>/dev/null || true + sleep 1 + + if [ -n "$(find_existing_oscar_pids)" ]; then + echo "Error: unable to stop the existing OSCAR instance." + exit 1 + fi +} + +check_existing_oscar() { + local pids + pids="$(find_existing_oscar_pids)" + + if [ -z "$pids" ]; then + return 0 + fi + + if [ "${FORCE_RESTART:-0}" = "1" ]; then + echo "Existing OSCAR instance found with PID(s): $pids. Replacing because FORCE_RESTART=1." + stop_existing_oscar "$pids" + return 0 + fi + + echo "OSCAR is already running with PID(s): $pids." + echo "Run stop-all.sh first, or set FORCE_RESTART=1 to replace the existing OSCAR process." + exit 1 +} + +ensure_runtime_paths() { + if [ ! -f "$SCRIPT_DIR/config.json" ]; then + echo "Error: missing config file: $SCRIPT_DIR/config.json" + exit 1 + fi + + if [ ! -d "$SCRIPT_DIR/lib" ]; then + echo "Error: missing library directory: $SCRIPT_DIR/lib" + exit 1 + fi + + if [ ! -d "$SCRIPT_DIR/nativelibs" ]; then + echo "Error: missing native library directory: $SCRIPT_DIR/nativelibs" + exit 1 + fi + + mkdir -p "$SCRIPT_DIR/db" + + if [ ! -f "$SCRIPT_DIR/load_trusted_certs.sh" ]; then + echo "Error: load_trusted_certs.sh not found in $SCRIPT_DIR" + exit 1 + fi + + if [ ! -f "$SCRIPT_DIR/set-initial-admin-password.sh" ]; then + echo "Error: set-initial-admin-password.sh not found in $SCRIPT_DIR" + exit 1 + fi +} + +ENV_FILE="" +if [ -f "$SCRIPT_DIR/.env" ]; then + ENV_FILE="$SCRIPT_DIR/.env" +elif [ -f "$SCRIPT_DIR/../.env" ]; then + ENV_FILE="$SCRIPT_DIR/../.env" +fi + +if [ -n "$ENV_FILE" ]; then + load_env "$ENV_FILE" +fi + +check_dependencies +check_existing_oscar +ensure_runtime_paths + +SYSTEM_PROFILE="${SYSTEM_PROFILE:-8GB}" + +case "$SYSTEM_PROFILE" in + RPI4) + JAVA_XMS="512m" + JAVA_XMX="1536m" + JAVACPP_MAX_BYTES_DEFAULT="512m" + JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT="2g" + ;; + 8GB) + JAVA_XMS="1g" + JAVA_XMX="2g" + JAVACPP_MAX_BYTES_DEFAULT="1g" + JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT="4g" + ;; + 16GB) + JAVA_XMS="1g" + JAVA_XMX="3g" + JAVACPP_MAX_BYTES_DEFAULT="2g" + JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT="8g" + ;; + 32GB) + JAVA_XMS="2g" + JAVA_XMX="6g" + JAVACPP_MAX_BYTES_DEFAULT="4g" + JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT="16g" + ;; + *) + echo "Unknown profile '$SYSTEM_PROFILE', using 8GB defaults." + JAVA_XMS="1g" + JAVA_XMX="2g" + JAVACPP_MAX_BYTES_DEFAULT="1g" + JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT="4g" + ;; +esac + +: "${JAVACPP_MAX_BYTES:=$JAVACPP_MAX_BYTES_DEFAULT}" +: "${JAVACPP_MAX_PHYSICAL_BYTES:=$JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT}" +: "${JFR_FILENAME:=$SCRIPT_DIR/oscar.jfr}" + +mkdir -p "$(dirname "$JFR_FILENAME")" + +export KEYSTORE="${KEYSTORE:-$SCRIPT_DIR/osh-keystore.p12}" +export KEYSTORE_TYPE="${KEYSTORE_TYPE:-PKCS12}" +export KEYSTORE_PASSWORD="${KEYSTORE_PASSWORD:-atakatak}" + +export TRUSTSTORE="${TRUSTSTORE:-$SCRIPT_DIR/truststore.jks}" +export TRUSTSTORE_TYPE="${TRUSTSTORE_TYPE:-JKS}" +export TRUSTSTORE_PASSWORD="${TRUSTSTORE_PASSWORD:-changeit}" + +export INITIAL_ADMIN_PASSWORD_FILE="${INITIAL_ADMIN_PASSWORD_FILE:-$SCRIPT_DIR/.s}" +if [ ! -f "$INITIAL_ADMIN_PASSWORD_FILE" ] && [ -z "${INITIAL_ADMIN_PASSWORD:-}" ]; then + export INITIAL_ADMIN_PASSWORD="admin" +fi + +if [ -z "${HOME:-}" ] && [ -n "${USER:-}" ]; then + export HOME="/home/${USER}" fi -"$SCRIPT_DIR/set-initial-admin-password.sh" +echo "Starting OSH Node with Profile: $SYSTEM_PROFILE" +echo " Heap: $JAVA_XMS / $JAVA_XMX" +echo " JavaCPP maxBytes: $JAVACPP_MAX_BYTES" +echo " JavaCPP maxPhysicalBytes: $JAVACPP_MAX_PHYSICAL_BYTES" +echo " JFR file: $JFR_FILENAME" +bash "$SCRIPT_DIR/load_trusted_certs.sh" +bash "$SCRIPT_DIR/set-initial-admin-password.sh" -# Start the node -java -Xms6g -Xmx6g -Xss256k -XX:ReservedCodeCacheSize=512m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError \ - -Dlogback.configurationFile=./logback.xml \ - -cp "lib/*" \ - -Djava.system.class.loader="org.sensorhub.utils.NativeClassLoader" \ - -Djavax.net.ssl.keyStore="./osh-keystore.p12" \ - -Djavax.net.ssl.keyStorePassword="atakatak" \ - -Djavax.net.ssl.trustStore="$SCRIPT_DIR/trustStore.jks" \ - -Djavax.net.ssl.trustStorePassword="changeit" \ - -Djava.library.path="./nativelibs" \ - com.botts.impl.security.SensorHubWrapper ./config.json ./db +exec java \ + -Xms"$JAVA_XMS" \ + -Xmx"$JAVA_XMX" \ + -Xss256k \ + -XX:ReservedCodeCacheSize=256m \ + -XX:+UseG1GC \ + -XX:+HeapDumpOnOutOfMemoryError \ + -XX:+UnlockDiagnosticVMOptions \ + -XX:NativeMemoryTracking=summary \ + "-Dorg.bytedeco.javacpp.maxBytes=$JAVACPP_MAX_BYTES" \ + "-Dorg.bytedeco.javacpp.maxPhysicalBytes=$JAVACPP_MAX_PHYSICAL_BYTES" \ + -Dorg.bytedeco.javacpp.maxRetries=2 \ + -Dorg.bytedeco.javacpp.mxbean=true \ + "-Dlogback.configurationFile=$SCRIPT_DIR/logback.xml" \ + -cp "$SCRIPT_DIR/lib/*" \ + "-Djava.system.class.loader=org.sensorhub.utils.NativeClassLoader" \ + "-Djava.library.path=$SCRIPT_DIR/nativelibs" \ + com.botts.impl.security.SensorHubWrapper "$SCRIPT_DIR/config.json" "$SCRIPT_DIR/db" diff --git a/dist/scripts/standard/launch_old.bat b/dist/scripts/standard/launch_old.bat new file mode 100644 index 0000000..b7d52fc --- /dev/null +++ b/dist/scripts/standard/launch_old.bat @@ -0,0 +1,144 @@ +@echo off +setlocal EnableExtensions + +rem Resolve this script's directory. %~dp0 includes the trailing backslash. +set "SCRIPT_DIR=%~dp0" + +rem Load .env from this directory, or from the parent directory when this script +rem is launched from osh-node-oscar under the project root. +set "ENV_FILE=" +if exist "%SCRIPT_DIR%.env" ( + set "ENV_FILE=%SCRIPT_DIR%.env" +) else if exist "%SCRIPT_DIR%..\.env" ( + set "ENV_FILE=%SCRIPT_DIR%..\.env" +) + +if defined ENV_FILE ( + call :load_env "%ENV_FILE%" + echo AFTER load_env ERR=%ERRORLEVEL% +) + +rem Pick Java and JavaCPP defaults from SYSTEM_PROFILE. +if not defined SYSTEM_PROFILE set "SYSTEM_PROFILE=8GB" + +if /I "%SYSTEM_PROFILE%"=="RPI4" ( + set "JAVA_XMS=512m" + set "JAVA_XMX=1536m" + set "JAVACPP_MAX_BYTES_DEFAULT=512m" + set "JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT=2g" +) else if /I "%SYSTEM_PROFILE%"=="8GB" ( + set "JAVA_XMS=1g" + set "JAVA_XMX=2g" + set "JAVACPP_MAX_BYTES_DEFAULT=1g" + set "JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT=4g" +) else if /I "%SYSTEM_PROFILE%"=="16GB" ( + set "JAVA_XMS=1g" + set "JAVA_XMX=3g" + set "JAVACPP_MAX_BYTES_DEFAULT=2g" + set "JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT=8g" +) else if /I "%SYSTEM_PROFILE%"=="32GB" ( + set "JAVA_XMS=2g" + set "JAVA_XMX=6g" + set "JAVACPP_MAX_BYTES_DEFAULT=4g" + set "JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT=16g" +) else ( + echo Unknown profile '%SYSTEM_PROFILE%', using 8GB defaults. + set "JAVA_XMS=1g" + set "JAVA_XMX=2g" + set "JAVACPP_MAX_BYTES_DEFAULT=1g" + set "JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT=4g" +) + +if not defined JAVACPP_MAX_BYTES set "JAVACPP_MAX_BYTES=%JAVACPP_MAX_BYTES_DEFAULT%" +if not defined JAVACPP_MAX_PHYSICAL_BYTES set "JAVACPP_MAX_PHYSICAL_BYTES=%JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT%" +if not defined JFR_FILENAME set "JFR_FILENAME=%SCRIPT_DIR%oscar.jfr" + +echo Starting OSH Node with Profile: %SYSTEM_PROFILE% +echo Heap: %JAVA_XMS% / %JAVA_XMX% +echo JavaCPP maxBytes: %JAVACPP_MAX_BYTES% +echo JavaCPP maxPhysicalBytes: %JAVACPP_MAX_PHYSICAL_BYTES% +echo JFR file: %JFR_FILENAME% + +rem Make sure all the necessary certificates are trusted by the system. +if not exist "%SCRIPT_DIR%load_trusted_certs.bat" ( + echo Error: load_trusted_certs.bat not found in "%SCRIPT_DIR%". + exit /b 1 +) +call "%SCRIPT_DIR%load_trusted_certs.bat" +echo AFTER load_trusted_certs ERR=%ERRORLEVEL% +if errorlevel 1 exit /b %ERRORLEVEL% + +set "KEYSTORE=%SCRIPT_DIR%osh-keystore.p12" +set "KEYSTORE_TYPE=PKCS12" +if not defined KEYSTORE_PASSWORD set "KEYSTORE_PASSWORD=atakatak" + +set "TRUSTSTORE=%SCRIPT_DIR%truststore.jks" +set "TRUSTSTORE_TYPE=JKS" +if not defined TRUSTSTORE_PASSWORD set "TRUSTSTORE_PASSWORD=changeit" + +set "INITIAL_ADMIN_PASSWORD_FILE=%SCRIPT_DIR%.s" + +rem If no secret file exists and no env var was supplied, use the dev default. +if not exist "%INITIAL_ADMIN_PASSWORD_FILE%" if not defined INITIAL_ADMIN_PASSWORD set "INITIAL_ADMIN_PASSWORD=admin" + +if not exist "%SCRIPT_DIR%set-initial-admin-password.bat" ( + echo Error: set-initial-admin-password.bat not found in "%SCRIPT_DIR%". + exit /b 1 +) +call "%SCRIPT_DIR%set-initial-admin-password.bat" +echo AFTER set-initial-admin-password ERR=%ERRORLEVEL% +if errorlevel 1 exit /b %ERRORLEVEL% + +echo BEFORE JAVA +where java +echo KEYSTORE=%KEYSTORE% +echo TRUSTSTORE=%TRUSTSTORE% +echo INITIAL_ADMIN_PASSWORD_FILE=%INITIAL_ADMIN_PASSWORD_FILE% +echo SCRIPT_DIR=%SCRIPT_DIR% +echo CONFIG=%SCRIPT_DIR%config.json +echo DBDIR=%SCRIPT_DIR%db +echo NATIVELIBS=%SCRIPT_DIR%nativelibs + +java ^ + -Xms%JAVA_XMS% ^ + -Xmx%JAVA_XMX% ^ + -Xss256k ^ + -XX:ReservedCodeCacheSize=256m ^ + -XX:+UseG1GC ^ + -XX:+HeapDumpOnOutOfMemoryError ^ + -XX:+UnlockDiagnosticVMOptions ^ + -XX:NativeMemoryTracking=summary ^ + "-Dorg.bytedeco.javacpp.maxBytes=%JAVACPP_MAX_BYTES%" ^ + "-Dorg.bytedeco.javacpp.maxPhysicalBytes=%JAVACPP_MAX_PHYSICAL_BYTES%" ^ + -Dorg.bytedeco.javacpp.maxRetries=2 ^ + -Dorg.bytedeco.javacpp.mxbean=true ^ + "-Dlogback.configurationFile=%SCRIPT_DIR%logback.xml" ^ + -cp "%SCRIPT_DIR%lib\*" ^ + "-Djava.system.class.loader=org.sensorhub.utils.NativeClassLoader" ^ + "-Djavax.net.ssl.keyStore=%KEYSTORE%" ^ + "-Djavax.net.ssl.keyStorePassword=%KEYSTORE_PASSWORD%" ^ + "-Djavax.net.ssl.trustStore=%TRUSTSTORE%" ^ + "-Djavax.net.ssl.trustStorePassword=%TRUSTSTORE_PASSWORD%" ^ + "-Djava.library.path=%SCRIPT_DIR%nativelibs" ^ + com.botts.impl.security.SensorHubWrapper "%SCRIPT_DIR%config.json" "%SCRIPT_DIR%db" + +set "JAVA_EXIT_CODE=%ERRORLEVEL%" +echo AFTER JAVA +echo JAVA_EXIT_CODE=%JAVA_EXIT_CODE% +pause +endlocal & exit /b %JAVA_EXIT_CODE% + +:load_env +for /f "usebackq tokens=1,* delims==" %%A in ("%~1") do ( + set "ENV_NAME=%%A" + set "ENV_VALUE=%%B" + call :set_env_var +) +exit /b 0 + +:set_env_var +if not defined ENV_NAME exit /b 0 +if "%ENV_NAME:~0,1%"=="#" exit /b 0 +if /I "%ENV_NAME:~0,7%"=="export " set "ENV_NAME=%ENV_NAME:~7%" +set "%ENV_NAME%=%ENV_VALUE%" +exit /b 0 \ No newline at end of file diff --git a/dist/scripts/standard/launch_old.sh b/dist/scripts/standard/launch_old.sh new file mode 100644 index 0000000..d88905e --- /dev/null +++ b/dist/scripts/standard/launch_old.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +load_env() { + local env_file="$1" + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ""|"#"*) continue ;; + export\ *) line="${line#export }" ;; + esac + local name="${line%%=*}" + local value="${line#*=}" + export "${name}=${value}" + done < "$env_file" +} + +ENV_FILE="" +if [ -f "$SCRIPT_DIR/.env" ]; then + ENV_FILE="$SCRIPT_DIR/.env" +elif [ -f "$SCRIPT_DIR/../.env" ]; then + ENV_FILE="$SCRIPT_DIR/../.env" +fi + +if [ -n "$ENV_FILE" ]; then + load_env "$ENV_FILE" +fi + +check_existing_oscar() { + local pids + pids="$(pgrep -f 'com.botts.impl.security.SensorHubWrapper' || true)" + + if [ -z "$pids" ]; then + return 0 + fi + + if [ "${FORCE_RESTART:-0}" = "1" ]; then + echo "Existing OSCAR instance found with PID(s): $pids. Stopping because FORCE_RESTART=1." + kill $pids || true + sleep 2 + return 0 + fi + + echo "OSCAR is already running with PID(s): $pids." + echo "Run stop-all.sh first, or set FORCE_RESTART=1 to replace the existing OSCAR process." + return 1 +} + +check_existing_oscar || exit 1 + +SYSTEM_PROFILE="${SYSTEM_PROFILE:-8GB}" + +case "$SYSTEM_PROFILE" in + RPI4) + JAVA_XMS="512m" + JAVA_XMX="1536m" + JAVACPP_MAX_BYTES_DEFAULT="512m" + JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT="2g" + ;; + 8GB) + JAVA_XMS="1g" + JAVA_XMX="2g" + JAVACPP_MAX_BYTES_DEFAULT="1g" + JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT="4g" + ;; + 16GB) + JAVA_XMS="1g" + JAVA_XMX="3g" + JAVACPP_MAX_BYTES_DEFAULT="2g" + JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT="8g" + ;; + 32GB) + JAVA_XMS="2g" + JAVA_XMX="6g" + JAVACPP_MAX_BYTES_DEFAULT="4g" + JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT="16g" + ;; + *) + echo "Unknown profile '$SYSTEM_PROFILE', using 8GB defaults." + JAVA_XMS="1g" + JAVA_XMX="2g" + JAVACPP_MAX_BYTES_DEFAULT="1g" + JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT="4g" + ;; +esac + +: "${JAVACPP_MAX_BYTES:=$JAVACPP_MAX_BYTES_DEFAULT}" +: "${JAVACPP_MAX_PHYSICAL_BYTES:=$JAVACPP_MAX_PHYSICAL_BYTES_DEFAULT}" +: "${JFR_FILENAME:=$SCRIPT_DIR/oscar.jfr}" + +echo "Starting OSH Node with Profile: $SYSTEM_PROFILE" +echo " Heap: $JAVA_XMS / $JAVA_XMX" +echo " JavaCPP maxBytes: $JAVACPP_MAX_BYTES" +echo " JavaCPP maxPhysicalBytes: $JAVACPP_MAX_PHYSICAL_BYTES" +echo " JFR file: $JFR_FILENAME" + +if [ ! -f "$SCRIPT_DIR/load_trusted_certs.sh" ]; then + echo "Error: load_trusted_certs.sh not found in $SCRIPT_DIR." + exit 1 +fi +bash "$SCRIPT_DIR/load_trusted_certs.sh" + +export KEYSTORE="${KEYSTORE:-$SCRIPT_DIR/osh-keystore.p12}" +export KEYSTORE_TYPE="${KEYSTORE_TYPE:-PKCS12}" +export KEYSTORE_PASSWORD="${KEYSTORE_PASSWORD:-atakatak}" + +export TRUSTSTORE="${TRUSTSTORE:-$SCRIPT_DIR/truststore.jks}" +export TRUSTSTORE_TYPE="${TRUSTSTORE_TYPE:-JKS}" +export TRUSTSTORE_PASSWORD="${TRUSTSTORE_PASSWORD:-changeit}" + +export INITIAL_ADMIN_PASSWORD_FILE="${INITIAL_ADMIN_PASSWORD_FILE:-$SCRIPT_DIR/.s}" + +if [ ! -f "$INITIAL_ADMIN_PASSWORD_FILE" ] && [ -z "${INITIAL_ADMIN_PASSWORD:-}" ]; then + export INITIAL_ADMIN_PASSWORD="admin" +fi + +if [ ! -f "$SCRIPT_DIR/set-initial-admin-password.sh" ]; then + echo "Error: set-initial-admin-password.sh not found in $SCRIPT_DIR." + exit 1 +fi +bash "$SCRIPT_DIR/set-initial-admin-password.sh" + +exec java \ + -Xms"$JAVA_XMS" \ + -Xmx"$JAVA_XMX" \ + -Xss256k \ + -XX:ReservedCodeCacheSize=256m \ + -XX:+UseG1GC \ + -XX:+HeapDumpOnOutOfMemoryError \ + -XX:+UnlockDiagnosticVMOptions \ + -XX:NativeMemoryTracking=summary \ + "-Dorg.bytedeco.javacpp.maxBytes=$JAVACPP_MAX_BYTES" \ + "-Dorg.bytedeco.javacpp.maxPhysicalBytes=$JAVACPP_MAX_PHYSICAL_BYTES" \ + -Dorg.bytedeco.javacpp.maxRetries=2 \ + -Dorg.bytedeco.javacpp.mxbean=true \ + "-Dlogback.configurationFile=$SCRIPT_DIR/logback.xml" \ + -cp "$SCRIPT_DIR/lib/*" \ + "-Djava.system.class.loader=org.sensorhub.utils.NativeClassLoader" \ + "-Djava.library.path=$SCRIPT_DIR/nativelibs" \ + com.botts.impl.security.SensorHubWrapper "$SCRIPT_DIR/config.json" "$SCRIPT_DIR/db" \ No newline at end of file diff --git a/dist/scripts/standard/load_trusted_certs.bat b/dist/scripts/standard/load_trusted_certs.bat index 5357164..65a1541 100755 --- a/dist/scripts/standard/load_trusted_certs.bat +++ b/dist/scripts/standard/load_trusted_certs.bat @@ -1,64 +1,99 @@ @echo off -setlocal +setlocal EnableExtensions EnableDelayedExpansion echo Building Java trust store... -REM Default password for the sytem trust store is "changeit". Edit this next -REM line if it's something different in your Java installation. set "STOREPASS=changeit" - -REM Get the path of this script. set "SCRIPTDIR=%~dp0" +set "NEWTRUSTSTORE=%SCRIPTDIR%truststore.jks" +set "CERTDIR=%SCRIPTDIR%trusted_certificates" +set "CACERTS=" +set "JAVA_HOME_DETECTED=" -REM Get the path where we'll build the new trust store. -set "NEWTRUSTSTORE=%SCRIPTDIR%trustStore.jks" +rem First try JAVA_HOME if already set +if defined JAVA_HOME ( + if exist "%JAVA_HOME%\conf\security\cacerts" set "CACERTS=%JAVA_HOME%\conf\security\cacerts" + if not defined CACERTS if exist "%JAVA_HOME%\lib\security\cacerts" set "CACERTS=%JAVA_HOME%\lib\security\cacerts" +) -REM To find the location of the system trust store, we start by finding the -REM path to "java.exe". -for /f "tokens=* usebackq" %%j in (`where java`) do (set "JAVA=%%~dpj" & goto :next ) -:next -REM Then we back up a directory and look in lib\security. -set "CACERTS=%JAVA%..\lib\security\cacerts" +rem If that did not work, ask Java itself for java.home +if not defined CACERTS ( + for /f "tokens=1,* delims==" %%A in ('java -XshowSettings:properties -version 2^>^&1 ^| findstr /c:"java.home ="') do ( + set "JAVA_HOME_DETECTED=%%B" + ) +) -REM Now make a copy of that default system trust store into this directory, -REM where we'll add our stuff to it. -copy /y "%CACERTS%" "%NEWTRUSTSTORE%" >NUL +rem Trim leading spaces +if defined JAVA_HOME_DETECTED ( + for /f "tokens=* delims= " %%H in ("!JAVA_HOME_DETECTED!") do set "JAVA_HOME_DETECTED=%%H" +) -REM Get the full path to where our certs are. -set CERTDIR=%SCRIPTDIR%trusted_certificates +rem Try common cacerts locations under detected java.home +if not defined CACERTS if defined JAVA_HOME_DETECTED ( + if exist "!JAVA_HOME_DETECTED!\conf\security\cacerts" set "CACERTS=!JAVA_HOME_DETECTED!\conf\security\cacerts" + if not defined CACERTS if exist "!JAVA_HOME_DETECTED!\lib\security\cacerts" set "CACERTS=!JAVA_HOME_DETECTED!\lib\security\cacerts" +) -REM Now for each .cer, .pem, and .crt file in our cert dir, check to see if we -REM need to add it to the system trust store. -for %%c in ( %CERTDIR%\*.cer %CERTDIR%\*.pem %CERTDIR%\*.crt ) do ( - call :check_certificate %%c +if not defined CACERTS ( + echo Error: could not locate Java cacerts. + if defined JAVA_HOME echo JAVA_HOME="%JAVA_HOME%" + if defined JAVA_HOME_DETECTED echo java.home="!JAVA_HOME_DETECTED!" + endlocal & exit /b 1 ) -goto :end_of_script +if not exist "%CACERTS%" ( + echo Error: Java cacerts path does not exist: "%CACERTS%" + endlocal & exit /b 1 +) -REM The next few lines define a function that checks whether a certificate -REM is already loaded in the system store. If so, it does nothing. If not, it -REM attempts to load it in. Note that the alias of the certificate is -REM calculated as the base file name (without path or extension). -REM NOTE: As currently written, this is performing an unnecessary check, since -REM we're guaranteed that none of the certificates will ever be present in the -REM original file. +echo Using Java cacerts: "%CACERTS%" -:check_certificate -set ALIAS=%~n1 -REM Check for existence. ERRORLEVEL is set to 0 if it's found, and something -REM else otherwise. -keytool -list -keystore "%NEWTRUSTSTORE%" -storepass "%STOREPASS%" -alias "%ALIAS%" >NUL 2>NUL -if not "%ERRORLEVEL%" == "0" ( - echo Importing "%ALIAS%" from "%1" - keytool -importcert -keystore "%NEWTRUSTSTORE%" -noprompt -storepass "%STOREPASS%" -alias "%ALIAS%" -file "%1" -) else ( - echo Certificate with alias "%ALIAS%" already exists. Skipping. +copy /y "%CACERTS%" "%NEWTRUSTSTORE%" >nul +if errorlevel 1 ( + echo Error: failed to create "%NEWTRUSTSTORE%" + endlocal & exit /b 1 +) + +if not exist "%CERTDIR%" ( + echo Trusted certificates directory not found: "%CERTDIR%" + echo Using copied default trust store only. + echo Done. + endlocal & exit /b 0 +) + +set "FOUND_CERT=0" +for %%c in ("%CERTDIR%\*.cer" "%CERTDIR%\*.pem" "%CERTDIR%\*.crt") do ( + if exist "%%~fc" ( + set "FOUND_CERT=1" + call :check_certificate "%%~fc" + if errorlevel 1 ( + endlocal & exit /b 1 + ) + ) ) -REM Return to caller. -exit /b 0 -:end_of_script +if "%FOUND_CERT%"=="0" ( + echo No certificate files found in "%CERTDIR%". +) echo Done. +endlocal & exit /b 0 + +:check_certificate +setlocal +set "CERTFILE=%~1" +set "ALIAS=%~n1" + +keytool -list -keystore "%NEWTRUSTSTORE%" -storepass "%STOREPASS%" -alias "%ALIAS%" >nul 2>nul +if not "%ERRORLEVEL%"=="0" ( + echo Importing "%ALIAS%" from "%CERTFILE%" + keytool -importcert -keystore "%NEWTRUSTSTORE%" -noprompt -storepass "%STOREPASS%" -alias "%ALIAS%" -file "%CERTFILE%" >nul + if errorlevel 1 ( + echo Error: failed to import "%ALIAS%" from "%CERTFILE%" + endlocal & exit /b 1 + ) +) else ( + echo Certificate with alias "%ALIAS%" already exists. Skipping. +) -endlocal +endlocal & exit /b 0 \ No newline at end of file From 6a08c7ea3e4e11220e1640d7ecd84a2e5b76fdc3 Mon Sep 17 00:00:00 2001 From: tyronechrisharris <142608718+tyronechrisharris@users.noreply.github.com> Date: Tue, 5 May 2026 19:58:01 -0400 Subject: [PATCH 2/2] harden OSCAR 3.5.1 deployment, monitoring, and DB stability --- include/osh-addons | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/osh-addons b/include/osh-addons index 8b0aabd..bbadeb2 160000 --- a/include/osh-addons +++ b/include/osh-addons @@ -1 +1 @@ -Subproject commit 8b0aabd5a74aa375a5424dde516897e67abb82d7 +Subproject commit bbadeb2dab95b8f1b3f52c458fc6fbc0b2997f2b

Operational note

-

If multiple extracted versions exist on the same machine, use the stop-all script to fully stop the application and Dockerized database for the version you were running. An older running container can accidentally populate data in the wrong directory and create confusion during configuration or tuning.