Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
736d096
feat: apply clang-format and add web GUI test files
jorge-ferreira-pii Apr 25, 2026
8f76516
fix: apply clang-tidy suggestions
jorge-ferreira-pii Apr 25, 2026
e33fd5a
fix: remove stdio.h in favor of cstdio to resolve clang-tidy warnings
jorge-ferreira-pii Apr 25, 2026
6b0d6b4
Merge master
jorge-ferreira-pii Apr 26, 2026
e3efb15
Update Web GUI and odb tests
jorge-ferreira-pii Apr 26, 2026
2be6d04
Merge branch 'master' into feature-Web-GUI
jorge-ferreira-pii Apr 27, 2026
a3f4135
Fix clang-format violations in web_serve.cpp
jorge-ferreira-pii Apr 27, 2026
9d24c6e
Fix formatting and missing brace in web_serve.cpp
jorge-ferreira-pii Apr 27, 2026
45dd46b
Fix duplicate declaration of log_sink_ in web.h
jorge-ferreira-pii Apr 27, 2026
b06b49a
Merge remote-tracking branch 'origin/master' into feature-Web-GUI
jorge-ferreira-pii Apr 27, 2026
d448ede
Merge remote-tracking branch 'origin/master' into feature-Web-GUI
jorge-ferreira-pii Apr 29, 2026
e731680
fix(web): fix server shutdown from browser exit.
jorge-ferreira-pii Apr 29, 2026
cfd4782
fix(web): link web library against gui
maliberty May 1, 2026
867d60d
Merge remote-tracking branch 'origin/master' into feature-Web-GUI
maliberty May 1, 2026
9358987
refactor(web): unify browser-exit shutdown via Tcl-result sentinel
maliberty May 1, 2026
d1c29e3
fix(web): detach browser launch with setsid; correct exit UX text
maliberty May 2, 2026
8257d3d
chore(web): untrack generated messages.txt
maliberty May 2, 2026
7ca7e75
fix(web): unbreak test linkage by decoupling web from gui.a
maliberty May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/web/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ target_link_libraries(web
PUBLIC
odb
PRIVATE
gui
gui_descriptors
gui_heatmap_core
dbSta_lib
Expand Down
24 changes: 17 additions & 7 deletions src/web/include/web/web.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#pragma once

#include <spdlog/common.h>

#include <condition_variable>
#include <cstdint>
#include <functional>
Expand Down Expand Up @@ -108,29 +110,37 @@ class WebServer
double dbu_per_pixel,
const std::string& vis_json);

private:
// Tears down the I/O threads and cleans up hooks. Called by the
// destructor; safe to call multiple times.
// Tears down the I/O threads and cleans up hooks. Safe to call multiple
// times and from any thread; after it returns, isRunning() is false and
// serve() may be called again to restart the server.
void stop();

private:
// Stops ioc_, joins every worker thread except the current one, and
// clears threads_. Detaches the current thread if it happens to be a
// worker (would otherwise raise EDEADLK on self-join).
void stopAndJoinIoThreads();

odb::dbDatabase* db_ = nullptr;
sta::dbSta* sta_ = nullptr;
utl::Logger* logger_ = nullptr;
Tcl_Interp* interp_ = nullptr;
int num_threads_ = 0;
std::shared_ptr<TileGenerator> generator_;
std::unique_ptr<WebViewerHook> viewer_hook_;
std::shared_ptr<spdlog::sinks::sink> log_sink_;

// Background I/O context and worker threads (non-null while running).
std::unique_ptr<boost::asio::io_context> ioc_;
std::vector<std::thread> threads_;

// Type-erased callback that closes the Listener's acceptor before the
// io_context is destroyed — prevents a crash where the acceptor's
// destructor references the io_context that's mid-destruction.
// Closes the Listener's acceptor before the io_context is destroyed,
// avoiding a crash where the acceptor references a half-destroyed
// io_context.
std::function<void()> shutdown_listener_;

