|
18 | 18 | #include <utility> |
19 | 19 | #include <vector> |
20 | 20 |
|
| 21 | +#include "boost/asio/error.hpp" |
21 | 22 | #include "boost/asio/io_context.hpp" |
22 | 23 | #include "boost/asio/ip/tcp.hpp" |
| 24 | +#include "boost/asio/post.hpp" |
| 25 | +#include "boost/asio/steady_timer.hpp" |
| 26 | +#include "boost/asio/strand.hpp" |
| 27 | +#include "boost/system/error_code.hpp" |
23 | 28 | // NOLINTNEXTLINE(misc-include-cleaner) |
24 | 29 | #include "boost/beast/core.hpp" |
25 | 30 | // NOLINTNEXTLINE(misc-include-cleaner) |
@@ -168,6 +173,13 @@ void WebServer::serve(int port) |
168 | 173 | logger_->addSink(log_sink_); |
169 | 174 | viewer_hook_->setDrainLogsFn( |
170 | 175 | [log_sink = std::move(log_sink)]() { log_sink->drainToClients(); }); |
| 176 | + // Flush WebLogSink at the end of every Tcl eval so log output |
| 177 | + // emitted during a command reaches clients before the response |
| 178 | + // carrying the Tcl result. viewer_hook_ outlives every io thread |
| 179 | + // that can run a request handler (stop() joins io threads before |
| 180 | + // resetting viewer_hook_), so the raw pointer capture is safe. |
| 181 | + tcl_eval->drain_output |
| 182 | + = [hook = viewer_hook_.get()]() { hook->drainLogs(); }; |
171 | 183 |
|
172 | 184 | TileGenerator::setDebugOverlayCallback( |
173 | 185 | [weak_gen = std::weak_ptr<TileGenerator>(generator_), |
@@ -210,6 +222,17 @@ void WebServer::serve(int port) |
210 | 222 |
|
211 | 223 | const std::string url = "http://localhost:" + std::to_string(handle.port); |
212 | 224 |
|
| 225 | + // Bind the timer to a strand so all timer operations (expires_after, |
| 226 | + // async_wait, cancel) run serialized on a single io thread. Without |
| 227 | + // this, stop()'s cancel from the caller thread would race with the |
| 228 | + // async_wait handler rescheduling on a worker thread — the same |
| 229 | + // steady_timer cannot be safely mutated from multiple threads. The |
| 230 | + // initial scheduleLogDrain() below runs before io threads start, so |
| 231 | + // it is single-threaded by construction. |
| 232 | + log_drain_timer_ = std::make_unique<net::steady_timer>( |
| 233 | + net::make_strand(ioc_->get_executor())); |
| 234 | + scheduleLogDrain(); |
| 235 | + |
213 | 236 | threads_.reserve(num_threads); |
214 | 237 | for (int i = 0; i < num_threads; ++i) { |
215 | 238 | threads_.emplace_back([this] { ioc_->run(); }); |
@@ -275,6 +298,32 @@ int WebServer::tclExitHandler(ClientData clientData, |
275 | 298 | return TCL_ERROR; |
276 | 299 | } |
277 | 300 |
|
| 301 | +// Drain interval chosen to keep the browser console feeling live without |
| 302 | +// burning CPU when nothing is logged. An idle tick costs one mutex |
| 303 | +// acquire + empty-buffer check inside WebLogSink::drainToClients. |
| 304 | +static constexpr auto kLogDrainInterval = std::chrono::milliseconds(250); |
| 305 | + |
| 306 | +void WebServer::scheduleLogDrain() |
| 307 | +{ |
| 308 | + if (!log_drain_timer_) { |
| 309 | + return; |
| 310 | + } |
| 311 | + log_drain_timer_->expires_after(kLogDrainInterval); |
| 312 | + log_drain_timer_->async_wait([this](const boost::system::error_code& ec) { |
| 313 | + // operation_aborted means cancel() was called from stop(). Any |
| 314 | + // other error (or none) means the timer fired normally — drain and |
| 315 | + // reschedule. ioc_->stop() in stop() will discard a re-armed timer |
| 316 | + // before its next firing, so no UAF risk on shutdown. |
| 317 | + if (ec == net::error::operation_aborted) { |
| 318 | + return; |
| 319 | + } |
| 320 | + if (viewer_hook_) { |
| 321 | + viewer_hook_->drainLogs(); |
| 322 | + } |
| 323 | + scheduleLogDrain(); |
| 324 | + }); |
| 325 | +} |
| 326 | + |
278 | 327 | void WebServer::requestStop() |
279 | 328 | { |
280 | 329 | if (!isRunning()) { |
@@ -316,7 +365,20 @@ void WebServer::stop() |
316 | 365 | shutdown_listener_(); |
317 | 366 | shutdown_listener_ = {}; |
318 | 367 | } |
| 368 | + // Cancel the periodic log drain so its handler stops re-arming. The |
| 369 | + // cancel is posted onto the timer's strand so it runs serialized with |
| 370 | + // scheduleLogDrain — calling cancel() directly from the caller thread |
| 371 | + // would race with the async_wait handler mutating the timer on an io |
| 372 | + // thread. Any in-flight handler completes normally; a re-armed timer |
| 373 | + // is discarded by ioc_->stop() below. |
| 374 | + if (log_drain_timer_) { |
| 375 | + auto* timer = log_drain_timer_.get(); |
| 376 | + net::post(timer->get_executor(), [timer] { timer->cancel(); }); |
| 377 | + } |
319 | 378 | stopAndJoinIoThreads(); |
| 379 | + // Reset only after threads are joined so no handler can dereference |
| 380 | + // the timer mid-shutdown. |
| 381 | + log_drain_timer_.reset(); |
320 | 382 | // Release without destroying — destroying io_context can crash on |
321 | 383 | // residual async handlers. Leak is bounded (at most one io_context |
322 | 384 | // per serve/stop cycle). |
|
0 commit comments