xo-reflect: ownership hints for struct members xo-printjson: use onwership to help decide on expansion vs ref xo-websock: adapt to adj xo-printjson api
654 lines
25 KiB
C++
654 lines
25 KiB
C++
/** @file introspect.cpp
|
|
*
|
|
* @author Roland Conybeare, Sep 2026
|
|
*
|
|
* A webserver that shows its own state in the browser.
|
|
*
|
|
* Serves ./mount-origin (index.html + introspect.js) and a websocket stream,
|
|
* "/introspect". The page subscribes, and on {"cmd": "send", "msg":
|
|
* "refresh"} this program replies with a snapshot of its own state, which
|
|
* the page draws with d3.
|
|
*
|
|
* Increments of .xo-backlog/xo-websock/issues/10:
|
|
* 1. the server's port and run state
|
|
* 2. its registered endpoints -- plus two demo endpoints, so there is more
|
|
* than /introspect to see
|
|
* 3. its live websocket sessions: one per connected page
|
|
* 4. each session's subscriptions -- /demo/${id} is now a ticker, so a
|
|
* subscription to it has traffic
|
|
* 5a-5d. the snapshot is the server itself, printed by xo-websock's
|
|
* native json printers: each object once, others by ref, with ids and
|
|
* refcounts, so the page draws how the objects share each other
|
|
* 5e. plus the application's own holds on sinks (the ticker's), which
|
|
* complete the refcounts -- reported as ids only, not drawn (the
|
|
* ticker box was dropped, .xo-backlog/xo-websock/issues/13)
|
|
*
|
|
* The page files live in mount-origin/ beside the executable (cmake copies
|
|
* them there); found from the executable's own location, so this runs from
|
|
* any directory:
|
|
*
|
|
* .build/xo-websock/example/introspect/websock_ex_introspect [port]
|
|
* [--type-maps=TEMPLATE]
|
|
* [--src-tree=ROOT]
|
|
* [--src-link=TEMPLATE]
|
|
*
|
|
* then open http://localhost:<port>/ ; Ctrl-C to stop.
|
|
*
|
|
* Type -> source locations (.xo-backlog/xo-websock/issues/12): built with
|
|
* -DXO_ENABLE_SOURCE_MAP=ON, cmake writes type-maps.json beside the
|
|
* executable -- where each subsystem's map is, for xo-websock and
|
|
* everything it depends on. http://host:port/dyn/types merges them, on
|
|
* every request, so a rebuilt map shows without a restart.
|
|
* --type-maps=TEMPLATE overrides the locations: each subsystem's map is
|
|
* TEMPLATE with {subsystem} replaced by its name.
|
|
*
|
|
* --src-tree=ROOT serves the source tree at ROOT (the repo root the maps'
|
|
* paths are relative to) as html pages, at
|
|
* http://host:port/dyn/src/<path>#L<line> -- the files of mapped types
|
|
* only. Off unless given.
|
|
*
|
|
* The page links each object to the source of its type (its
|
|
* _canonical_type_ looked up in /dyn/types), by the url template
|
|
* /dyn/types reports as "link":
|
|
* --src-link=TEMPLATE, with {file} and {line} replaced, e.g. a forgejo
|
|
* commit:
|
|
* --src-link=https://<host>/<owner>/xo-umbrella2/src/commit/<sha>/{file}#L{line}
|
|
* else, with --src-tree, /dyn/src/{file}#L{line}; else none -- no links.
|
|
**/
|
|
|
|
#include <xo/websock/Webserver.hpp>
|
|
#include <xo/websock/cx/WebsockAppcx.hpp>
|
|
#include <xo/printjson/cx/PrintJsonAppcx.hpp>
|
|
#include <xo/reflect/cx/ReflectAppcx.hpp>
|
|
#include <xo/websock/WebsocketSink.hpp>
|
|
#include <xo/printjson/JsonPrinter.hpp>
|
|
#include <xo/ppsink/quoted_ostream.hpp> /* quot(..) */
|
|
#include <xo/reflect/Reflect.hpp>
|
|
#include <xo/reflect/StructReflector.hpp>
|
|
#include <xo/reflectutil/type_name.hpp>
|
|
#include <xo/indentlog2/cx/Indentlog2Appcx.hpp>
|
|
#include <xo/indentlog2/cx/Indentlog2Config.hpp>
|
|
#include <xo/indentlog2/init_indentlog2.hpp>
|
|
#include <xo/ppsink/PpStyle.hpp>
|
|
#include <xo/subsys/Subsystem.hpp>
|
|
#include <json/json.h>
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <csignal>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace xo {
|
|
using xo::web::Webserver;
|
|
using xo::web::WebserverConfig;
|
|
using xo::web::StreamEndpointDescr;
|
|
using xo::web::HttpEndpointDescr;
|
|
using xo::web::HttpRequest;
|
|
using xo::web::HttpResponse;
|
|
using xo::web::html_escape;
|
|
using xo::web::StreamReceiver;
|
|
using xo::web::WebsocketSink;
|
|
using xo::reflect::Reflect;
|
|
using xo::reflect::StructReflector;
|
|
using xo::reflect::TaggedPtr;
|
|
using xo::reflect::TaggedRcptr;
|
|
using xo::reflect::TypeDescrTable;
|
|
using xo::json::PrintJson;
|
|
using xo::json::JsonPrinter;
|
|
using xo::pp::quot;
|
|
using xo::fn::CallbackId;
|
|
|
|
namespace web {
|
|
/** /demo/${id}: sends each subscriber a counter, once a second.
|
|
* Keeps its sinks by callback id, for unsubscribe.
|
|
**/
|
|
class Ticker {
|
|
public:
|
|
CallbackId subscribe(rp<WebsocketSink> const & sink) {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
std::uint32_t id = ++last_id_;
|
|
sink_map_[id] = sink;
|
|
|
|
return CallbackId(id);
|
|
}
|
|
|
|
void unsubscribe(CallbackId id) {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
sink_map_.erase(id.id());
|
|
}
|
|
|
|
/* one tick to every subscriber. Sends with the lock RELEASED:
|
|
* a send enters the server, which may be running subscribe on
|
|
* its own thread, waiting for this lock
|
|
*/
|
|
void tick() {
|
|
std::vector<rp<WebsocketSink>> sink_v;
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
for (auto const & ix : sink_map_)
|
|
sink_v.push_back(ix.second);
|
|
}
|
|
|
|
std::int64_t n = ++n_tick_;
|
|
|
|
for (auto const & sink : sink_v)
|
|
sink->notify_ev_tp(Reflect::make_tp(&n));
|
|
}
|
|
|
|
private:
|
|
mutable std::mutex mutex_;
|
|
std::uint32_t last_id_ = 0;
|
|
std::map<std::uint32_t, rp<WebsocketSink>> sink_map_;
|
|
/* ticker thread only */
|
|
std::int64_t n_tick_ = 0;
|
|
};
|
|
|
|
/** what the page is told: the server itself. PrintJson follows
|
|
* the server pointer to its printer (installed by WebsockAppcx)
|
|
**/
|
|
struct IntrospectSnapshot {
|
|
static void reflect_self() {
|
|
StructReflector<IntrospectSnapshot> sr;
|
|
|
|
/* owning: the snapshot is where the server prints. A raw
|
|
* pointer is borrowed by default, printing only a ref
|
|
* (.xo-backlog/xo-printjson/issues/08)
|
|
*/
|
|
if (sr.is_incomplete()) {
|
|
REFLECT_MEMBER(sr, server).owning();
|
|
}
|
|
}
|
|
|
|
Webserver * server_ = nullptr;
|
|
};
|
|
|
|
/** answers {"cmd": "send", "msg": "refresh"} with a snapshot, on the
|
|
* asking subscription
|
|
**/
|
|
class IntrospectReceiver : public StreamReceiver {
|
|
public:
|
|
explicit IntrospectReceiver(Webserver * websrv)
|
|
: websrv_{websrv} {}
|
|
|
|
/** describe IntrospectReceiver to xo-reflect, using @p table.
|
|
*
|
|
* websrv_ prints as a ref to the server: a snapshot prints the
|
|
* server first, and each object prints once
|
|
* (.xo-backlog/xo-printjson/issues/02). Until that, it was left
|
|
* out -- printed in full, it nested the whole Webserver inside
|
|
* its own endpoint
|
|
**/
|
|
static void reflect_self(TypeDescrTable * /*table*/) {
|
|
StructReflector<IntrospectReceiver> sr;
|
|
|
|
if (sr.is_incomplete())
|
|
REFLECT_MEMBER(sr, websrv);
|
|
}
|
|
|
|
// ----- Inherited from SelfTagging -----
|
|
|
|
TaggedRcptr self_tp() override { return Reflect::make_rctp(this); }
|
|
|
|
// ----- Inherited from StreamReceiver -----
|
|
|
|
void receive(rp<WebsocketSink> const & sink, Json::Value const & msg) override {
|
|
if (!msg.isString() || msg.asString() != "refresh")
|
|
throw std::runtime_error("expected \"refresh\"");
|
|
|
|
IntrospectSnapshot snap;
|
|
snap.server_ = websrv_;
|
|
|
|
sink->notify_ev_tp(Reflect::make_tp(&snap));
|
|
}
|
|
|
|
private:
|
|
/* borrowed: outlives the server's use of this receiver */
|
|
Webserver * websrv_ = nullptr;
|
|
};
|
|
|
|
/** where each subsystem's type -> source map is (xo-type-src-map,
|
|
* one per subsystem); merged on request, so never stale
|
|
**/
|
|
class TypeMaps {
|
|
public:
|
|
/** from @p list_file (cmake's type-maps.json). A non-empty
|
|
* @p tmpl overrides each map's location: @p tmpl with
|
|
* "{subsystem}" replaced by the subsystem's name
|
|
**/
|
|
static TypeMaps from_list(std::filesystem::path const & list_file,
|
|
std::string const & tmpl) {
|
|
TypeMaps retval;
|
|
|
|
std::ifstream in(list_file);
|
|
if (!in)
|
|
return retval;
|
|
|
|
Json::Value list;
|
|
Json::CharReaderBuilder rb;
|
|
std::string errs;
|
|
if (!Json::parseFromStream(rb, in, &list, &errs))
|
|
throw std::runtime_error("TypeMaps: cannot parse "
|
|
+ list_file.string() + ": " + errs);
|
|
|
|
Json::Value const & maps = list["maps"];
|
|
for (auto const & name : maps.getMemberNames()) {
|
|
std::string path = maps[name].asString();
|
|
|
|
if (!tmpl.empty()) {
|
|
path = tmpl;
|
|
std::string::size_type p = path.find("{subsystem}");
|
|
if (p != std::string::npos)
|
|
path.replace(p, std::string("{subsystem}").size(), name);
|
|
}
|
|
|
|
retval.maps_.emplace_back(name, path);
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
bool empty() const { return maps_.empty(); }
|
|
|
|
/** true iff @p rel (repo-relative) is the file of some type in
|
|
* the merged maps -- read now, like merged()
|
|
**/
|
|
bool names_file(std::string const & rel) const {
|
|
Json::Value const m = this->merged();
|
|
Json::Value const & types = m["types"];
|
|
|
|
for (auto const & tname : types.getMemberNames()) {
|
|
if (types[tname]["file"].asString() == rel)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** the union of the maps, as json -- read from the files now, so
|
|
* a rebuilt map shows without a restart. A missing or
|
|
* unreadable map is listed under "missing"; a name in two maps
|
|
* at different places is left out of "types" and listed under
|
|
* "conflicts" -- as xo-type-src-merge does
|
|
**/
|
|
Json::Value merged() const {
|
|
Json::Value types(Json::objectValue);
|
|
Json::Value owner(Json::objectValue); /* name -> subsystem */
|
|
Json::Value conflicts(Json::objectValue);
|
|
Json::Value used(Json::arrayValue);
|
|
Json::Value missing(Json::arrayValue);
|
|
|
|
for (auto const & [name, path] : maps_) {
|
|
std::ifstream in(path);
|
|
Json::Value m;
|
|
Json::CharReaderBuilder rb;
|
|
std::string errs;
|
|
|
|
if (!in || !Json::parseFromStream(rb, in, &m, &errs)) {
|
|
missing.append(name);
|
|
continue;
|
|
}
|
|
|
|
used.append(name);
|
|
|
|
Json::Value const & mtypes = m["types"];
|
|
for (auto const & tname : mtypes.getMemberNames()) {
|
|
Json::Value const & loc = mtypes[tname];
|
|
|
|
if (!types.isMember(tname)) {
|
|
types[tname] = loc;
|
|
owner[tname] = name;
|
|
} else if (types[tname] != loc) {
|
|
if (!conflicts.isMember(tname))
|
|
conflicts[tname].append(located(owner[tname].asString(),
|
|
types[tname]));
|
|
conflicts[tname].append(located(name, loc));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto const & tname : conflicts.getMemberNames())
|
|
types.removeMember(tname);
|
|
|
|
Json::Value out(Json::objectValue);
|
|
out["format"] = "xo-type-src-map/1";
|
|
out["subsystems"] = used;
|
|
out["missing"] = missing;
|
|
out["types"] = types;
|
|
out["conflicts"] = conflicts;
|
|
|
|
return out;
|
|
}
|
|
|
|
private:
|
|
static Json::Value located(std::string const & subsystem, Json::Value const & loc) {
|
|
Json::Value x = loc;
|
|
x["subsystem"] = subsystem;
|
|
return x;
|
|
}
|
|
|
|
/* (subsystem, path to its types.json) */
|
|
std::vector<std::pair<std::string, std::string>> maps_;
|
|
};
|
|
|
|
/** @brief a source tree, served read-only as html pages with
|
|
* numbered lines -- the "uncommitted tree" link provider
|
|
* (.xo-backlog/xo-websock/issues/12, step 4b).
|
|
*
|
|
* Confined: a path must be relative, without "..", and resolve --
|
|
* symlinks followed -- to a regular file inside the root; and it
|
|
* must be the file of some type in the maps, so this is no general
|
|
* file server.
|
|
**/
|
|
class SourceTree {
|
|
public:
|
|
/** @p root must exist **/
|
|
explicit SourceTree(std::filesystem::path const & root)
|
|
: root_{std::filesystem::canonical(root)} {}
|
|
|
|
std::filesystem::path const & root() const { return root_; }
|
|
|
|
/** the page for repo-relative @p rel, or why not **/
|
|
HttpResponse serve(std::string const & rel, TypeMaps const & maps) const {
|
|
namespace fs = std::filesystem;
|
|
|
|
if (rel.empty())
|
|
return HttpResponse::not_found("no file named");
|
|
if (rel.front() == '/')
|
|
return HttpResponse::forbidden("absolute path [" + rel + "]");
|
|
|
|
/* no ".." (nor "." or empty) segment */
|
|
for (std::size_t b = 0; b <= rel.size(); ) {
|
|
std::size_t e = rel.find('/', b);
|
|
if (e == std::string::npos)
|
|
e = rel.size();
|
|
|
|
std::string_view seg(rel.data() + b, e - b);
|
|
if (seg.empty() || seg == "." || seg == "..")
|
|
return HttpResponse::forbidden("path [" + rel + "] is not plain");
|
|
|
|
b = e + 1;
|
|
}
|
|
|
|
std::error_code ec;
|
|
fs::path file = fs::canonical(this->root_ / rel, ec);
|
|
|
|
if (ec)
|
|
return HttpResponse::not_found("no file [" + rel + "]");
|
|
|
|
/* inside the root, after symlinks */
|
|
auto [ri, fi] = std::mismatch(root_.begin(), root_.end(), file.begin(), file.end());
|
|
if (ri != root_.end())
|
|
return HttpResponse::forbidden("[" + rel + "] is outside the source tree");
|
|
|
|
if (!fs::is_regular_file(file))
|
|
return HttpResponse::not_found("[" + rel + "] is not a file");
|
|
|
|
if (!maps.names_file(rel))
|
|
return HttpResponse::not_found("[" + rel + "] is not a file of any mapped type");
|
|
|
|
std::ifstream in(file);
|
|
if (!in)
|
|
return HttpResponse::not_found("cannot read [" + rel + "]");
|
|
|
|
return HttpResponse::html(this->page(rel, in));
|
|
}
|
|
|
|
private:
|
|
/** @p in as a page: a line per <span id="L<n>">, the target
|
|
* highlighted
|
|
**/
|
|
std::string page(std::string const & rel, std::istream & in) const {
|
|
std::string out;
|
|
|
|
out += "<!doctype html><html><head><meta charset=\"utf-8\"><title>"
|
|
+ html_escape(rel) + "</title><style>"
|
|
"body{margin:0;font:13px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace}"
|
|
"header{position:sticky;top:0;background:#f4f4f4;padding:6px 12px;"
|
|
"border-bottom:1px solid #ccc;font-family:sans-serif}"
|
|
".note{color:#666;font-size:12px}"
|
|
"pre{margin:0;padding:4px 0}"
|
|
".l{display:block;scroll-margin-top:5em}"
|
|
".l:target{background:#fff3b0}"
|
|
".n{display:inline-block;width:6ch;padding-right:1.5ch;text-align:right;"
|
|
"color:#999;text-decoration:none;user-select:none}"
|
|
"</style></head><body><header><b>" + html_escape(rel) + "</b>"
|
|
"<div class=\"note\">the working tree at " + html_escape(root_.string())
|
|
+ ": if files changed since this program was built, lines may have moved"
|
|
"</div></header><pre>";
|
|
|
|
std::string line;
|
|
for (int n = 1; std::getline(in, line); ++n) {
|
|
std::string id = "L" + std::to_string(n);
|
|
|
|
out += "<span class=\"l\" id=\"" + id + "\"><a class=\"n\" href=\"#"
|
|
+ id + "\">" + std::to_string(n) + "</a>" + html_escape(line)
|
|
+ "</span>";
|
|
}
|
|
|
|
out += "</pre></body></html>";
|
|
|
|
return out;
|
|
}
|
|
|
|
private:
|
|
/** canonical **/
|
|
std::filesystem::path root_;
|
|
};
|
|
} /*namespace web*/
|
|
} /*namespace xo*/
|
|
|
|
namespace {
|
|
std::atomic<bool> s_stop{false};
|
|
|
|
/** directory holding this executable. /proc/self/exe on linux; else
|
|
* argv[0], which holds a path whenever the program was run by path --
|
|
* examples are not installed on PATH
|
|
**/
|
|
std::filesystem::path exe_dir(char const * argv0) {
|
|
namespace fs = std::filesystem;
|
|
|
|
std::error_code ec;
|
|
fs::path self = fs::read_symlink("/proc/self/exe", ec);
|
|
|
|
if (ec)
|
|
self = fs::absolute(argv0, ec);
|
|
|
|
return self.parent_path();
|
|
}
|
|
|
|
extern "C" void on_signal(int) { s_stop = true; }
|
|
|
|
/** capacity for the thread-local scratch arena behind tostr() **/
|
|
constexpr std::uint32_t c_temp_arena_capacity = 64 * 1024;
|
|
}
|
|
|
|
int
|
|
main(int argc, char * argv[])
|
|
{
|
|
using namespace xo;
|
|
using xo::web::IntrospectSnapshot;
|
|
using xo::web::IntrospectReceiver;
|
|
|
|
using xo::web::TypeMaps;
|
|
using xo::web::SourceTree;
|
|
|
|
std::int32_t port = 7681;
|
|
std::string type_maps_tmpl;
|
|
std::string src_tree;
|
|
std::string src_link;
|
|
|
|
for (int i = 1; i < argc; ++i) {
|
|
std::string arg = argv[i];
|
|
|
|
if (arg.rfind("--type-maps=", 0) == 0)
|
|
type_maps_tmpl = arg.substr(std::string("--type-maps=").size());
|
|
else if (arg.rfind("--src-tree=", 0) == 0)
|
|
src_tree = arg.substr(std::string("--src-tree=").size());
|
|
else if (arg.rfind("--src-link=", 0) == 0)
|
|
src_link = arg.substr(std::string("--src-link=").size());
|
|
else
|
|
port = std::atoi(argv[i]);
|
|
}
|
|
|
|
/* the subsystem stack: logging, reflection, json printing, websock.
|
|
* Establishing the websock context installs its json printers; a
|
|
* Webserver is made from it
|
|
*/
|
|
using IntrospectConfig = AppConfig<S_indentlog2_tag, S_reflect_tag,
|
|
S_printjson_tag, S_websock_tag>;
|
|
using IntrospectContext = AppContext<S_indentlog2_tag, S_reflect_tag,
|
|
S_printjson_tag, S_websock_tag>;
|
|
|
|
IntrospectConfig app_config{ Indentlog2Config(pp::PpConfig::plain(),
|
|
c_temp_arena_capacity),
|
|
ReflectConfig(),
|
|
PrintJsonConfig(),
|
|
WebsockConfig() };
|
|
IntrospectContext app_cx{ app_config };
|
|
|
|
Subsystem::initialize_all();
|
|
|
|
IntrospectSnapshot::reflect_self();
|
|
IntrospectReceiver::reflect_self(app_cx.cx<S_reflect_tag>().type_table());
|
|
|
|
/* the ticker: each /demo subscriber gets a counter once a second. Made
|
|
* before /introspect, whose receiver reports its holds
|
|
*/
|
|
auto ticker = std::make_shared<xo::web::Ticker>();
|
|
|
|
/* the page: served from beside the executable, not the cwd. Checked up
|
|
* front -- otherwise every request falls through to the server's "no
|
|
* dynamic content" page, which does not say why
|
|
*/
|
|
std::filesystem::path origin = exe_dir(argv[0]) / "mount-origin";
|
|
|
|
if (!std::filesystem::exists(origin / "index.html")) {
|
|
std::cerr << "introspect: page not found: " << (origin / "index.html")
|
|
<< "\n (cmake copies mount-origin/ beside the executable;"
|
|
" rebuild target websock_ex_introspect)" << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
rp<Webserver> websrv
|
|
= Webserver::make(app_cx.cx<S_websock_tag>(),
|
|
WebserverConfig(port, false, false, false)
|
|
.with_mount_origin(origin.string()));
|
|
|
|
websrv->register_stream_endpoint
|
|
(StreamEndpointDescr("/introspect",
|
|
/* nothing to attach: every frame is a reply */
|
|
[](rp<WebsocketSink> const &) { return CallbackId(1); },
|
|
[](CallbackId) {},
|
|
new IntrospectReceiver(websrv.get())));
|
|
|
|
/* demo endpoints: something besides /introspect to look at.
|
|
* http endpoints are served under the server's dynamic mount, /dyn --
|
|
* so this one answers http://host:port/dyn/hello/<name>
|
|
*/
|
|
websrv->register_http_endpoint
|
|
(HttpEndpointDescr("/hello/${name}",
|
|
[](HttpRequest const & req)
|
|
{
|
|
return HttpResponse::html("<html>hello, "
|
|
+ html_escape(req.var("name"))
|
|
+ "</html>");
|
|
}));
|
|
|
|
/* type -> source locations: http://host:port/dyn/types */
|
|
auto type_maps = std::make_shared<TypeMaps>
|
|
(TypeMaps::from_list(exe_dir(argv[0]) / "type-maps.json", type_maps_tmpl));
|
|
|
|
if (type_maps->empty())
|
|
std::cerr << "introspect: no type maps (build with -DXO_ENABLE_SOURCE_MAP=ON):"
|
|
" /dyn/types is empty" << std::endl;
|
|
|
|
/* how the page links a type to its source: see the file comment */
|
|
if (src_link.empty() && !src_tree.empty())
|
|
src_link = "/dyn/src/{file}#L{line}";
|
|
|
|
websrv->register_http_endpoint
|
|
(HttpEndpointDescr("/types",
|
|
[type_maps, src_link](HttpRequest const &)
|
|
{
|
|
Json::Value out = type_maps->merged();
|
|
out["link"] = (src_link.empty()
|
|
? Json::Value(Json::nullValue)
|
|
: Json::Value(src_link));
|
|
|
|
Json::StreamWriterBuilder wb;
|
|
wb["indentation"] = "";
|
|
|
|
return HttpResponse::json(Json::writeString(wb, out));
|
|
}));
|
|
|
|
/* the source tree: http://host:port/dyn/src/<path>, when asked for */
|
|
if (!src_tree.empty()) {
|
|
std::error_code ec;
|
|
if (!std::filesystem::is_directory(src_tree, ec)) {
|
|
std::cerr << "introspect: --src-tree: not a directory: " << src_tree << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
auto tree = std::make_shared<SourceTree>(src_tree);
|
|
|
|
std::cerr << "introspect: serving source tree " << tree->root()
|
|
<< " at /dyn/src/" << std::endl;
|
|
|
|
websrv->register_http_endpoint
|
|
(HttpEndpointDescr("/src/${path...}",
|
|
[tree, type_maps](HttpRequest const & req)
|
|
{
|
|
return tree->serve(std::string(req.var("path")),
|
|
*type_maps);
|
|
}));
|
|
}
|
|
|
|
websrv->register_stream_endpoint
|
|
(StreamEndpointDescr("/demo/${id}",
|
|
[ticker](rp<WebsocketSink> const & sink) { return ticker->subscribe(sink); },
|
|
[ticker](CallbackId id) { ticker->unsubscribe(id); }));
|
|
|
|
std::signal(SIGINT, on_signal);
|
|
std::signal(SIGTERM, on_signal);
|
|
|
|
websrv->start_webserver();
|
|
|
|
/* wait until listening, to report the real port */
|
|
while ((websrv->listen_port() == 0) && !s_stop)
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
|
|
std::cerr << "introspect: open http://localhost:" << websrv->listen_port()
|
|
<< "/ (Ctrl-C to stop)" << std::endl;
|
|
|
|
/* main thread drives the ticker; 100ms steps so Ctrl-C is prompt */
|
|
for (int i = 1; !s_stop; ++i) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
|
|
if (i % 10 == 0)
|
|
ticker->tick();
|
|
}
|
|
|
|
websrv->stop_webserver();
|
|
websrv->join_webserver();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* end introspect.cpp */
|