// Held so stop() can remove it from the Logger before viewer_hook_ is
// destroyed — the sink stores a raw pointer into the hook.
spdlog::sink_ptr log_sink_;
Comment thread
maliberty marked this conversation as resolved.
// Blocking support: waitForStop() sleeps on stop_cv_ until
// requestStop() sets stop_requested_.
std::mutex stop_mutex_;
Expand Down
28 changes: 28 additions & 0 deletions src/web/messages.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
GUI 0019 web.tcl:125 Display option must have 2 elements {control name} {value}. ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.tcl#L125
WEB 0001 web_serve.cpp:230 Server started on {}. INFO https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web_serve.cpp#L230
WEB 0002 web_serve.cpp:234 Server error : {} ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web_serve.cpp#L234
WEB 0006 web_serve.cpp:119 Web server is already running. WARN https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web_serve.cpp#L119
WEB 0010 web.cpp:746 Failed to open acceptor: {} ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.cpp#L746
WEB 0011 web.cpp:751 Failed to set reuse_address option: {} ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.cpp#L751
WEB 0017 web.cpp:757 Failed to bind to endpoint: {} ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.cpp#L757
WEB 0018 web.cpp:763 Failed to listen: {} ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.cpp#L763
WEB 0020 tile_generator.cpp:1705 No design loaded. ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/tile_generator.cpp#L1705
WEB 0021 tile_generator.cpp:1737 Invalid image dimensions. ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/tile_generator.cpp#L1737
WEB 0022 tile_generator.cpp:1744 Image dimensions {}x{} exceed max {}; clamping. WARN https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/tile_generator.cpp#L1744
WEB 0023 tile_generator.cpp:1861 PNG encode error: {} ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/tile_generator.cpp#L1861
WEB 0024 tile_generator.cpp:1866 Saved {}x{} image to {} INFO https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/tile_generator.cpp#L1866
WEB 0025 web.tcl:53 Resolution too high for design, defaulting to ${res_per_pixel}um per pixel WARN https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.tcl#L53
WEB 0026 web.tcl:61 Area must contain 4 elements. ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.tcl#L61
WEB 0027 web.tcl:68 Cannot set -width if -resolution has already been specified. ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.tcl#L68
WEB 0028 web.tcl:102 Display option must have 2 elements {control} {value}. ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.tcl#L102
WEB 0029 web.tcl:73 Specified -width cannot be zero. ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.tcl#L73
WEB 0030 web.cpp:920 No STA data — timing sections will be empty. WARN https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.cpp#L920
WEB 0031 web.cpp:894 Cannot open file: {} ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.cpp#L894
WEB 0032 web.cpp:1102 Saved timing report to {} INFO https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.cpp#L1102
WEB 0033 web.cpp:972 Cached {} tiles at zoom {}. INFO https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.cpp#L972
WEB 0034 web.cpp:996 Rendered {} setup + {} hold path overlays. INFO https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.cpp#L996
WEB 0035 web.cpp:888 No design loaded. ERROR https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.cpp#L888
WEB 0036 web_serve.cpp:297 Web server is not running. WARN https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web_serve.cpp#L297
WEB 0037 web.tcl:23 -dir is deprecated and ignored; assets are embedded in the binary. WARN https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web.tcl#L23
WEB 0040 request_handler.cpp:1441 Exit requested from web GUI; shutting down. INFO https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/request_handler.cpp#L1441
WEB 0041 web_serve.cpp:349 Web session closed. INFO https://github.com/The-OpenROAD-Project/OpenROAD/tree/master/workspace/teste/OpenROAD-flow-scripts/tools/OpenROAD/src/web/src/web_serve.cpp#L349
26 changes: 26 additions & 0 deletions src/web/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,29 @@ function tclAppend(text, className) {
app.tclOutputEl.scrollTop = app.tclOutputEl.scrollHeight;
}

