diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index b375cc4c56..24f82c79df 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -292,11 +292,21 @@ FetchContent_MakeAvailable(papilo) # PSLP - Lightweight C presolver for linear programs # https://github.com/dance858/PSLP +# +# v0.0.11 ignores its own verbose=false setting for one message: run_presolver() prints +# "PSLP declares problem as infeasible[.| or unbounded.]" unconditionally, unlike every other +# console message in that function, which are all gated on verbose. cuOpt sets verbose = false +# (see third_party_presolve.cpp) precisely so PSLP stays silent, so this writes unexpectedly +# straight to the process's native stdout -- observed corrupting Maven Surefire's forked-JVM +# protocol in the Java bindings, which also uses stdout as its own channel. Patched upstream at +# https://github.com/dance858/PSLP/pull/55; drop this patch once a release containing it is +# available and this GIT_TAG is bumped past it. FetchContent_Declare( pslp GIT_REPOSITORY "https://github.com/dance858/PSLP.git" GIT_TAG "v0.0.11" GIT_PROGRESS TRUE + PATCH_COMMAND sh -c "git apply --check '${CMAKE_CURRENT_SOURCE_DIR}/cmake/patches/pslp/respect_verbose_for_infeasible_message.patch' 2>/dev/null && git apply '${CMAKE_CURRENT_SOURCE_DIR}/cmake/patches/pslp/respect_verbose_for_infeasible_message.patch'; true" EXCLUDE_FROM_ALL SYSTEM ) diff --git a/cpp/cmake/patches/pslp/respect_verbose_for_infeasible_message.patch b/cpp/cmake/patches/pslp/respect_verbose_for_infeasible_message.patch new file mode 100644 index 0000000000..784ab23370 --- /dev/null +++ b/cpp/cmake/patches/pslp/respect_verbose_for_infeasible_message.patch @@ -0,0 +1,15 @@ +diff --git a/src/core/Presolver.c b/src/core/Presolver.c +index c0bdc9e..426008e 100644 +--- a/src/core/Presolver.c ++++ b/src/core/Presolver.c +@@ -720,6 +720,9 @@ PresolveStatus run_presolver(Presolver *presolver) + if (status != UNCHANGED) + { + // problem detected to be infeasible or unbounded +- print_infeas_or_unbnd_message(status); ++ if (stgs->verbose) ++ { ++ print_infeas_or_unbnd_message(status); ++ } + return status; + } diff --git a/cpp/src/utilities/logger.cpp b/cpp/src/utilities/logger.cpp index 217f9c64cb..7b2170db34 100644 --- a/cpp/src/utilities/logger.cpp +++ b/cpp/src/utilities/logger.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -55,6 +55,25 @@ log_buffer& global_log_buffer() return buffer; } +// Overrides the sink used when log_to_console is true. Null (the default) keeps writing to +// std::cout; set by language bindings whose host runtime cannot safely receive writes to the +// native stdout stream -- for example Java, where a raw write there bypasses System.out and can +// corrupt tools that intercept it, such as Maven Surefire's forked-process protocol. +static std::mutex g_console_callback_mutex; +static log_console_callback_t g_console_callback = nullptr; + +void set_console_log_callback(log_console_callback_t callback) +{ + std::lock_guard lock(g_console_callback_mutex); + g_console_callback = callback; +} + +static log_console_callback_t console_log_callback() +{ + std::lock_guard lock(g_console_callback_mutex); + return g_console_callback; +} + // Callback function for the buffer sink static void buffer_log_callback(int lvl, const char* msg) { @@ -161,8 +180,13 @@ init_logger_t::init_logger_t(std::string log_file, bool log_to_console) // re-initialize sinks if (log_to_console) { - cuopt::default_logger().sinks().push_back( - std::make_shared(std::cout)); + if (auto callback = console_log_callback(); callback != nullptr) { + cuopt::default_logger().sinks().push_back( + std::make_shared(callback)); + } else { + cuopt::default_logger().sinks().push_back( + std::make_shared(std::cout)); + } } if (!log_file.empty()) { cuopt::default_logger().sinks().push_back( diff --git a/cpp/src/utilities/logger.hpp b/cpp/src/utilities/logger.hpp index 2f9053b05f..cc0f79175e 100644 --- a/cpp/src/utilities/logger.hpp +++ b/cpp/src/utilities/logger.hpp @@ -38,6 +38,19 @@ rapids_logger::logger& default_logger(); */ void reset_default_logger(); +using log_console_callback_t = void (*)(int level, const char* message); + +/** + * @brief Overrides the sink used for console logging (settings.log_to_console == true). + * + * Passing nullptr (the default) restores writing to std::cout. Intended for language bindings + * whose host runtime cannot safely receive a raw write to the native stdout stream -- see the + * definition site in logger.cpp for why that matters. + * + * @param callback The callback to invoke for each logged line, or nullptr to restore std::cout. + */ +void set_console_log_callback(log_console_callback_t callback); + // Ref-counted logger initializer class init_logger_t { // Using shared_ptr for ref-counting diff --git a/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicaloptimization/NativeLogSink.java b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicaloptimization/NativeLogSink.java new file mode 100644 index 0000000000..1656aaea6f --- /dev/null +++ b/java/cuopt/src/main/java/com/nvidia/cuopt/mathematicaloptimization/NativeLogSink.java @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuopt.mathematicaloptimization; + +/** + * Receives cuOpt's console log lines from native code and writes them through {@link + * System#out}, rather than the native library writing to the process's stdout stream directly. + * + *

