diff --git a/.github/config/dependency-review-config.yml b/.github/config/dependency-review-config.yml new file mode 100644 index 000000000..5df58cdb9 --- /dev/null +++ b/.github/config/dependency-review-config.yml @@ -0,0 +1 @@ +allow-ghsas: GHSA-wrw7-89jp-8q8g \ No newline at end of file diff --git a/.github/workflows/README-tauri.md b/.github/workflows/README-tauri.md new file mode 100644 index 000000000..be6346045 --- /dev/null +++ b/.github/workflows/README-tauri.md @@ -0,0 +1,182 @@ +# Tauri Build Workflows + +This directory contains GitHub Actions workflows for building Tauri desktop applications for Stirling-PDF. + +## Workflow + +### `tauri-build.yml` - Unified Build Workflow + +**Purpose**: Build Tauri applications for all platforms (Windows, macOS, Linux) with comprehensive testing and validation. + +**Triggers**: +- Manual dispatch with platform selection (windows, macos, linux, or all) +- Pull requests affecting Tauri-related files +- Pushes to main branch affecting Tauri-related files + +**Platforms**: +- **Windows**: x86_64 (exe and msi) +- **macOS**: Apple Silicon (aarch64) and Intel (x86_64) (dmg) +- **Linux**: x86_64 (deb and AppImage) + +**Features**: +- **Dynamic Platform Selection**: Choose specific platforms or build all +- **Smart JRE Bundling**: Uses JLink to create optimized custom JRE +- **Apple Code Signing**: Full macOS notarization and signing support +- **Comprehensive Validation**: Artifact verification and size checks +- **Self-Contained**: No Java installation required for end users +- **Cross-Platform**: Builds on actual target platforms for compatibility +- **Detailed Logging**: Complete build process visibility + +## Usage + +### Manual Testing + +1. **Test All Platforms**: + ```bash + # Go to Actions tab in GitHub + # Run "Build Tauri Applications" workflow + # Select "all" for platform + ``` + +2. **Test Specific Platform**: + ```bash + # Go to Actions tab in GitHub + # Run "Build Tauri Applications" workflow + # Select specific platform (windows/macos/linux) + ``` + +3. **Automatic Testing**: + - Builds are automatically triggered on PRs and pushes + - All platforms are tested by default + - Artifacts are uploaded for download and testing + +## Configuration + +### Required Secrets + +#### For macOS Code Signing (Required for distribution) + +Configure these secrets in your repository for macOS app signing: + +- `APPLE_CERTIFICATE`: Base64-encoded .p12 certificate file +- `APPLE_CERTIFICATE_PASSWORD`: Password for the .p12 certificate +- `APPLE_SIGNING_IDENTITY`: Certificate name (e.g., "Developer ID Application: Your Name") +- `APPLE_ID`: Your Apple ID email +- `APPLE_PASSWORD`: App-specific password for your Apple ID +- `APPLE_TEAM_ID`: Your Apple Developer Team ID + +#### Setting Up Apple Code Signing + +1. **Get a Developer ID Certificate**: + - Join the Apple Developer Program ($99/year) + - Create a "Developer ID Application" certificate in Apple Developer portal + - Download the certificate as a .p12 file + +2. **Convert Certificate to Base64**: + ```bash + base64 -i certificate.p12 | pbcopy + ``` + +3. **Create App-Specific Password**: + - Go to appleid.apple.com → Sign-In and Security → App-Specific Passwords + - Generate a new password for "Tauri CI" + +4. **Find Your Team ID**: + - Apple Developer portal → Membership → Team ID + +5. **Add to GitHub Secrets**: + - Repository → Settings → Secrets and variables → Actions + - Add each secret with the exact names listed above + +#### For General Tauri Signing (Optional) + +- `TAURI_SIGNING_PRIVATE_KEY`: Private key for signing Tauri applications +- `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`: Password for the signing private key + +### File Structure + +The workflows expect this structure: +``` +├── frontend/ +│ ├── src-tauri/ +│ │ ├── Cargo.toml +│ │ ├── tauri.conf.json +│ │ └── src/ +│ ├── package.json +│ └── src/ +├── gradlew +└── stirling-pdf/ + └── build/libs/ +``` + +## Validation + +Both workflows include comprehensive validation: + +1. **Build Validation**: Ensures all expected artifacts are created +2. **Size Validation**: Checks artifacts aren't suspiciously small +3. **Platform Validation**: Verifies platform-specific requirements +4. **Integration Testing**: Tests that Java backend builds correctly + +## Troubleshooting + +### Common Issues + +1. **Missing Dependencies**: + - Ubuntu: Ensure system dependencies are installed + - macOS: Check Rust toolchain targets + - Windows: Verify MSVC tools are available + +2. **Java Backend Build Fails**: + - Check Gradle permissions (`chmod +x ./gradlew`) + - Verify JDK 21 is properly configured + +3. **Artifact Size Issues**: + - Small artifacts usually indicate build failures + - Check that backend JAR is properly copied to Tauri resources + +4. **Signing Issues**: + - Ensure signing secrets are configured if needed + - Check that signing keys are valid + +### Debugging + +1. **Check Logs**: Each step provides detailed logging +2. **Artifact Inspection**: Download artifacts to verify contents +3. **Local Testing**: Test builds locally before running workflows + +## JLink Integration Benefits + +The workflows now use JLink to create custom Java runtimes: + +### **Self-Contained Applications** +- **No Java Required**: Users don't need Java installed +- **Consistent Runtime**: Same Java version across all deployments +- **Smaller Size**: Only includes needed Java modules (~30-50MB vs full JRE) + +### **Security & Performance** +- **Minimal Attack Surface**: Only required modules included +- **Faster Startup**: Optimized runtime with stripped debug info +- **Better Compression**: JLink level 2 compression reduces size + +### **Module Analysis** +- **Automatic Detection**: Uses `jdeps` to analyze JAR dependencies +- **Fallback Safety**: Predefined module list if analysis fails +- **Platform Optimized**: Different modules per platform if needed + +## Integration with Existing Workflows + +These workflows are designed to complement the existing build system: + +- Uses same JDK and Gradle setup as `build.yml` +- Follows same security practices as `multiOSReleases.yml` +- Compatible with existing release processes +- Integrates JLink logic from `build-tauri-jlink.sh/bat` scripts + +## Next Steps + +1. Test the workflows on your branch +2. Verify all platforms build successfully +3. Check artifact quality and sizes +4. Configure signing if needed +5. Merge when all tests pass \ No newline at end of file diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 154b6bdae..9d697e98f 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -25,3 +25,5 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: "Dependency Review" uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 + with: + config-file: './.github/config/dependency-review-config.yml' diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml new file mode 100644 index 000000000..631de2f70 --- /dev/null +++ b/.github/workflows/tauri-build.yml @@ -0,0 +1,329 @@ +name: Build Tauri Applications + +on: + workflow_dispatch: + inputs: + platform: + description: "Platform to build (windows, macos, linux, or all)" + required: true + default: "all" + type: choice + options: + - all + - windows + - macos + - linux + pull_request: + branches: [main, V2] + paths: + - 'frontend/src-tauri/**' + - 'frontend/src/**' + - 'frontend/package.json' + - 'frontend/package-lock.json' + - '.github/workflows/tauri-build.yml' + push: + branches: [main, V2] + paths: + - 'frontend/src-tauri/**' + - 'frontend/src/**' + - 'frontend/package.json' + - 'frontend/package-lock.json' + - '.github/workflows/tauri-build.yml' + +permissions: + contents: read + +jobs: + determine-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Determine build matrix + id: set-matrix + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + case "${{ github.event.inputs.platform }}" in + "windows") + echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT + ;; + "macos") + echo 'matrix={"include":[{"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT + ;; + "linux") + echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT + ;; + *) + echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT + ;; + esac + else + # For PR/push events, build all platforms + echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT + fi + + build: + needs: determine-matrix + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.determine-matrix.outputs.matrix) }} + runs-on: ${{ matrix.platform }} + steps: + - name: Harden Runner + uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install dependencies (ubuntu only) + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: ${{ (matrix.platform == 'macos-latest' || matrix.platform == 'macos-13') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + + + - name: Set up JDK 21 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + java-version: "21" + distribution: "temurin" + + - name: Build Java backend with JLink + working-directory: ./ + shell: bash + run: | + chmod +x ./gradlew + echo "🔧 Building Stirling-PDF JAR..." + # STIRLING_PDF_DESKTOP_UI=false ./gradlew clean bootJar --no-daemon + ./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube + + # Find the built JAR + STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1) + echo "✅ Built JAR: $STIRLING_JAR" + + # Create Tauri directories + mkdir -p ./frontend/src-tauri/libs + mkdir -p ./frontend/src-tauri/runtime + + # Copy JAR to Tauri libs + cp "$STIRLING_JAR" ./frontend/src-tauri/libs/ + echo "✅ JAR copied to Tauri libs" + + # Analyze JAR dependencies for jlink modules + echo "🔍 Analyzing JAR dependencies..." + if command -v jdeps &> /dev/null; then + DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "") + if [ -n "$DETECTED_MODULES" ]; then + echo "📋 jdeps detected modules: $DETECTED_MODULES" + MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" + else + echo "⚠️ jdeps analysis failed, using predefined modules" + MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" + fi + else + echo "⚠️ jdeps not available, using predefined modules" + MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" + fi + + # Create custom JRE with jlink (always rebuild) + echo "🔧 Creating custom JRE with jlink..." + echo "📋 Using modules: $MODULES" + + # Remove any existing JRE + rm -rf ./frontend/src-tauri/runtime/jre + + # Create the custom JRE + jlink \ + --add-modules "$MODULES" \ + --strip-debug \ + --compress=2 \ + --no-header-files \ + --no-man-pages \ + --output ./frontend/src-tauri/runtime/jre + + if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then + echo "❌ Failed to create JLink runtime" + exit 1 + fi + + # Test the bundled runtime + if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then + RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1) + echo "✅ Custom JRE created successfully: $RUNTIME_VERSION" + else + echo "❌ Custom JRE executable not found" + exit 1 + fi + + # Calculate runtime size + RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1) + echo "📊 Custom JRE size: $RUNTIME_SIZE" + env: + DISABLE_ADDITIONAL_FEATURES: true + + - name: Install frontend dependencies + working-directory: ./frontend + run: npm install + + - name: Import Apple Developer Certificate + if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13' + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + echo "Importing Apple Developer Certificate..." + echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12 + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security set-keychain-settings -t 3600 -u build.keychain + security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain + security find-identity -v -p codesigning build.keychain + - name: Verify Certificate + if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13' + run: | + echo "Verifying Apple Developer Certificate..." + CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application") + echo "Certificate Info: $CERT_INFO" + CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}') + echo "Certificate ID: $CERT_ID" + echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV + echo "Certificate imported." + + - name: Check DMG creation dependencies (macOS only) + if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13' + run: | + echo "🔍 Checking DMG creation dependencies on ${{ matrix.platform }}..." + echo "hdiutil version: $(hdiutil --version || echo 'NOT FOUND')" + echo "create-dmg availability: $(which create-dmg || echo 'NOT FOUND')" + echo "Available disk space: $(df -h /tmp | tail -1)" + echo "macOS version: $(sw_vers -productVersion)" + echo "Available tools:" + ls -la /usr/bin/hd* || echo "No hd* tools found" + + - name: Build Tauri app + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }} + SIGN: 1 + CI: true + with: + projectPath: ./frontend + tauriScript: npx tauri + args: ${{ matrix.args }} + + - name: Rename artifacts + shell: bash + run: | + mkdir -p ./dist + cd ./frontend/src-tauri/target + + # Find and rename artifacts based on platform + if [ "${{ matrix.platform }}" = "windows-latest" ]; then + find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \; + find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \; + elif [ "${{ matrix.platform }}" = "macos-latest" ] || [ "${{ matrix.platform }}" = "macos-13" ]; then + find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \; + find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \; + else + find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \; + find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \; + fi + + - name: Upload artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: Stirling-PDF-${{ matrix.name }} + path: ./dist/* + retention-days: 7 + + - name: Verify build artifacts + shell: bash + run: | + cd ./frontend/src-tauri/target + + # Check for expected artifacts based on platform + if [ "${{ matrix.platform }}" = "windows-latest" ]; then + echo "Checking for Windows artifacts..." + find . -name "*.exe" -o -name "*.msi" | head -5 + if [ $(find . -name "*.exe" | wc -l) -eq 0 ]; then + echo "❌ No Windows executable found" + exit 1 + fi + elif [ "${{ matrix.platform }}" = "macos-latest" ] || [ "${{ matrix.platform }}" = "macos-13" ]; then + echo "Checking for macOS artifacts..." + find . -name "*.dmg" -o -name "*.app" | head -5 + if [ $(find . -name "*.dmg" -o -name "*.app" | wc -l) -eq 0 ]; then + echo "❌ No macOS artifacts found" + exit 1 + fi + else + echo "Checking for Linux artifacts..." + find . -name "*.deb" -o -name "*.AppImage" | head -5 + if [ $(find . -name "*.deb" -o -name "*.AppImage" | wc -l) -eq 0 ]; then + echo "❌ No Linux artifacts found" + exit 1 + fi + fi + + echo "✅ Build artifacts found for ${{ matrix.name }}" + + - name: Test artifact sizes + shell: bash + run: | + cd ./frontend/src-tauri/target + echo "Artifact sizes for ${{ matrix.name }}:" + find . -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.AppImage" -o -name "*.msi" | while read file; do + if [ -f "$file" ]; then + size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo "unknown") + echo "$file: $size bytes" + # Check if file is suspiciously small (less than 1MB) + if [ "$size" != "unknown" ] && [ "$size" -lt 1048576 ]; then + echo "⚠️ Warning: $file is smaller than 1MB" + fi + fi + done + + report: + needs: build + runs-on: ubuntu-latest + if: always() + steps: + - name: Report build results + run: | + if [ "${{ needs.build.result }}" = "success" ]; then + echo "✅ All Tauri builds completed successfully!" + echo "Artifacts are ready for distribution." + else + echo "❌ Some Tauri builds failed." + echo "Please check the logs and fix any issues." + exit 1 + fi \ No newline at end of file diff --git a/DesktopApplicationDevelopmentGuide.md b/DesktopApplicationDevelopmentGuide.md new file mode 100644 index 000000000..aaefd5b1b --- /dev/null +++ b/DesktopApplicationDevelopmentGuide.md @@ -0,0 +1,228 @@ +# JLink Runtime Bundling for Stirling-PDF + +This guide explains how to use JLink to bundle a custom Java runtime with your Tauri application, eliminating the need for users to have Java installed. + +## Overview + +Instead of requiring users to install a JRE separately, JLink creates a minimal, custom Java runtime that includes only the modules your application needs. This approach: + +- **Eliminates JRE dependency**: Users don't need Java installed +- **Reduces size**: Only includes necessary Java modules +- **Improves security**: Minimal attack surface with fewer modules +- **Ensures consistency**: Same Java version across all deployments + +## Prerequisites + +- **JDK 17 or higher** (not just JRE - you need `jlink` command) +- **Node.js and npm** for the frontend +- **Rust and Tauri CLI** for building the desktop app + +## Quick Start + +### 1. Build with JLink + +Run the appropriate build script for your platform: + +**Linux/macOS:** +```bash +./scripts/build-tauri-jlink.sh +``` + +**Windows:** +```cmd +scripts\build-tauri-jlink.bat +``` + +### 2. Build Tauri Application + +```bash +cd frontend +npm run tauri-build +``` + +The resulting application will include the bundled JRE and won't require Java to be installed on the target system. + +## What the Build Script Does + +1. **Builds the Stirling-PDF JAR** using Gradle +2. **Analyzes dependencies** using `jdeps` to determine required Java modules +3. **Creates custom JRE** using `jlink` with only necessary modules +4. **Copies files** to the correct Tauri directories: + - JAR file → `frontend/src-tauri/libs/` + - Custom JRE → `frontend/src-tauri/runtime/jre/` +5. **Creates test launchers** for standalone testing + +## Directory Structure + +After running the build script: + +``` +frontend/src-tauri/ +├── libs/ +│ └── Stirling-PDF-X.X.X.jar +├── runtime/ +│ ├── jre/ # Custom JLink runtime +│ │ ├── bin/ +│ │ │ ├── java(.exe) +│ │ │ └── ... +│ │ ├── lib/ +│ │ └── ... +│ ├── launch-stirling.sh # Test launcher (Linux/macOS) +│ └── launch-stirling.bat # Test launcher (Windows) +└── tauri.conf.json # Already configured to bundle runtime +``` + +## Testing the Bundled Runtime + +Before building the full Tauri app, you can test the bundled runtime: + +**Linux/macOS:** +```bash +./frontend/src-tauri/runtime/launch-stirling.sh +``` + +**Windows:** +```cmd +frontend\src-tauri\runtime\launch-stirling.bat +``` + +This will start Stirling-PDF using the bundled JRE, accessible at http://localhost:8080 + +## Configuration Details + +### Tauri Configuration (`tauri.conf.json`) + +The bundle resources are configured to include both the JAR and runtime: + +```json +{ + "bundle": { + "resources": [ + "libs/*.jar", + "runtime/jre/**/*" + ] + } +} +``` + +### Gradle Configuration (`build.gradle`) + +JLink options are configured in the jpackage section: + +```gradle +jLinkOptions = [ + "--strip-debug", + "--compress=2", + "--no-header-files", + "--no-man-pages" +] + +addModules = [ + "java.base", + "java.desktop", + "java.logging", + "java.management", + // ... other required modules +] +``` + +### Rust Code (`lib.rs`) + +The application automatically detects and uses the bundled JRE instead of system Java. + +## Modules Included + +The custom runtime includes these Java modules: + +- `java.base` - Core Java functionality +- `java.desktop` - AWT/Swing (for UI components) +- `java.instrument` - Java instrumentation (required by Jetty) +- `java.logging` - Logging framework +- `java.management` - JMX and monitoring +- `java.naming` - JNDI services +- `java.net.http` - HTTP client +- `java.security.jgss` - Security services +- `java.sql` - Database connectivity +- `java.xml` - XML processing +- `java.xml.crypto` - XML security +- `jdk.crypto.ec` - Elliptic curve cryptography +- `jdk.crypto.cryptoki` - PKCS#11 support +- `jdk.unsupported` - Internal APIs (used by some libraries) + +## Troubleshooting + +### JLink Not Found +``` +❌ jlink is not available +``` +**Solution**: Install a full JDK (not just JRE). JLink is included with JDK 9+. + +### Module Not Found During Runtime +If the application fails with module-related errors, you may need to add additional modules to the `addModules` list in `build.gradle`. + +### Large Runtime Size +The bundled runtime should be 50-80MB. If it's much larger: +- Ensure `--strip-debug` and `--compress=2` options are used +- Review the module list - remove unnecessary modules +- Consider using `--no-header-files` and `--no-man-pages` + +## Benefits Over Traditional JAR Approach + +| Aspect | Traditional JAR | JLink Bundle | +|--------|----------------|--------------| +| User Setup | Requires JRE installation | No Java installation needed | +| Distribution Size | Smaller JAR, but requires ~200MB JRE | Larger bundle (~80MB), but self-contained | +| Java Version | Depends on user's installed version | Consistent, controlled version | +| Security Updates | User manages JRE updates | Developer controls runtime version | +| Startup Time | May be faster (shared JRE) | Slightly slower (isolated runtime) | + +## Advanced Usage + +### Custom Module Analysis + +To analyze your specific JAR's module requirements: + +```bash +jdeps --print-module-deps --ignore-missing-deps build/libs/Stirling-PDF-*.jar +``` + +### Manual JLink Command + +If you want to create the runtime manually: + +```bash +jlink \ + --add-modules java.base,java.desktop,java.logging,java.management,java.naming,java.net.http,java.security.jgss,java.sql,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported \ + --strip-debug \ + --compress=2 \ + --no-header-files \ + --no-man-pages \ + --output frontend/src-tauri/runtime/jre +``` + +## Migration Guide + +### From JAR-based Tauri App + +1. Update your Tauri configuration to include the runtime resources +2. Update your Rust code to use the bundled JRE path +3. Run the JLink build script +4. Test the bundled runtime +5. Build and distribute the new self-contained app + +### Deployment + +The final Tauri application will be completely self-contained. Users can: +- Install the app normally (no Java installation required) +- Run the app immediately after installation +- Not worry about Java version compatibility issues + +## Support + +If you encounter issues with the JLink bundling: + +1. Ensure you have a JDK (not JRE) installed +2. Check that the Java version is 17 or higher +3. Verify that the build script completed successfully +4. Test the bundled runtime using the provided launcher scripts +5. Check the Tauri build logs for any missing resources \ No newline at end of file diff --git a/DeveloperGuide.md b/DeveloperGuide.md index 0728a1cdc..0e9ce6c95 100644 --- a/DeveloperGuide.md +++ b/DeveloperGuide.md @@ -32,6 +32,12 @@ This guide focuses on developing for Stirling 2.0, including both the React fron - Docker for containerization - Gradle for build management +**Desktop Application (Tauri):** +- Tauri for cross-platform desktop app packaging +- Rust backend for system integration +- PDF file association support +- Self-contained JRE bundling with JLink + **Legacy (reference only during development):** - Thymeleaf templates (being completely replaced in 2.0) @@ -44,6 +50,8 @@ This guide focuses on developing for Stirling 2.0, including both the React fron - Java JDK 17 or later (JDK 21 recommended) - Node.js 18+ and npm (required for frontend development) - Gradle 7.0 or later (Included within the repo) +- Rust and Cargo (required for Tauri desktop app development) +- Tauri CLI (install with `cargo install tauri-cli`) ### Setup Steps @@ -95,6 +103,14 @@ Stirling 2.0 uses client-side file storage: ### Legacy Code Reference The existing Thymeleaf templates remain in the codebase during development as reference material but will be completely removed for the 2.0 release. +### Tauri Desktop App Development +Stirling-PDF can be packaged as a cross-platform desktop application using Tauri with PDF file association support and bundled JRE: + +**Quick Start:** +1. **Development/Testing**: `npm run tauri-dev "path/to/test.pdf"` +2. **Building**: See [DesktopApplicationDevelopmentGuide.md](DesktopApplicationDevelopmentGuide.md) for complete build instructions +3. **Features**: File associations, self-contained JRE, cross-platform support + ## 5. Project Structure ```bash @@ -109,6 +125,12 @@ Stirling-PDF/ │ │ ├── services/ # API and utility services │ │ ├── types/ # TypeScript type definitions │ │ └── utils/ # Utility functions +│ ├── src-tauri/ # Tauri desktop app configuration +│ │ ├── src/ # Rust backend code +│ │ ├── libs/ # JAR files (generated by build scripts) +│ │ ├── runtime/ # Bundled JRE (generated by build scripts) +│ │ ├── Cargo.toml # Rust dependencies +│ │ └── tauri.conf.json # Tauri configuration │ ├── public/ │ │ └── locales/ # Internationalization files (JSON) │ ├── package.json # Frontend dependencies diff --git a/app/core/build.gradle b/app/core/build.gradle index 745dbb87a..014f934de 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -29,7 +29,6 @@ dependencies { if (System.getenv('STIRLING_PDF_DESKTOP_UI') != 'false' || (project.hasProperty('STIRLING_PDF_DESKTOP_UI') && project.getProperty('STIRLING_PDF_DESKTOP_UI') != 'false')) { - implementation 'me.friwi:jcefmaven:132.3.1' implementation 'org.openjfx:javafx-controls:21' implementation 'org.openjfx:javafx-swing:21' } diff --git a/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java b/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java index 2131b4239..4211f5dcd 100644 --- a/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -151,6 +151,13 @@ public class SPDFApplication { serverPortStatic = serverPort; String url = baseUrl + ":" + getStaticPort() + contextPath; + // Log Tauri mode information + if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"))) { + String parentPid = System.getenv("TAURI_PARENT_PID"); + log.info( + "Running in Tauri mode. Parent process PID: {}", + parentPid != null ? parentPid : "not set"); + } if (webBrowser != null && Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { webBrowser.initWebUI(url); diff --git a/app/core/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java b/app/core/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java deleted file mode 100644 index 959e7f354..000000000 --- a/app/core/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java +++ /dev/null @@ -1,497 +0,0 @@ -package stirling.software.SPDF.UI.impl; - -import java.awt.AWTException; -import java.awt.BorderLayout; -import java.awt.Frame; -import java.awt.Image; -import java.awt.MenuItem; -import java.awt.PopupMenu; -import java.awt.SystemTray; -import java.awt.TrayIcon; -import java.awt.event.WindowEvent; -import java.awt.event.WindowStateListener; -import java.io.File; -import java.io.InputStream; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; - -import javax.imageio.ImageIO; -import javax.swing.JFrame; -import javax.swing.JPanel; -import javax.swing.SwingUtilities; -import javax.swing.Timer; - -import org.cef.CefApp; -import org.cef.CefClient; -import org.cef.CefSettings; -import org.cef.browser.CefBrowser; -import org.cef.callback.CefBeforeDownloadCallback; -import org.cef.callback.CefDownloadItem; -import org.cef.callback.CefDownloadItemCallback; -import org.cef.handler.CefDownloadHandlerAdapter; -import org.cef.handler.CefLoadHandlerAdapter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; - -import jakarta.annotation.PreDestroy; - -import lombok.extern.slf4j.Slf4j; - -import me.friwi.jcefmaven.CefAppBuilder; -import me.friwi.jcefmaven.EnumProgress; -import me.friwi.jcefmaven.MavenCefAppHandlerAdapter; -import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler; - -import stirling.software.SPDF.UI.WebBrowser; -import stirling.software.common.configuration.InstallationPathConfig; -import stirling.software.common.util.UIScaling; - -@Component -@Slf4j -@ConditionalOnProperty( - name = "STIRLING_PDF_DESKTOP_UI", - havingValue = "true", - matchIfMissing = false) -public class DesktopBrowser implements WebBrowser { - private static CefApp cefApp; - private static CefClient client; - private static CefBrowser browser; - private static JFrame frame; - private static LoadingWindow loadingWindow; - private static volatile boolean browserInitialized = false; - private static TrayIcon trayIcon; - private static SystemTray systemTray; - - public DesktopBrowser() { - SwingUtilities.invokeLater( - () -> { - loadingWindow = new LoadingWindow(null, "Initializing..."); - loadingWindow.setVisible(true); - }); - } - - public void initWebUI(String url) { - CompletableFuture.runAsync( - () -> { - try { - CefAppBuilder builder = new CefAppBuilder(); - configureCefSettings(builder); - builder.setProgressHandler(createProgressHandler()); - builder.setInstallDir( - new File(InstallationPathConfig.getClientWebUIPath())); - // Build and initialize CEF - cefApp = builder.build(); - client = cefApp.createClient(); - - // Set up download handler - setupDownloadHandler(); - - // Create browser and frame on EDT - SwingUtilities.invokeAndWait( - () -> { - browser = client.createBrowser(url, false, false); - setupMainFrame(); - setupLoadHandler(); - - // Force initialize UI after 7 seconds if not already done - Timer timeoutTimer = - new Timer( - 2500, - e -> { - log.warn( - "Loading timeout reached. Forcing" - + " UI transition."); - if (!browserInitialized) { - // Force UI initialization - forceInitializeUI(); - } - }); - timeoutTimer.setRepeats(false); - timeoutTimer.start(); - }); - } catch (Exception e) { - log.error("Error initializing JCEF browser: ", e); - cleanup(); - } - }); - } - - private void configureCefSettings(CefAppBuilder builder) { - CefSettings settings = builder.getCefSettings(); - String basePath = InstallationPathConfig.getClientWebUIPath(); - log.info("basePath " + basePath); - settings.cache_path = new File(basePath + "cache").getAbsolutePath(); - settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath(); - // settings.browser_subprocess_path = new File(basePath + - // "subprocess").getAbsolutePath(); - // settings.resources_dir_path = new File(basePath + "resources").getAbsolutePath(); - // settings.locales_dir_path = new File(basePath + "locales").getAbsolutePath(); - settings.log_file = new File(basePath, "debug.log").getAbsolutePath(); - - settings.persist_session_cookies = true; - settings.windowless_rendering_enabled = false; - settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO; - - builder.setAppHandler( - new MavenCefAppHandlerAdapter() { - @Override - public void stateHasChanged(org.cef.CefApp.CefAppState state) { - log.info("CEF state changed: " + state); - if (state == CefApp.CefAppState.TERMINATED) { - System.exit(0); - } - } - }); - } - - private void setupDownloadHandler() { - client.addDownloadHandler( - new CefDownloadHandlerAdapter() { - @Override - public boolean onBeforeDownload( - CefBrowser browser, - CefDownloadItem downloadItem, - String suggestedName, - CefBeforeDownloadCallback callback) { - callback.Continue("", true); - return true; - } - - @Override - public void onDownloadUpdated( - CefBrowser browser, - CefDownloadItem downloadItem, - CefDownloadItemCallback callback) { - if (downloadItem.isComplete()) { - log.info("Download completed: " + downloadItem.getFullPath()); - } else if (downloadItem.isCanceled()) { - log.info("Download canceled: " + downloadItem.getFullPath()); - } - } - }); - } - - private ConsoleProgressHandler createProgressHandler() { - return new ConsoleProgressHandler() { - @Override - public void handleProgress(EnumProgress state, float percent) { - Objects.requireNonNull(state, "state cannot be null"); - SwingUtilities.invokeLater( - () -> { - if (loadingWindow != null) { - switch (state) { - case LOCATING: - loadingWindow.setStatus("Locating Files..."); - loadingWindow.setProgress(0); - break; - case DOWNLOADING: - if (percent >= 0) { - loadingWindow.setStatus( - String.format( - "Downloading additional files: %.0f%%", - percent)); - loadingWindow.setProgress((int) percent); - } - break; - case EXTRACTING: - loadingWindow.setStatus("Extracting files..."); - loadingWindow.setProgress(60); - break; - case INITIALIZING: - loadingWindow.setStatus("Initializing UI..."); - loadingWindow.setProgress(80); - break; - case INITIALIZED: - loadingWindow.setStatus("Finalising startup..."); - loadingWindow.setProgress(90); - break; - } - } - }); - } - }; - } - - private void setupMainFrame() { - frame = new JFrame("Stirling-PDF"); - frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - frame.setUndecorated(true); - frame.setOpacity(0.0f); - - JPanel contentPane = new JPanel(new BorderLayout()); - contentPane.setDoubleBuffered(true); - contentPane.add(browser.getUIComponent(), BorderLayout.CENTER); - frame.setContentPane(contentPane); - - frame.addWindowListener( - new java.awt.event.WindowAdapter() { - @Override - public void windowClosing(java.awt.event.WindowEvent windowEvent) { - cleanup(); - System.exit(0); - } - }); - - frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800)); - frame.setLocationRelativeTo(null); - - loadIcon(); - } - - private void setupLoadHandler() { - final long initStartTime = System.currentTimeMillis(); - log.info("Setting up load handler at: {}", initStartTime); - - client.addLoadHandler( - new CefLoadHandlerAdapter() { - @Override - public void onLoadingStateChange( - CefBrowser browser, - boolean isLoading, - boolean canGoBack, - boolean canGoForward) { - log.debug( - "Loading state change - isLoading: {}, canGoBack: {}, canGoForward:" - + " {}, browserInitialized: {}, Time elapsed: {}ms", - isLoading, - canGoBack, - canGoForward, - browserInitialized, - System.currentTimeMillis() - initStartTime); - - if (!isLoading && !browserInitialized) { - log.info( - "Browser finished loading, preparing to initialize UI" - + " components"); - browserInitialized = true; - SwingUtilities.invokeLater( - () -> { - try { - if (loadingWindow != null) { - log.info("Starting UI initialization sequence"); - - // Close loading window first - loadingWindow.setVisible(false); - loadingWindow.dispose(); - loadingWindow = null; - log.info("Loading window disposed"); - - // Then setup the main frame - frame.setVisible(false); - frame.dispose(); - frame.setOpacity(1.0f); - frame.setUndecorated(false); - frame.pack(); - frame.setSize( - UIScaling.scaleWidth(1280), - UIScaling.scaleHeight(800)); - frame.setLocationRelativeTo(null); - log.debug("Frame reconfigured"); - - // Show the main frame - frame.setVisible(true); - frame.requestFocus(); - frame.toFront(); - log.info("Main frame displayed and focused"); - - // Focus the browser component - Timer focusTimer = - new Timer( - 100, - e -> { - try { - browser.getUIComponent() - .requestFocus(); - log.info( - "Browser component" - + " focused"); - } catch (Exception ex) { - log.error( - "Error focusing" - + " browser", - ex); - } - }); - focusTimer.setRepeats(false); - focusTimer.start(); - } - } catch (Exception e) { - log.error("Error during UI initialization", e); - // Attempt cleanup on error - if (loadingWindow != null) { - loadingWindow.dispose(); - loadingWindow = null; - } - if (frame != null) { - frame.setVisible(true); - frame.requestFocus(); - } - } - }); - } - } - }); - } - - private void setupTrayIcon(Image icon) { - if (!SystemTray.isSupported()) { - log.warn("System tray is not supported"); - return; - } - - try { - systemTray = SystemTray.getSystemTray(); - - // Create popup menu - PopupMenu popup = new PopupMenu(); - - // Create menu items - MenuItem showItem = new MenuItem("Show"); - showItem.addActionListener( - e -> { - frame.setVisible(true); - frame.setState(Frame.NORMAL); - }); - - MenuItem exitItem = new MenuItem("Exit"); - exitItem.addActionListener( - e -> { - cleanup(); - System.exit(0); - }); - - // Add menu items to popup menu - popup.add(showItem); - popup.addSeparator(); - popup.add(exitItem); - - // Create tray icon - trayIcon = new TrayIcon(icon, "Stirling-PDF", popup); - trayIcon.setImageAutoSize(true); - - // Add double-click behavior - trayIcon.addActionListener( - e -> { - frame.setVisible(true); - frame.setState(Frame.NORMAL); - }); - - // Add tray icon to system tray - systemTray.add(trayIcon); - - // Modify frame behavior to minimize to tray - frame.addWindowStateListener( - new WindowStateListener() { - public void windowStateChanged(WindowEvent e) { - if (e.getNewState() == Frame.ICONIFIED) { - frame.setVisible(false); - } - } - }); - - } catch (AWTException e) { - log.error("Error setting up system tray icon", e); - } - } - - private void loadIcon() { - try { - Image icon = null; - String[] iconPaths = {"/static/favicon.ico"}; - - for (String path : iconPaths) { - if (icon != null) break; - try { - try (InputStream is = getClass().getResourceAsStream(path)) { - if (is != null) { - icon = ImageIO.read(is); - break; - } - } - } catch (Exception e) { - log.debug("Could not load icon from " + path, e); - } - } - - if (icon != null) { - frame.setIconImage(icon); - setupTrayIcon(icon); - } else { - log.warn("Could not load icon from any source"); - } - } catch (Exception e) { - log.error("Error loading icon", e); - } - } - - @PreDestroy - public void cleanup() { - if (browser != null) browser.close(true); - if (client != null) client.dispose(); - if (cefApp != null) cefApp.dispose(); - if (loadingWindow != null) loadingWindow.dispose(); - } - - public static void forceInitializeUI() { - try { - if (loadingWindow != null) { - log.info("Forcing start of UI initialization sequence"); - - // Close loading window first - loadingWindow.setVisible(false); - loadingWindow.dispose(); - loadingWindow = null; - log.info("Loading window disposed"); - - // Then setup the main frame - frame.setVisible(false); - frame.dispose(); - frame.setOpacity(1.0f); - frame.setUndecorated(false); - frame.pack(); - frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800)); - frame.setLocationRelativeTo(null); - log.debug("Frame reconfigured"); - - // Show the main frame - frame.setVisible(true); - frame.requestFocus(); - frame.toFront(); - log.info("Main frame displayed and focused"); - - // Focus the browser component if available - if (browser != null) { - Timer focusTimer = - new Timer( - 100, - e -> { - try { - browser.getUIComponent().requestFocus(); - log.info("Browser component focused"); - } catch (Exception ex) { - log.error( - "Error focusing browser during force ui" - + " initialization.", - ex); - } - }); - focusTimer.setRepeats(false); - focusTimer.start(); - } - } - } catch (Exception e) { - log.error("Error during Forced UI initialization.", e); - // Attempt cleanup on error - if (loadingWindow != null) { - loadingWindow.dispose(); - loadingWindow = null; - } - if (frame != null) { - frame.setVisible(true); - frame.setOpacity(1.0f); - frame.setUndecorated(false); - frame.requestFocus(); - } - } - } -} diff --git a/app/core/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java b/app/core/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java new file mode 100644 index 000000000..a3feded74 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java @@ -0,0 +1,157 @@ +package stirling.software.SPDF.config; + +import java.lang.management.ManagementFactory; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +/** + * Monitor for Tauri parent process to detect orphaned Java backend processes. When running in Tauri + * mode, this component periodically checks if the parent Tauri process is still alive. If the + * parent process terminates unexpectedly, this will trigger a graceful shutdown of the Java backend + * to prevent orphaned processes. + */ +@Component +@ConditionalOnProperty(name = "STIRLING_PDF_TAURI_MODE", havingValue = "true") +public class TauriProcessMonitor { + + private static final Logger logger = LoggerFactory.getLogger(TauriProcessMonitor.class); + + private final ApplicationContext applicationContext; + private String parentProcessId; + private ScheduledExecutorService scheduler; + private volatile boolean monitoring = false; + + public TauriProcessMonitor(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @PostConstruct + public void init() { + parentProcessId = System.getenv("TAURI_PARENT_PID"); + + if (parentProcessId != null && !parentProcessId.trim().isEmpty()) { + logger.info("Tauri mode detected. Parent process ID: {}", parentProcessId); + startMonitoring(); + } else { + logger.warn( + "TAURI_PARENT_PID environment variable not found. Tauri process monitoring disabled."); + } + } + + private void startMonitoring() { + scheduler = + Executors.newSingleThreadScheduledExecutor( + r -> { + Thread t = new Thread(r, "tauri-process-monitor"); + t.setDaemon(true); + return t; + }); + + monitoring = true; + + // Check every 5 seconds + scheduler.scheduleAtFixedRate(this::checkParentProcess, 5, 5, TimeUnit.SECONDS); + + logger.info("Started monitoring parent Tauri process (PID: {})", parentProcessId); + } + + private void checkParentProcess() { + if (!monitoring) { + return; + } + + try { + if (!isProcessAlive(parentProcessId)) { + logger.warn( + "Parent Tauri process (PID: {}) is no longer alive. Initiating graceful shutdown...", + parentProcessId); + initiateGracefulShutdown(); + } + } catch (Exception e) { + logger.error("Error checking parent process status", e); + } + } + + private boolean isProcessAlive(String pid) { + try { + long processId = Long.parseLong(pid); + + // Check if process exists using ProcessHandle (Java 9+) + return ProcessHandle.of(processId).isPresent(); + + } catch (NumberFormatException e) { + logger.error("Invalid parent process ID format: {}", pid); + return false; + } catch (Exception e) { + logger.error("Error checking if process {} is alive", pid, e); + return false; + } + } + + private void initiateGracefulShutdown() { + monitoring = false; + + logger.info("Orphaned Java backend detected. Shutting down gracefully..."); + + // Shutdown asynchronously to avoid blocking the monitor thread + CompletableFuture.runAsync( + () -> { + try { + // Give a small delay to ensure logging completes + Thread.sleep(1000); + + if (applicationContext instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) applicationContext).close(); + } else { + // Fallback to system exit + logger.warn( + "Unable to shutdown Spring context gracefully, using System.exit"); + System.exit(0); + } + } catch (Exception e) { + logger.error("Error during graceful shutdown", e); + System.exit(1); + } + }); + } + + @PreDestroy + public void cleanup() { + monitoring = false; + + if (scheduler != null && !scheduler.isShutdown()) { + logger.info("Shutting down Tauri process monitor"); + scheduler.shutdown(); + + try { + if (!scheduler.awaitTermination(2, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { + scheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + /** Get the current Java process ID for logging/debugging purposes */ + public static String getCurrentProcessId() { + try { + return ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; + } catch (Exception e) { + return "unknown"; + } + } +} diff --git a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index c3e204b3c..96212c6bf 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.config; import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -28,4 +29,17 @@ public class WebMvcConfig implements WebMvcConfigurer { "file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/"); // .setCachePeriod(0); // Optional: disable caching } + + @Override + public void addCorsMappings(CorsRegistry registry) { + if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"))) { + // Tauri mode CORS configuration + registry.addMapping("/**") + .allowedOrigins( + "http://localhost:5173", "http://tauri.localhost", "tauri://localhost") + .allowedMethods("*") + .allowedHeaders("*"); + return; + } + } } diff --git a/docker/frontend/entrypoint.sh b/docker/frontend/entrypoint.sh index a81272969..283e20a82 100644 --- a/docker/frontend/entrypoint.sh +++ b/docker/frontend/entrypoint.sh @@ -6,5 +6,13 @@ VITE_API_BASE_URL=${VITE_API_BASE_URL:-"http://backend:8080"} # Replace the placeholder in nginx.conf with the actual backend URL sed -i "s|\${VITE_API_BASE_URL}|${VITE_API_BASE_URL}|g" /etc/nginx/nginx.conf +# Inject runtime configuration into config.js +cat > /usr/share/nginx/html/config.js << EOF +// Runtime configuration - injected at container startup +window.runtimeConfig = { + apiBaseUrl: '${VITE_API_BASE_URL}' +}; +EOF + # Start nginx exec nginx -g "daemon off;" \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index 115fcca84..1640a506e 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -72,3 +72,15 @@ This section has moved here: [https://facebook.github.io/create-react-app/docs/d ### `npm run build` fails to minify This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) + + +## Tauri +### Dev +To run Tauri in development. Use the command: +````npm run tauri-dev``` +This will run the gradle runboot command and the tauri dev command concurrently, starting the app once both are stable. + +### Build +To build a deployment of the Tauri app. Use the command: +```npm run tauri-build``` +This will bundle the backend and frontend into one executable for each target. Targets can be set within the `tauri.conf.json` file. diff --git a/frontend/index.html b/frontend/index.html index 0fc165c66..964c2d4b8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -12,11 +12,12 @@ -