// Browser UX for `exit`/`quit` typed in the Tcl console. The OpenROAD
// process keeps running in the terminal — only the web session ends.
// window.close() only succeeds when the tab was opened via JS (or via
// certain launcher integrations); when it fails we replace the page with
// a terminal overlay so the user knows the web_server stopped and they
// can close the tab manually.
function handleServerShutdown() {
// Suppress the normal "disconnected" banner — the disconnect is intentional.
if (app.websocketManager) {
app.websocketManager.onPush = () => {};
}
Comment thread
maliberty marked this conversation as resolved.
const overlay = document.createElement('div');
overlay.style.cssText =
'position:fixed;inset:0;z-index:99999;background:#1e1e1e;color:#ddd;' +
'display:flex;flex-direction:column;align-items:center;justify-content:center;' +
'font-family:system-ui,sans-serif;font-size:16px;padding:24px;text-align:center;';
overlay.innerHTML =
'<div style="font-size:22px;margin-bottom:12px;">Web session closed</div>' +
'<div style="opacity:0.7;">OpenROAD is still running in the terminal. You can close this tab.</div>';
document.body.appendChild(overlay);
setTimeout(() => { try { window.close(); } catch (e) { /* ignore */ } }, 400);
}

function createTclConsole(container) {
const el = document.createElement('div');
el.className = 'tcl-console';
Expand Down Expand Up @@ -396,6 +419,9 @@ function createTclConsole(container) {
tclAppend(data.result + '\n',
data.is_error ? 'tcl-error' : '');
}
if (data.action === 'shutdown') {
handleServerShutdown();
}
})
.catch(err => tclAppend(`Error: ${err}\n`, 'tcl-error'));
}
Expand Down
52 changes: 51 additions & 1 deletion src/web/src/request_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <algorithm>
#include <any>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <exception>
Expand All @@ -19,6 +20,7 @@
#include <stdexcept>
#include <string>
#include <system_error>
#include <thread>
#include <utility>
#include <variant>
#include <vector>
Expand All @@ -37,6 +39,7 @@
#include "request_dispatcher.h"
#include "tile_generator.h"
#include "timing_report.h"
#include "utl/Logger.h"

namespace web {

Expand Down Expand Up @@ -1412,13 +1415,60 @@ void TclHandler::registerRequests(RequestDispatcher& d)
});
}

// Returns true if cmd is `exit` or `quit` (with optional surrounding
// whitespace). Evaluating these on a boost::asio worker thread would
// trigger ~WebServer from inside that worker and cause a self-join
// deadlock; they need special handling.
static bool isTerminalCommand(const std::string& cmd)
{
const auto first = cmd.find_first_not_of(" \t\r\n");
if (first == std::string::npos) {
return false;
}
const auto last = cmd.find_last_not_of(" \t\r\n");
const std::string trimmed = cmd.substr(first, last - first + 1);
return trimmed == "exit" || trimmed == "quit";
}
Comment thread
maliberty marked this conversation as resolved.
Outdated