A direct native write bypasses {@code System.out}, so it is invisible to anything that + * intercepts or redirects it -- {@link System#setOut}, a logging framework bridge, or Maven + * Surefire, which uses the forked JVM's stdout as its own communication channel and can + * misinterpret an unexpected raw write on it as the forked process having crashed. + * + *

Called from {@code cuopt_jni.cpp}; not part of the public API. + */ +final class NativeLogSink { + private NativeLogSink() {} + + static void onLogLine(String message) { + System.out.print(message); + } +} diff --git a/java/cuopt/src/main/native/cuopt_jni.cpp b/java/cuopt/src/main/native/cuopt_jni.cpp index 2f7cc27ef1..439ac63cd1 100644 --- a/java/cuopt/src/main/native/cuopt_jni.cpp +++ b/java/cuopt/src/main/native/cuopt_jni.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include @@ -378,6 +379,57 @@ void mip_set_solution_callback(cuopt_float_t* solution, if (detach) { g_jvm->DetachCurrentThread(); } } +jclass g_log_sink_class = nullptr; +jmethodID g_log_sink_method = nullptr; +std::once_flag g_log_sink_once; + +// cuopt::log_console_callback_t: forwards a console log line to NativeLogSink.onLogLine, so it +// is written through System.out instead of directly to the native stdout stream. See +// register_console_log_sink for why that distinction matters. +void console_log_callback(int /* level */, const char* message) +{ + if (g_log_sink_class == nullptr || g_log_sink_method == nullptr) { return; } + + bool detach = false; + JNIEnv* env = get_callback_env(detach); + if (env == nullptr) { return; } + + jstring line = env->NewStringUTF(message); + if (line != nullptr) { + env->CallStaticVoidMethod(g_log_sink_class, g_log_sink_method, line); + // A logging call is not the place to raise a Java exception; drop it rather than leave it + // pending for whatever JNI call happens to run next on this thread. + if (env->ExceptionCheck() == JNI_TRUE) { env->ExceptionClear(); } + env->DeleteLocalRef(line); + } + + if (detach) { g_jvm->DetachCurrentThread(); } +} + +// Registers console_log_callback with the native logger, once. Done lazily on first use (rather +// than in JNI_OnLoad) because FindClass needs the caller's classloader, which JNI_OnLoad does not +// reliably have. +void register_console_log_sink(JNIEnv* env) +{ + std::call_once(g_log_sink_once, [env]() { + jclass local_cls = env->FindClass("com/nvidia/cuopt/mathematicaloptimization/NativeLogSink"); + if (local_cls == nullptr) { + env->ExceptionClear(); + return; + } + jmethodID method = env->GetStaticMethodID(local_cls, "onLogLine", "(Ljava/lang/String;)V"); + if (method == nullptr) { + env->ExceptionClear(); + env->DeleteLocalRef(local_cls); + return; + } + g_log_sink_class = static_cast(env->NewGlobalRef(local_cls)); + g_log_sink_method = method; + env->DeleteLocalRef(local_cls); + cuopt::set_console_log_callback(&console_log_callback); + }); +} + } // namespace extern "C" jint JNI_OnLoad(JavaVM* vm, void*) @@ -421,6 +473,7 @@ Java_com_nvidia_cuopt_mathematicaloptimization_NativeCuOpt_readProblemWithFormat extern "C" JNIEXPORT jlong JNICALL Java_com_nvidia_cuopt_mathematicaloptimization_NativeCuOpt_createSolverSettings(JNIEnv* env, jclass) { + register_console_log_sink(env); cuOptSolverSettings settings = nullptr; if (!check_status(env, cuOptCreateSolverSettings(&settings), "cuOptCreateSolverSettings")) { return 0;