mirror of
https://github.com/pybind/pybind11.git
synced 2026-05-14 10:10:47 +00:00
* fix: segfault when moving `scoped_ostream_redirect` The default move constructor left the stream (`std::cout`) pointing at the moved-from `pythonbuf`, whose internal buffer and streambuf pointers were nulled by the move. Any subsequent write through the stream dereferenced null, causing a segfault. Replace `= default` with an explicit move constructor that re-points the stream to the new buffer and disarms the moved-from destructor. * fix: mark move constructor noexcept to satisfy clang-tidy * fix: use bool flag instead of nullptr sentinel for moved-from state Using `old == nullptr` as the moved-from sentinel was incorrect because nullptr is a valid original rdbuf() value (e.g. `std::ostream os(nullptr)`). Replace with an explicit `active` flag so the destructor correctly restores nullptr buffers. Add tests for the nullptr-rdbuf edge case. * fix: remove noexcept and propagate active flag from source - Remove noexcept: pythonbuf inherits from std::streambuf whose move is not guaranteed nothrow on all implementations. Suppress clang-tidy with NOLINTNEXTLINE instead. - Initialize active from other.active so that moving an already moved-from object does not incorrectly re-activate the redirect. - Only rebind the stream and disarm the source when active. * test: add unflushed ostream redirect regression Cover the buffered-before-move case for `scoped_ostream_redirect`, which still crashes despite the current move fix. This gives the PR a direct reproducer for the remaining bug path. Made-with: Cursor * fix: disarm moved-from pythonbuf after redirect move The redirect guard now survives moves, but buffered output could still remain in the moved-from `pythonbuf` and be flushed during destruction through moved-out Python handles. Rebuild the destination put area from the transferred storage and clear the source put area so unflushed bytes follow the active redirect instead of crashing in the moved-from destructor. Made-with: Cursor --------- Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
163 lines
5.5 KiB
C++
163 lines
5.5 KiB
C++
/*
|
|
tests/test_iostream.cpp -- Usage of scoped_output_redirect
|
|
|
|
Copyright (c) 2017 Henry F. Schreiner
|
|
|
|
All rights reserved. Use of this source code is governed by a
|
|
BSD-style license that can be found in the LICENSE file.
|
|
*/
|
|
|
|
#include <pybind11/iostream.h>
|
|
|
|
#include "pybind11_tests.h"
|
|
|
|
#include <atomic>
|
|
#include <iostream>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
void noisy_function(const std::string &msg, bool flush) {
|
|
|
|
std::cout << msg;
|
|
if (flush) {
|
|
std::cout << std::flush;
|
|
}
|
|
}
|
|
|
|
void noisy_funct_dual(const std::string &msg, const std::string &emsg) {
|
|
std::cout << msg;
|
|
std::cerr << emsg;
|
|
}
|
|
|
|
// object to manage C++ thread
|
|
// simply repeatedly write to std::cerr until stopped
|
|
// redirect is called at some point to test the safety of scoped_estream_redirect
|
|
struct TestThread {
|
|
TestThread() : stop_{false} {
|
|
auto thread_f = [this] {
|
|
static std::mutex cout_mutex;
|
|
while (!stop_) {
|
|
{
|
|
// #HelpAppreciated: Work on iostream.h thread safety.
|
|
// Without this lock, the clang ThreadSanitizer (tsan) reliably reports a
|
|
// data race, and this test is predictably flakey on Windows.
|
|
// For more background see the discussion under
|
|
// https://github.com/pybind/pybind11/pull/2982 and
|
|
// https://github.com/pybind/pybind11/pull/2995.
|
|
const std::lock_guard<std::mutex> lock(cout_mutex);
|
|
std::cout << "x" << std::flush;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::microseconds(50));
|
|
}
|
|
};
|
|
t_ = new std::thread(std::move(thread_f));
|
|
}
|
|
|
|
~TestThread() { delete t_; }
|
|
|
|
void stop() { stop_ = true; }
|
|
|
|
void join() const {
|
|
py::gil_scoped_release gil_lock;
|
|
t_->join();
|
|
}
|
|
|
|
void sleep() {
|
|
py::gil_scoped_release gil_lock;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
}
|
|
|
|
std::thread *t_{nullptr};
|
|
std::atomic<bool> stop_;
|
|
};
|
|
|
|
TEST_SUBMODULE(iostream, m) {
|
|
|
|
add_ostream_redirect(m);
|
|
|
|
// test_evals
|
|
|
|
m.def("captured_output_default", [](const std::string &msg) {
|
|
py::scoped_ostream_redirect redir;
|
|
std::cout << msg << std::flush;
|
|
});
|
|
|
|
m.def("captured_output", [](const std::string &msg) {
|
|
py::scoped_ostream_redirect redir(std::cout, py::module_::import("sys").attr("stdout"));
|
|
std::cout << msg << std::flush;
|
|
});
|
|
|
|
m.def("guard_output",
|
|
&noisy_function,
|
|
py::call_guard<py::scoped_ostream_redirect>(),
|
|
py::arg("msg"),
|
|
py::arg("flush") = true);
|
|
|
|
m.def("captured_err", [](const std::string &msg) {
|
|
py::scoped_ostream_redirect redir(std::cerr, py::module_::import("sys").attr("stderr"));
|
|
std::cerr << msg << std::flush;
|
|
});
|
|
|
|
m.def("noisy_function", &noisy_function, py::arg("msg"), py::arg("flush") = true);
|
|
|
|
m.def("dual_guard",
|
|
&noisy_funct_dual,
|
|
py::call_guard<py::scoped_ostream_redirect, py::scoped_estream_redirect>(),
|
|
py::arg("msg"),
|
|
py::arg("emsg"));
|
|
|
|
m.def("raw_output", [](const std::string &msg) { std::cout << msg << std::flush; });
|
|
|
|
m.def("raw_err", [](const std::string &msg) { std::cerr << msg << std::flush; });
|
|
|
|
m.def("captured_dual", [](const std::string &msg, const std::string &emsg) {
|
|
py::scoped_ostream_redirect redirout(std::cout, py::module_::import("sys").attr("stdout"));
|
|
py::scoped_ostream_redirect redirerr(std::cerr, py::module_::import("sys").attr("stderr"));
|
|
std::cout << msg << std::flush;
|
|
std::cerr << emsg << std::flush;
|
|
});
|
|
|
|
py::class_<TestThread>(m, "TestThread")
|
|
.def(py::init<>())
|
|
.def("stop", &TestThread::stop)
|
|
.def("join", &TestThread::join)
|
|
.def("sleep", &TestThread::sleep);
|
|
|
|
m.def("move_redirect_output", [](const std::string &msg_before, const std::string &msg_after) {
|
|
py::scoped_ostream_redirect redir1(std::cout, py::module_::import("sys").attr("stdout"));
|
|
std::cout << msg_before << std::flush;
|
|
py::scoped_ostream_redirect redir2(std::move(redir1));
|
|
std::cout << msg_after << std::flush;
|
|
});
|
|
|
|
m.def("move_redirect_output_unflushed",
|
|
[](const std::string &msg_before, const std::string &msg_after) {
|
|
py::scoped_ostream_redirect redir1(std::cout,
|
|
py::module_::import("sys").attr("stdout"));
|
|
std::cout << msg_before;
|
|
py::scoped_ostream_redirect redir2(std::move(redir1));
|
|
std::cout << msg_after << std::flush;
|
|
});
|
|
|
|
// Redirect a stream whose original rdbuf is nullptr, then move the redirect.
|
|
// Verifies that nullptr is correctly restored (not confused with a moved-from sentinel).
|
|
m.def("move_redirect_null_rdbuf", [](const std::string &msg) {
|
|
std::ostream os(nullptr);
|
|
py::scoped_ostream_redirect redir1(os, py::module_::import("sys").attr("stdout"));
|
|
os << msg << std::flush;
|
|
py::scoped_ostream_redirect redir2(std::move(redir1));
|
|
os << msg << std::flush;
|
|
// After redir2 goes out of scope, os.rdbuf() should be restored to nullptr.
|
|
});
|
|
|
|
m.def("get_null_rdbuf_restored", [](const std::string &msg) -> bool {
|
|
std::ostream os(nullptr);
|
|
{
|
|
py::scoped_ostream_redirect redir(os, py::module_::import("sys").attr("stdout"));
|
|
os << msg << std::flush;
|
|
}
|
|
return os.rdbuf() == nullptr;
|
|
});
|
|
}
|