WebSocketResponse TclHandler::handleTclEval(const WebSocketRequest& req)
{
WebSocketResponse resp;
resp.id = req.id;
resp.type = WebSocketResponse::kJson;
try {
auto result = tcl_eval_->eval(extract_string(req.raw_json, "cmd"));
const std::string cmd = extract_string(req.raw_json, "cmd");
if (isTerminalCommand(cmd)) {
tcl_eval_->logger->info(
utl::WEB, 40, "Exit requested from web GUI; shutting down.");
// Respond immediately with an `action: shutdown` marker so the
// browser can close its tab, then tear down the web server on a
// detached thread after a short delay. The delay lets this
// response flush before the WebSocket is closed, and the detached
// thread avoids a self-join inside stop() (the current worker is
// in threads_). The OpenROAD Tcl prompt keeps running — only the
// web session ends.
JsonBuilder builder;
builder.beginObject();
builder.field("output", "");
builder.field(
"result",
"Web session closed. OpenROAD is still running in the terminal.");
builder.field("is_error", false);
builder.field("action", "shutdown");
builder.endObject();
const std::string& json = builder.str();
resp.payload.assign(json.begin(), json.end());

if (tcl_eval_->close_session) {
auto close_session = tcl_eval_->close_session;
std::thread([close_session] {
std::this_thread::sleep_for(std::chrono::milliseconds(300));
close_session();
}).detach();
}
Comment thread
maliberty marked this conversation as resolved.
Outdated
return resp;
}
auto result = tcl_eval_->eval(cmd);
JsonBuilder builder;
builder.beginObject();
builder.field("output", result.output);
Expand Down
5 changes: 5 additions & 0 deletions src/web/src/request_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#pragma once

#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
Expand Down Expand Up @@ -32,6 +33,10 @@ struct TclEvaluator
Tcl_Interp* interp;
utl::Logger* logger;
std::mutex mutex;
// Callback wired up by WebServer::serve() that stops the web server
// without killing the OpenROAD process. Invoked by the Tcl handler
// when the browser sends `exit`/`quit` (see handleTclEval).
std::function<void()> close_session;

struct Result
{
Expand Down
24 changes: 6 additions & 18 deletions src/web/src/web.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -825,24 +825,12 @@ WebServer::~WebServer()

// The destructor fires during Tcl_Exit → atexit → ~OpenRoad chain.
// By this point the Tcl interpreter is partially torn down and static
// objects may be destroyed. Calling stop() (which joins 32 threads
// and tears down boost::asio's reactor) triggers SIGSEGV because the
// reactor's internal state references destroyed statics.
//
// Since the destructor only runs at process exit, the OS reclaims all
// memory and closes all sockets. We just need to stop the threads so
// the process can actually exit:
if (ioc_) {
ioc_->stop();
}
for (auto& t : threads_) {
if (t.joinable()) {
t.join();
}
}
threads_.clear();
// Close the Listener's acceptor and release the shared_ptr to it
// before the io_context goes away.
// objects may be destroyed. We avoid the full stop() path (which
// tears down boost::asio's reactor and would crash on residual async
// handlers referencing destroyed statics) — the OS reclaims memory
// and sockets at process exit, so we only need to release the worker
// threads so the process can actually exit.
stopAndJoinIoThreads();
if (shutdown_listener_) {
shutdown_listener_();
shutdown_listener_ = {};
Expand Down
49 changes: 40 additions & 9 deletions src/web/src/web_serve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ void WebServer::serve(int port)
auto clock_report = std::make_shared<ClockTreeReport>(sta_);

auto tcl_eval = std::make_shared<TclEvaluator>(interp_, logger_);
// The Tcl handler calls this when the browser sends `exit`/`quit`.
// Use requestStop() (not stop()) so waitForStop() on the main thread
// wakes up and tears down the server itself; calling stop() directly
// from the worker would leave the main thread blocked on stop_cv_,
// and the Tcl prompt would never resume reading stdin.
tcl_eval->close_session = [this] { requestStop(); };

// Override Tcl's `exit` so a user typing `exit` in the browser tcl
// widget doesn't run Tcl_Exit on the worker thread (which triggers
Expand Down Expand Up @@ -229,6 +235,28 @@ void WebServer::serve(int port)
}
}

void WebServer::stopAndJoinIoThreads()
{
if (ioc_) {
ioc_->stop();
}
const auto self_id = std::this_thread::get_id();
for (auto& t : threads_) {
if (!t.joinable()) {
continue;
}
if (t.get_id() == self_id) {
// Self-join would raise EDEADLK. ioc_->stop() above unblocks the
// worker so detaching is safe — the thread runs to completion on
// its own.
t.detach();
} else {
t.join();
}
}
threads_.clear();
}

void WebServer::waitForStop()
{
std::unique_lock<std::mutex> lock(stop_mutex_);
Comment thread
maliberty marked this conversation as resolved.
Comment thread
maliberty marked this conversation as resolved.
Expand Down Expand Up @@ -304,18 +332,21 @@ void WebServer::stop()
shutdown_listener_();
shutdown_listener_ = {};
}
if (ioc_) {
ioc_->stop();
}
for (auto& t : threads_) {
if (t.joinable()) {
t.join();
}
}
threads_.clear();
stopAndJoinIoThreads();
// Release without destroying — destroying io_context can crash on
// residual async handlers. Leak is bounded (at most one io_context
// per serve/stop cycle).
(void) ioc_.release(); // NOLINT(bugprone-unused-return-value)
generator_.reset();
// Remove the log sink before destroying viewer_hook_ — the sink
// stores a raw pointer into it and the CLI thread may emit a log
// line at any moment.
if (log_sink_) {
logger_->removeSink(log_sink_);
log_sink_.reset();
}
viewer_hook_.reset();
logger_->info(utl::WEB, 41, "Web session closed.");
}

} // namespace web
Loading