21#include <ixwebsocket/IXBase64.h>
22#include <ixwebsocket/IXNetSystem.h>
23#include <ixwebsocket/IXWebSocket.h>
24#include <ixwebsocket/IXWebSocketServer.h>
25#include <nlohmann/json.hpp>
30#include <condition_variable>
41#include <unordered_map>
42#include <unordered_set>
51#include <netinet/in.h>
52#include <sys/socket.h>
58using json = nlohmann::json;
69static void writePortFile(uint16_t port) {
70 FILE *fd = fopen(
"cosim.cfg",
"w");
73 fprintf(fd,
"port: %u\n",
static_cast<unsigned int>(port));
82static int pickEphemeralPort() {
84 SOCKET fd = socket(AF_INET, SOCK_STREAM, 0);
85 if (fd == INVALID_SOCKET)
88 int fd = socket(AF_INET, SOCK_STREAM, 0);
93 setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
94 reinterpret_cast<const char *
>(&enable),
sizeof(enable));
96 addr.sin_family = AF_INET;
97 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
99 if (bind(fd,
reinterpret_cast<sockaddr *
>(&addr),
sizeof(addr)) < 0) {
108 socklen_t len =
sizeof(bound);
109 int rc = getsockname(fd,
reinterpret_cast<sockaddr *
>(&bound), &len);
117 return ntohs(bound.sin_port);
120class RpcServerReadPort;
121class RpcServerWritePort;
140 const std::string &type);
142 const std::string &type);
145 void stop(uint32_t timeoutMS);
188 std::shared_ptr<ix::WebSocket>
ws;
216 std::map<std::string, std::unique_ptr<RpcServerReadPort>>
readPorts;
217 std::map<std::string, std::unique_ptr<RpcServerWritePort>>
writePorts;
219 std::unordered_map<std::string, uint64_t>
idByName;
236 std::shared_ptr<ClientSession>
sessionFor(
const ix::WebSocket &ws) {
237 std::shared_ptr<ClientSession> s =
getSession();
238 if (s && s->ws.get() != &ws)
247 std::unique_ptr<ix::WebSocketServer>
server;
252 ix::WebSocket &ws,
const ix::WebSocketMessagePtr &msg);
253 void onOpen(
const std::weak_ptr<ix::WebSocket> &wsWeak);
254 void onClose(ix::WebSocket &ws);
258 void handleHello(ix::WebSocket &ws, uint64_t requestId,
const json ¶ms);
265 std::optional<ChannelInfo>
findChannel(ix::WebSocket &ws, uint64_t requestId,
268 void sendResult(ix::WebSocket &ws, uint64_t requestId,
const json &result);
269 void sendError(ix::WebSocket &ws, uint64_t requestId,
const std::string &code,
270 const std::string &message);
322 RpcServerReadPort(
Type *type,
Impl &impl, uint64_t channelId)
327 void connect(
const ConnectOptions &options = {})
override {
328 ConnectOptions forced = options;
329 forced.bufferSize = 0;
333 void connect(ReadCallback callback,
334 const ConnectOptions &options = {})
override {
341 void clientDisconnected(uint64_t generation) {
342 std::lock_guard<std::recursive_mutex> lock(creditMutex);
343 retiredGeneration = (std::max)(retiredGeneration, generation);
344 if (unreportedGeneration <= generation)
348 void disconnect()
override {
350 std::lock_guard<std::mutex> lock(connectMutex);
355 std::lock_guard<std::recursive_mutex> lock(creditMutex);
360 for (
auto [generation, count] : held)
361 credit(generation, count);
370 bool deliver(uint64_t generation,
const MessageData &data) {
372 std::lock_guard<std::mutex> lock(connectMutex);
373 if (!connected || !parked.empty()) {
374 parked.emplace_back(generation, data);
378 if (handOver(generation, data))
382 std::lock_guard<std::mutex> lock(connectMutex);
385 parked.emplace_front(generation, data);
391 void dropped(uint64_t generation) {
392 std::lock_guard<std::recursive_mutex> lock(creditMutex);
393 credit(generation, 1);
398 void markConsumed() {
399 std::lock_guard<std::recursive_mutex> lock(creditMutex);
403 auto &[generation, count] = held.front();
404 credit(generation, 1);
411 uint64_t takeConsumed(uint64_t generation) {
412 std::lock_guard<std::recursive_mutex> lock(creditMutex);
413 if (generation != unreportedGeneration)
415 return std::exchange(unreported, 0);
421 bool handOver(uint64_t generation,
const MessageData &data) {
422 std::unique_ptr<SegmentedMessageData> msg =
423 std::make_unique<MessageData>(data);
427 std::lock_guard<std::recursive_mutex> lock(creditMutex);
428 if (!held.empty() && held.back().first == generation)
429 ++held.back().second;
431 held.emplace_back(generation, 1);
434 ok = invokeCallback(msg);
448 if (--held.back().second == 0)
454 void credit(uint64_t generation, uint64_t count) {
455 if (generation <= retiredGeneration || generation < unreportedGeneration)
457 if (generation != unreportedGeneration) {
458 unreportedGeneration = generation;
462 impl.readyIds.markReady(channelId);
467 void markConnected() {
468 std::unique_lock<std::mutex> lock(connectMutex);
473 while (!parked.empty()) {
474 auto [generation, front] = parked.front();
476 bool ok = handOver(generation, front);
484 impl.logRefusedFrame(channelId, front.getSize());
497 std::recursive_mutex creditMutex;
500 std::deque<std::pair<uint64_t, uint64_t>> held;
502 uint64_t unreported = 0;
503 uint64_t unreportedGeneration = 0;
505 uint64_t retiredGeneration = 0;
507 std::mutex connectMutex;
508 bool connected =
false;
511 std::deque<std::pair<uint64_t, MessageData>> parked;
528 RpcServerWritePort(
Type *type,
Impl &impl, uint64_t channelId)
530 writeQueue([&impl, channelId] { impl.readyIds.markReady(channelId); }) {
533 uint64_t getChannelId()
const {
return channelId; }
541 std::optional<MessageData> popForSend(uint64_t generation) {
542 std::lock_guard<std::mutex> lock(stateMutex);
543 if (generation <= retiredGeneration || generation != ownerGeneration)
545 std::optional<MessageData> msg = writeQueue.
pop();
550 unacked.fetch_add(1);
557 void ack(uint64_t count, uint64_t generation) {
558 std::lock_guard<std::mutex> lock(stateMutex);
559 if (generation <= retiredGeneration || generation != ownerGeneration)
562 unacked.store(unacked.load() - (std::min)(unacked.load(), count));
563 spaceCv.notify_all();
569 void subscribed(uint64_t limit, uint64_t generation) {
570 std::lock_guard<std::mutex> lock(stateMutex);
571 if (generation <= retiredGeneration)
573 if (generation != ownerGeneration) {
574 ownerGeneration = generation;
577 maxInFlight.store(limit);
578 spaceCv.notify_all();
583 void unsubscribed(uint64_t generation) {
584 std::lock_guard<std::mutex> lock(stateMutex);
585 if (generation == ownerGeneration)
586 resetFlowControlLocked();
592 void retireClient(uint64_t generation) {
593 std::lock_guard<std::mutex> lock(stateMutex);
594 retiredGeneration = (std::max)(retiredGeneration, generation);
595 if (ownerGeneration <= generation)
596 resetFlowControlLocked();
600 void disconnect()
override {
602 std::lock_guard<std::mutex> lock(stateMutex);
605 spaceCv.notify_all();
610 void connectImpl(
const ConnectOptions &)
override {
611 std::lock_guard<std::mutex> lock(stateMutex);
612 disconnected =
false;
620 std::unique_lock<std::mutex> lock(stateMutex);
621 spaceCv.wait(lock, [&] {
return disconnected || hasSpace(); });
623 throw std::runtime_error(
"Channel id " + std::to_string(channelId) +
624 " disconnected while writing");
627 bool tryWriteImpl(
const MessageData &data)
override {
628 std::lock_guard<std::mutex> lock(stateMutex);
638 writeQueue.
push(data);
641 void resetFlowControlLocked() {
644 spaceCv.notify_all();
647 bool hasSpace()
const {
648 uint64_t limit = maxInFlight.load();
649 return limit == 0 || queued.load() + unacked.load() < limit;
653 std::atomic<uint64_t> queued{0};
654 std::atomic<uint64_t> unacked{0};
659 std::mutex stateMutex;
661 std::condition_variable spaceCv;
662 bool disconnected =
false;
663 uint64_t retiredGeneration = 0;
665 uint64_t ownerGeneration = 0;
676 if (!ix::initNetSystem())
677 throw std::runtime_error(
678 "RpcServer: ix::initNetSystem() failed (WSAStartup)");
682 int requestedPort = port;
683 if (requestedPort <= 0) {
684 requestedPort = pickEphemeralPort();
685 if (requestedPort <= 0)
686 throw std::runtime_error(
687 "RpcServer: failed to obtain an ephemeral TCP port");
690 const std::string host =
"127.0.0.1";
691 server = std::make_unique<ix::WebSocketServer>(requestedPort, host);
692 server->disablePerMessageDeflate();
697 server->setOnConnectionCallback(
698 [
this](std::weak_ptr<ix::WebSocket> wsWeak,
699 std::shared_ptr<ix::ConnectionState> ) {
700 std::shared_ptr<ix::WebSocket> ws = wsWeak.lock();
703 ix::WebSocket *raw = ws.get();
704 ws->setOnMessageCallback(
705 [
this, wsWeak, raw](
const ix::WebSocketMessagePtr &msg) {
710 auto res =
server->listen();
712 throw std::runtime_error(
"RpcServer: listen failed: " + res.second);
716 writePortFile(
static_cast<uint16_t
>(
boundPort));
717 ctxt.getLogger().info(
"cosim", std::format(
"Server listening on {}:{}", host,
735 }
catch (
const std::exception &e) {
736 ctxt.getLogger().error(
738 std::string(
"Suppressed exception during ~RpcServer::Impl: ") +
741 ctxt.getLogger().error(
742 "cosim",
"Suppressed non-std::exception during ~RpcServer::Impl");
760 const std::string &type) {
764 auto port = std::make_unique<RpcServerReadPort>(
new Type(type), *
this,
id);
765 RpcServerReadPort *raw = port.get();
766 readPorts.emplace(name, std::move(port));
768 ChannelInfo info{id, name, type, ChannelDirection::ToServer, raw,
nullptr};
777 static_cast<RpcServerReadPort &
>(port).markConsumed();
781 const std::string &type) {
785 auto port = std::make_unique<RpcServerWritePort>(
new Type(type), *
this,
id);
786 RpcServerWritePort *raw = port.get();
789 ChannelInfo info{id, name, type, ChannelDirection::ToClient,
nullptr, raw};
819 std::shared_ptr<ClientSession> old;
836 const ix::WebSocketMessagePtr &msg) {
838 case ix::WebSocketMessageType::Open:
841 case ix::WebSocketMessageType::Close:
842 case ix::WebSocketMessageType::Error: {
843 ctxt.getLogger().debug(
844 "cosim", std::format(
"Client disconnected: {}",
845 msg->type == ix::WebSocketMessageType::Error
846 ? msg->errorInfo.reason
847 : msg->closeInfo.reason));
851 case ix::WebSocketMessageType::Message:
858 if (std::shared_ptr<ClientSession> s =
sessionFor(ws))
873 std::shared_ptr<ix::WebSocket> ws = wsWeak.lock();
879 session = std::make_shared<ClientSession>();
882 ctxt.getLogger().debug(
"cosim",
"Client connected");
890 ctxt.getLogger().warning(
891 "cosim",
"Rejecting additional client; one already connected");
893 err[
"type"] =
"error";
894 err[
"error"] = {{
"code",
"server_busy"},
895 {
"message",
"cosim server allows only one client at a "
896 "time; another client is already connected"}};
897 ws->sendUtf8Text(err.dump());
899 ws->close(1013,
"cosim server busy: another client is already connected");
903 std::shared_ptr<ClientSession> old;
911 old->closed.store(
true);
922 port->retireClient(generation);
924 port->clientDisconnected(generation);
929 const uint8_t *payloadBytes;
931 if (!
parseDataFrame(data, channelId, payloadBytes, payloadSize)) {
932 ctxt.getLogger().error(
"cosim",
933 "Received binary frame shorter than 8-byte header");
937 RpcServerReadPort *port =
nullptr;
942 it->second.direction != ChannelDirection::ToServer)
945 port = it->second.readPort;
948 ctxt.getLogger().error(
949 "cosim", std::format(
"Binary frame for unknown to-server channel id {}",
960 if (!port->deliver(generation, payload)) {
962 port->dropped(generation);
967 ctxt.getLogger().warning(
968 "cosim", std::format(
"Dropped {} bytes for channel id {}: refused by "
969 "the simulator-side consumer",
976 req = json::parse(text);
977 }
catch (
const std::exception &e) {
978 ctxt.getLogger().error(
979 "cosim", std::format(
"Failed to parse control frame: {}", e.what()));
981 std::string(
"Failed to parse JSON: ") + e.what());
986 auto typeIt = req.find(
"type");
987 if (typeIt != req.end() && typeIt->is_string() &&
988 typeIt->get<std::string>() ==
"event") {
990 if (std::shared_ptr<ClientSession> s =
sessionFor(ws))
994 if (typeIt == req.end() || !typeIt->is_string() ||
995 typeIt->get<std::string>() !=
"request") {
997 "Control frame missing \"type\":\"request\"");
1000 uint64_t requestId = 0;
1001 if (
auto idIt = req.find(
"request_id"); idIt != req.end()) {
1003 if (!idIt->is_number_unsigned()) {
1005 "\"request_id\" must be an unsigned integer");
1009 requestId = idIt->get<uint64_t>();
1010 }
catch (
const std::exception &e) {
1012 std::string(
"Invalid \"request_id\": ") + e.what());
1016 auto methodIt = req.find(
"method");
1017 if (methodIt == req.end() || !methodIt->is_string()) {
1018 sendError(ws, requestId,
"protocol_error",
"Missing \"method\"");
1021 std::string method = methodIt->get<std::string>();
1022 json params = req.value(
"params", json::object());
1024 if (method ==
"hello")
1026 else if (method ==
"subscribe")
1028 else if (method ==
"unsubscribe")
1031 sendError(ws, requestId,
"protocol_error",
"Unknown method: " + method);
1041 std::vector<std::pair<uint64_t, uint64_t>> &out,
1043 auto paramsIt = frame.find(
"params");
1044 if (paramsIt == frame.end() || !paramsIt->is_object()) {
1045 err =
"missing \"params\" object";
1048 auto chIt = paramsIt->find(
"channels");
1049 if (chIt == paramsIt->end() || !chIt->is_array()) {
1050 err =
"missing \"channels\" array";
1053 for (
const json &c : *chIt) {
1054 auto idIt = c.find(
"channel_id");
1055 auto countIt = c.find(
"count");
1056 if (idIt == c.end() || !idIt->is_number_unsigned() || countIt == c.end() ||
1057 !countIt->is_number_unsigned()) {
1058 err =
"entries need unsigned \"channel_id\" and \"count\"";
1061 out.emplace_back(idIt->get<uint64_t>(), countIt->get<uint64_t>());
1067 std::string
event = frame.value(
"event", std::string());
1068 if (event !=
"consumed") {
1071 ctxt.getLogger().debug(
"cosim",
1072 "Ignoring unknown client event \"" + event +
"\"");
1075 std::vector<std::pair<uint64_t, uint64_t>> acks;
1078 ctxt.getLogger().warning(
"cosim",
"Malformed consumed event: " + err);
1082 for (
auto [
id, count] : acks) {
1084 if (it ==
channelById.end() || !it->second.writePort) {
1085 ctxt.getLogger().warning(
1087 std::format(
"consumed event for unknown to-client channel id {}",
1091 it->second.writePort->ack(count, generation);
1096 const json ¶ms) {
1097 auto verIt = params.find(
"client_protocol_version");
1098 if (verIt == params.end() || !verIt->is_number_unsigned() ||
1100 std::string got = verIt == params.end() ?
"none" : verIt->dump();
1101 sendError(ws, requestId,
"unsupported_version",
1102 std::format(
"Server speaks cosim protocol v{}; client requested "
1103 "{}. Update the ESI runtime on both sides.",
1120 result[
"compressed_manifest_b64"] = macaron::Base64::Encode(
1125 json channelsJson = json::array();
1135 c[
"channel_id"] = info.id;
1136 c[
"name"] = info.name;
1137 c[
"type"] = info.typeId;
1138 c[
"direction"] = info.direction == ChannelDirection::ToServer
1141 channelsJson.push_back(std::move(c));
1144 result[
"channels"] = std::move(channelsJson);
1146 if (std::shared_ptr<ClientSession> s =
sessionFor(ws))
1147 s->helloDone =
true;
1153 const json ¶ms) {
1154 auto chIdIt = params.find(
"channel_id");
1155 if (chIdIt == params.end() || !chIdIt->is_number_unsigned()) {
1156 sendError(ws, requestId,
"protocol_error",
1157 "subscribe requires unsigned \"channel_id\"");
1160 uint64_t channelId = chIdIt->get<uint64_t>();
1162 if (
auto limitIt = params.find(
"max_in_flight"); limitIt != params.end()) {
1163 if (!limitIt->is_number_unsigned()) {
1164 sendError(ws, requestId,
"protocol_error",
1165 "\"max_in_flight\" must be an unsigned integer");
1168 maxInFlight = limitIt->get<uint64_t>();
1171 std::optional<ChannelInfo> info =
1172 findChannel(ws, requestId, channelId, ChannelDirection::ToClient);
1175 std::shared_ptr<ClientSession> s =
sessionFor(ws);
1177 sendError(ws, requestId,
"internal",
"No active session");
1181 std::lock_guard<std::mutex> subLock(s->subscribedMutex);
1182 if (s->subscribed.count(channelId)) {
1183 sendError(ws, requestId,
"protocol_error",
1184 std::format(
"Channel id {} is already subscribed", channelId));
1187 s->subscribed.insert(channelId);
1188 info->writePort->subscribed(maxInFlight, s->generation);
1203 const json ¶ms) {
1204 auto chIdIt = params.find(
"channel_id");
1205 if (chIdIt == params.end() || !chIdIt->is_number_unsigned()) {
1206 sendError(ws, requestId,
"protocol_error",
1207 "unsubscribe requires unsigned \"channel_id\"");
1210 uint64_t channelId = chIdIt->get<uint64_t>();
1212 RpcServerWritePort *writePort =
nullptr;
1217 writePort = it->second.writePort;
1220 std::shared_ptr<ClientSession> s =
sessionFor(ws);
1222 sendError(ws, requestId,
"internal",
"No active session");
1228 std::lock_guard<std::mutex> subLock(s->subscribedMutex);
1229 auto removed = s->subscribed.erase(channelId);
1231 sendError(ws, requestId,
"not_subscribed",
1232 std::format(
"Channel id {} is not subscribed", channelId));
1239 writePort->unsubscribed(s->generation);
1248 std::optional<ChannelInfo> info;
1256 sendError(ws, requestId,
"unknown_channel",
1257 std::format(
"No channel with id {}", channelId));
1258 return std::nullopt;
1260 if (info->direction != dir) {
1261 sendError(ws, requestId,
"wrong_direction",
1262 std::format(
"Channel id {} is not a {} channel", channelId,
1263 dir == ChannelDirection::ToServer ?
"to-server"
1265 return std::nullopt;
1271 const json &result) {
1273 resp[
"type"] =
"response";
1274 resp[
"request_id"] = requestId;
1275 resp[
"result"] = result;
1278 ws.sendUtf8Text(resp.dump());
1282 const std::string &code,
const std::string &message) {
1284 resp[
"type"] =
"response";
1285 resp[
"request_id"] = requestId;
1286 resp[
"error"] = {{
"code", code}, {
"message", message}};
1287 ws.sendUtf8Text(resp.dump());
1299 std::unordered_set<uint64_t> ids;
1303 for (uint64_t
id : ids) {
1304 RpcServerWritePort *writePort =
nullptr;
1309 it->second.direction != ChannelDirection::ToClient)
1311 writePort = it->second.writePort;
1325 std::shared_ptr<ClientSession> s =
getSession();
1331 if (s->closed.load())
1333 std::lock_guard<std::mutex> subLock(s->subscribedMutex);
1334 if (!s->subscribed.count(
id))
1336 std::optional<MessageData> msg = writePort->popForSend(s->generation);
1339 std::string frame =
buildDataFrame(
id, msg->getBytes(), msg->getSize());
1342 s->ws->sendBinary(frame);
1351 std::shared_ptr<ClientSession> s =
getSession();
1352 if (!s || !s->helloDone)
1354 std::vector<std::pair<uint64_t, RpcServerReadPort *>> ports;
1357 for (uint64_t
id : ids) {
1359 if (it !=
channelById.end() && it->second.readPort)
1360 ports.emplace_back(
id, it->second.readPort);
1363 json channels = json::array();
1364 for (
auto [
id, port] : ports)
1365 if (uint64_t count = port->takeConsumed(s->generation))
1366 channels.push_back({{
"channel_id",
id}, {
"count", count}});
1367 if (channels.empty())
1371 event[
"type"] =
"event";
1372 event[
"event"] =
"consumed";
1373 event[
"params"] = {{
"channels", std::move(channels)}};
1374 s->ws->sendUtf8Text(event.dump());
1385 const std::vector<uint8_t> &compressedManifest) {
1387 throw std::runtime_error(
"Server not running");
1388 impl->setManifest(esiVersion, compressedManifest);
1392 const std::string &type) {
1394 throw std::runtime_error(
"Server not running");
1395 return impl->registerReadPort(name, type);
1399 const std::string &type) {
1401 throw std::runtime_error(
"Server not running");
1402 return impl->registerWritePort(name, type);
1407 throw std::runtime_error(
"Server not running");
1408 impl->messageConsumed(port);
1413 throw std::runtime_error(
"Server already running");
1414 impl = std::make_unique<Impl>(
ctxt, port);
1419 throw std::runtime_error(
"Server not running");
1420 impl->stop(timeoutMS);
1425 throw std::runtime_error(
"Server not running");
1426 return impl->getPort();
esi::cosim::RpcServer::Impl Impl
static bool parseConsumedEvent(const json &frame, std::vector< std::pair< uint64_t, uint64_t > > &out, std::string &err)
Parse the channels list of a consumed event into (channel_id, count) pairs.
A client for the cosim RPC server.
static constexpr unsigned DefaultToClientMaxInFlight
Default maximum number of frames in flight (queued in the simulator or sent but not yet consumed by t...
AcceleratorConnections, Accelerators, and Manifests must all share a context.
A concrete flat message backed by a single vector of bytes.
size_t getSize() const
Get the size of the data in bytes.
A ChannelPort which reads data from the accelerator.
virtual void connect(ReadCallback callback, const ConnectOptions &options={})
virtual void disconnect() override
Disconnect the channel.
Root class of the ESI type system.
A ChannelPort which sends data to the accelerator.
virtual void disconnect() override
A client for the cosim RPC server.
ChannelDirection
Channel direction as reported by the server.
static constexpr unsigned DefaultToClientMaxInFlight
Default maximum number of frames in flight (queued in the simulator or sent but not yet consumed by t...
Cross-thread error channel for the IXWebSocket network thread.
ReadChannelPort & registerReadPort(const std::string &name, const std::string &type)
std::unique_ptr< ix::WebSocketServer > server
void onClose(ix::WebSocket &ws)
void logRefusedFrame(uint64_t channelId, size_t size)
Log that a size-byte frame for channel channelId was dropped since the simulator-side consumer refuse...
void handleBinaryFrame(uint64_t generation, const std::string &data)
std::vector< uint8_t > compressedManifest
void handleUnsubscribe(ix::WebSocket &ws, uint64_t requestId, const json ¶ms)
std::optional< ChannelInfo > findChannel(ix::WebSocket &ws, uint64_t requestId, uint64_t channelId, ChannelDirection dir)
Look up a channel.
std::unordered_map< std::string, uint64_t > idByName
uint64_t nextSessionGeneration
void sendResult(ix::WebSocket &ws, uint64_t requestId, const json &result)
void handleHello(ix::WebSocket &ws, uint64_t requestId, const json ¶ms)
::esi::cosim::FaultStash faultStash
void sendConsumedEvent(const std::unordered_set< uint64_t > &ids)
Send a batched consumed event for any to-server messages the simulation has consumed since the last c...
void handleControlFrame(ix::WebSocket &ws, const std::string &text)
std::shared_ptr< ClientSession > session
void sendError(ix::WebSocket &ws, uint64_t requestId, const std::string &code, const std::string &message)
Impl(Context &ctxt, int port)
void stop(uint32_t timeoutMS)
std::condition_variable manifestReadyCV
std::map< std::string, std::unique_ptr< RpcServerWritePort > > writePorts
std::shared_ptr< ClientSession > sessionFor(const ix::WebSocket &ws)
The active session if ws is its socket, else null.
void handleEvent(uint64_t generation, const json &frame)
WriteChannelPort & registerWritePort(const std::string &name, const std::string &type)
std::unordered_map< uint64_t, ChannelInfo > channelById
std::map< std::string, std::unique_ptr< RpcServerReadPort > > readPorts
std::thread transportThread
backends::cosim::RpcClient::ChannelDirection ChannelDirection
void handleSubscribe(ix::WebSocket &ws, uint64_t requestId, const json ¶ms)
void onOpen(const std::weak_ptr< ix::WebSocket > &wsWeak)
void onClientMessage(const std::weak_ptr< ix::WebSocket > &wsWeak, ix::WebSocket &ws, const ix::WebSocketMessagePtr &msg)
utils::ReadyIdSet< uint64_t > readyIds
void retireSession(uint64_t generation)
Forget all per-channel state belonging to client session generation, which has disconnected.
std::shared_ptr< ClientSession > getSession()
The active session, if any.
void messageConsumed(ReadChannelPort &port)
void setManifest(int esiVersion, const std::vector< uint8_t > &compressedManifest)
void messageConsumed(ReadChannelPort &port)
Report that the simulation has consumed one message read from port (which must have been returned by ...
ReadChannelPort & registerReadPort(const std::string &name, const std::string &type)
Register a read or write port which communicates over RPC.
void stop(uint32_t timeoutMS=0)
void setManifest(int esiVersion, const std::vector< uint8_t > &compressedManifest)
Set the manifest and version.
std::unique_ptr< Impl > impl
WriteChannelPort & registerWritePort(const std::string &name, const std::string &type)
Multi-producer / single-consumer dirty-set of channel ids, with CV-style blocking drain semantics.
void push(E... t)
Push onto the queue.
std::optional< T > pop()
Pop something off the queue but return nullopt if the queue is empty.
std::string buildDataFrame(uint64_t channelId, const uint8_t *bytes, size_t size)
Pack a cosim binary data frame: [u64 LE channel_id][payload].
constexpr uint64_t kProtocolVersion
Wire protocol version.
bool parseDataFrame(const std::string &data, uint64_t &channelId, const uint8_t *&payload, size_t &payloadSize)
Parse a cosim binary data frame.
RpcServerReadPort * readPort
ChannelDirection direction
RpcServerWritePort * writePort
std::shared_ptr< ix::WebSocket > ws
std::atomic< bool > closed
std::mutex subscribedMutex
std::unordered_set< uint64_t > subscribed
std::atomic< bool > helloDone