diff --git a/.github/actions/build-macos-x86_64/action.yml b/.github/actions/build-macos-x86_64/action.yml index 5320fe5e..7536707f 100644 --- a/.github/actions/build-macos-x86_64/action.yml +++ b/.github/actions/build-macos-x86_64/action.yml @@ -28,14 +28,14 @@ runs: shell: bash - name: Set up WebRTC cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/${{ env.WEBRTC_INSTALL_FOLDER }} key: webrtc-${{ env.WEBRTC_CACHE_BRANCH }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt') }}-${{ hashFiles('webrtc-jni/pom.xml') }} restore-keys: webrtc-${{ env.WEBRTC_CACHE_BRANCH }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt') }}- - name: Set up Maven cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: maven-${{ inputs.platform-name }}-${{ hashFiles('**/pom.xml') }} diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 99ca8e90..887b1aa4 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -28,7 +28,7 @@ runs: shell: bash - name: Set up WebRTC cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/${{ env.WEBRTC_INSTALL_FOLDER }} key: webrtc-${{ env.WEBRTC_CACHE_BRANCH }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt') }}-${{ hashFiles('webrtc-jni/pom.xml') }} @@ -39,14 +39,14 @@ runs: shell: bash - name: Set up FFmpeg cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/ffmpeg key: ffmpeg-${{ env.FFMPEG_VERSION }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt') }}-${{ hashFiles('webrtc-java-media/pom.xml') }} restore-keys: ffmpeg-${{ env.FFMPEG_VERSION }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt') }}- - name: Set up Maven cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: maven-${{ inputs.platform-name }}-${{ hashFiles('**/pom.xml') }} diff --git a/.github/actions/prepare-linux/action.yml b/.github/actions/prepare-linux/action.yml index 23f07c40..9fc88bfc 100644 --- a/.github/actions/prepare-linux/action.yml +++ b/.github/actions/prepare-linux/action.yml @@ -34,12 +34,16 @@ runs: # nasm is what FFmpeg assembles its x86 code with. sudo apt install -y binutils cmake git locales lsb-release nasm ninja-build pipewire pipewire-pulse pkg-config python3 python3-setuptools rsync unzip wget xz-utils - # Chromium Clang to be used with the clang toolchain file - #curl -s https://raw.githubusercontent.com/chromium/chromium/main/tools/clang/scripts/update.py | python3 - --output-dir=/opt/clang - # Use a more stable version of Clang + # Chromium Clang to be used with the clang toolchain file. The JNI code + # compiles against the libc++ headers of the WebRTC build, which only + # support the Clang that WebRTC branch ships with, so this must be the + # package pinned by CLANG_REVISION and CLANG_SUB_REVISION in + # tools/clang/scripts/update.py of the webrtc.branch set in + # webrtc-jni/pom.xml. Update it whenever that branch changes. + CLANG_PACKAGE=clang-llvmorg-23-init-19482-g53d18800-1.tar.xz sudo mkdir -p /opt/clang - wget https://commondatastorage.googleapis.com/chromium-browser-clang/Linux_x64/clang-llvmorg-20-init-9764-gb81d8e90-72.tar.xz - sudo tar -xvf clang-llvmorg-20-init-9764-gb81d8e90-72.tar.xz -C /opt/clang + wget https://commondatastorage.googleapis.com/chromium-browser-clang/Linux_x64/$CLANG_PACKAGE + sudo tar -xvf $CLANG_PACKAGE -C /opt/clang shell: bash - name: Install required packages for x86-64 diff --git a/.github/actions/prepare-windows/action.yml b/.github/actions/prepare-windows/action.yml index dd0e96a5..692cf2bf 100644 --- a/.github/actions/prepare-windows/action.yml +++ b/.github/actions/prepare-windows/action.yml @@ -7,7 +7,7 @@ runs: steps: - name: Set up Python 3.11 if: false - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.11" diff --git a/.github/actions/release-macos-x86_64/action.yml b/.github/actions/release-macos-x86_64/action.yml index c14d53d5..ba402521 100644 --- a/.github/actions/release-macos-x86_64/action.yml +++ b/.github/actions/release-macos-x86_64/action.yml @@ -20,14 +20,14 @@ runs: shell: bash - name: Set up WebRTC cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/${{ env.WEBRTC_INSTALL_FOLDER }} key: webrtc-${{ env.WEBRTC_CACHE_BRANCH }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt') }}-${{ hashFiles('webrtc-jni/pom.xml') }} restore-keys: webrtc-${{ env.WEBRTC_CACHE_BRANCH }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt') }}- - name: Set up Maven cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: maven-${{ inputs.platform-name }}-${{ hashFiles('**/pom.xml') }} @@ -52,7 +52,7 @@ runs: # deploy. The native library jar is handed to the aggregating publish-central # job instead, which attaches every platform to a single deployment. - name: Upload native library jar - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: natives-${{ inputs.platform-name }} path: webrtc-jni/target/webrtc-java-*.jar diff --git a/.github/actions/release/action.yml b/.github/actions/release/action.yml index 0c6fe4af..fc4a5a6b 100644 --- a/.github/actions/release/action.yml +++ b/.github/actions/release/action.yml @@ -19,7 +19,7 @@ runs: shell: bash - name: Set up WebRTC cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/${{ env.WEBRTC_INSTALL_FOLDER }} key: webrtc-${{ env.WEBRTC_CACHE_BRANCH }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-jni/src/main/cpp/dependencies/webrtc/CMakeLists.txt') }}-${{ hashFiles('webrtc-jni/pom.xml') }} @@ -30,14 +30,14 @@ runs: shell: bash - name: Set up FFmpeg cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/ffmpeg key: ffmpeg-${{ env.FFMPEG_VERSION }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt') }}-${{ hashFiles('webrtc-java-media/pom.xml') }} restore-keys: ffmpeg-${{ env.FFMPEG_VERSION }}-${{ inputs.platform-name }}-${{ hashFiles('webrtc-java-media/src/main/cpp/dependencies/ffmpeg/CMakeLists.txt') }}- - name: Set up Maven cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: maven-${{ inputs.platform-name }}-${{ hashFiles('**/pom.xml') }} @@ -71,7 +71,7 @@ runs: # deploy. The native library jar is handed to the aggregating publish-central # job instead, which attaches every platform to a single deployment. - name: Upload native library jar - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: natives-${{ inputs.platform-name }} path: webrtc-jni/target/webrtc-java-*.jar diff --git a/.github/actions/test-natives/action.yml b/.github/actions/test-natives/action.yml index 02459615..6a09796d 100644 --- a/.github/actions/test-natives/action.yml +++ b/.github/actions/test-natives/action.yml @@ -34,7 +34,7 @@ runs: using: "composite" steps: - name: Download natives - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: natives-${{ inputs.platform-name }} path: natives diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4de02cd7..1a195411 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: - name: Upload natives for native testing if: matrix.platform.name == 'windows_arm64' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: natives-${{ matrix.platform.name }} path: | @@ -96,7 +96,7 @@ jobs: - name: Upload natives for native testing if: matrix.platform.name == 'linux_arm' || matrix.platform.name == 'linux_arm64' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: natives-${{ matrix.platform.name }} path: | diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 9eb41846..e4a23ee7 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -32,7 +32,7 @@ jobs: cache-dependency-path: docs/package-lock.json - name: Setup Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@v6 - name: Install dependencies run: | @@ -45,7 +45,7 @@ jobs: npm run build - name: Upload artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: docs/.vitepress/dist @@ -59,4 +59,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v5 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 33085969..2ee8ba10 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -278,7 +278,7 @@ jobs: fetch-tags: true - name: Set up Maven cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: maven-publish-${{ hashFiles('**/pom.xml') }} @@ -311,7 +311,7 @@ jobs: echo "version=$version" >> "$GITHUB_OUTPUT" - name: Download the native library jars of all platforms - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: natives-* path: natives @@ -380,7 +380,7 @@ jobs: - name: Upload the deployment bundle for inspection if: ${{ inputs.dryRun }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: deployment-bundle-dry-run path: | diff --git a/webrtc-jni/src/main/cpp/include/JNI_RTCPeerConnection.h b/webrtc-jni/src/main/cpp/include/JNI_RTCPeerConnection.h index fd3aaea9..32ac991a 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_RTCPeerConnection.h +++ b/webrtc-jni/src/main/cpp/include/JNI_RTCPeerConnection.h @@ -151,7 +151,6 @@ extern "C" { JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_addIceCandidate (JNIEnv *, jobject, jobject); - /* /* * Class: dev_onvoid_webrtc_RTCPeerConnection * Method: getSignalingState diff --git a/webrtc-jni/src/main/cpp/src/api/RTCDataChannelSendObserver.cpp b/webrtc-jni/src/main/cpp/src/api/RTCDataChannelSendObserver.cpp index b8e1f34d..9758efc3 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCDataChannelSendObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCDataChannelSendObserver.cpp @@ -3,6 +3,44 @@ #include "JavaString.h" #include "JNI_WebRTC.h" +namespace +{ + // Hands an exception thrown by the observer to the calling thread's uncaught + // exception handler, as the JVM does for an exception escaping a Java thread. + // ExceptionDescribe would print the "Exception in thread" prefix straight to + // native stderr and the stack trace to System.err, splitting the report + // across two streams and leaving the prefix on an unterminated line. + void ReportUncaughtException(JNIEnv * env, jthrowable exception) + { + jni::JavaLocalRef threadClass(env, env->FindClass("java/lang/Thread")); + jni::JavaLocalRef handlerClass(env, env->FindClass("java/lang/Thread$UncaughtExceptionHandler")); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return; + } + + jmethodID currentThread = env->GetStaticMethodID(threadClass.get(), "currentThread", "()Ljava/lang/Thread;"); + jmethodID getHandler = env->GetMethodID(threadClass.get(), "getUncaughtExceptionHandler", + "()Ljava/lang/Thread$UncaughtExceptionHandler;"); + jmethodID uncaughtException = env->GetMethodID(handlerClass.get(), "uncaughtException", + "(Ljava/lang/Thread;Ljava/lang/Throwable;)V"); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return; + } + + jni::JavaLocalRef thread(env, env->CallStaticObjectMethod(threadClass.get(), currentThread)); + jni::JavaLocalRef handler(env, env->CallObjectMethod(thread.get(), getHandler)); + if (!env->ExceptionCheck() && handler.get()) { + env->CallVoidMethod(handler.get(), uncaughtException, thread.get(), exception); + } + // Like the JVM, ignore an exception thrown by the handler itself. + if (env->ExceptionCheck()) { + env->ExceptionClear(); + } + } +} + namespace jni { RTCDataChannelSendObserver::RTCDataChannelSendObserver(JNIEnv * env, jobject observer) : @@ -58,8 +96,9 @@ namespace jni } // A Java exception must not escape into WebRTC's network task or a destructor. if (env->ExceptionCheck()) { - env->ExceptionDescribe(); + JavaLocalRef exception(env, env->ExceptionOccurred()); env->ExceptionClear(); + ReportUncaughtException(env, exception.get()); } } diff --git a/webrtc/src/main/java/module-info.java b/webrtc/src/main/java/module-info.java index 999476b6..743fff91 100644 --- a/webrtc/src/main/java/module-info.java +++ b/webrtc/src/main/java/module-info.java @@ -1,3 +1,6 @@ +// The qualified export below names webrtc.java.media, which depends on this +// module and so is never on the module path while this module compiles. +@SuppressWarnings("module") module webrtc.java { requires java.desktop; diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDataChannelSendCompletionTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDataChannelSendCompletionTests.java index cf12c1ed..80ff23d9 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDataChannelSendCompletionTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDataChannelSendCompletionTests.java @@ -58,7 +58,8 @@ void rejectsInvalidArgumentsWithoutCallingObserver() { @Test void callbackExceptionDoesNotStopLaterSends() throws Exception { - try (TestDataChannelPair pair = new TestDataChannelPair(factory)) { + try (UncaughtCapture uncaught = new UncaughtCapture(); + TestDataChannelPair pair = new TestDataChannelPair(factory)) { pair.connect(); CountDownLatch entered = new CountDownLatch(1); RTCDataChannelBuffer buffer = new RTCDataChannelBuffer(ByteBuffer.allocate(1), true); @@ -80,12 +81,14 @@ public void onFailure(String error) { pair.sender.sendAsync(buffer, next); assertNull(next.result.get(5, TimeUnit.SECONDS)); assertEquals(1, next.calls.get()); + uncaught.assertReceived("Send observer test exception"); } } @Test void failureCallbackExceptionDoesNotStopLaterSends() throws Exception { - try (TestDataChannelPair pair = new TestDataChannelPair(factory)) { + try (UncaughtCapture uncaught = new UncaughtCapture(); + TestDataChannelPair pair = new TestDataChannelPair(factory)) { CountDownLatch entered = new CountDownLatch(1); RTCDataChannelBuffer buffer = new RTCDataChannelBuffer(ByteBuffer.allocate(1), true); pair.sender.sendAsync(buffer, new RTCDataChannelSendObserver() { @@ -103,6 +106,7 @@ public void onFailure(String error) { }); assertTrue(entered.await(5, TimeUnit.SECONDS), "Observer was not called"); assertRejected(pair.sender); + uncaught.assertReceived("Send failure observer test exception"); } } @@ -196,4 +200,28 @@ public void onFailure(String error) { result.complete(error); } } + + /** + * Replaces the default uncaught exception handler while open, so an exception + * an observer throws on purpose is captured and asserted instead of printed. + */ + private static class UncaughtCapture implements AutoCloseable { + final CompletableFuture exception = new CompletableFuture<>(); + final Thread.UncaughtExceptionHandler previous = Thread.getDefaultUncaughtExceptionHandler(); + + UncaughtCapture() { + Thread.setDefaultUncaughtExceptionHandler((thread, e) -> exception.complete(e)); + } + + void assertReceived(String message) throws Exception { + Throwable received = exception.get(5, TimeUnit.SECONDS); + assertEquals(IllegalStateException.class, received.getClass()); + assertEquals(message, received.getMessage()); + } + + @Override + public void close() { + Thread.setDefaultUncaughtExceptionHandler(previous); + } + } }