yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
mesen_socket_client.cc
Go to the documentation of this file.
2
3#include <fcntl.h>
4#include <sys/stat.h>
5
6#include <filesystem>
7
8#ifdef _WIN32
9// clang-format off
10// winsock2.h must precede afunix.h (defines ADDRESS_FAMILY)
11#include <winsock2.h>
12#include <ws2tcpip.h>
13#include <afunix.h>
14#include <io.h>
15// clang-format on
16#define close closesocket
17typedef int ssize_t;
18#else
19#include <arpa/inet.h>
20#include <netdb.h>
21#include <netinet/in.h>
22#include <poll.h>
23#include <sys/socket.h>
24#include <sys/un.h>
25#include <unistd.h>
26#endif
27
28#include <cerrno>
29#include <charconv>
30#include <chrono>
31#include <cstdlib>
32#include <cstring>
33#include <regex>
34#include <sstream>
35#include <utility>
36
37#include "absl/cleanup/cleanup.h"
38#include "absl/strings/numbers.h"
39#include "absl/strings/str_cat.h"
40#include "absl/strings/str_format.h"
41#include "absl/strings/str_split.h"
42
43namespace yaze {
44namespace emu {
45namespace mesen {
46
47namespace {
48
49// Simple JSON value extraction (avoids pulling in a full JSON library)
50std::string ExtractJsonString(const std::string& json, const std::string& key) {
51 std::string search = "\"" + key + "\":";
52 size_t pos = json.find(search);
53 if (pos == std::string::npos)
54 return "";
55
56 pos += search.length();
57 while (pos < json.length() && (json[pos] == ' ' || json[pos] == '\t'))
58 pos++;
59
60 if (pos >= json.length())
61 return "";
62
63 if (json[pos] == '"') {
64 // String value
65 size_t start = pos + 1;
66 size_t end = json.find('"', start);
67 while (end != std::string::npos && end > 0 && json[end - 1] == '\\') {
68 end = json.find('"', end + 1);
69 }
70 if (end == std::string::npos)
71 return "";
72 return json.substr(start, end - start);
73 } else if (json[pos] == '{') {
74 // Object value - find matching brace
75 int depth = 1;
76 size_t start = pos;
77 pos++;
78 while (pos < json.length() && depth > 0) {
79 if (json[pos] == '{')
80 depth++;
81 else if (json[pos] == '}')
82 depth--;
83 pos++;
84 }
85 return json.substr(start, pos - start);
86 } else if (json[pos] == '[') {
87 // Array value - find matching bracket
88 int depth = 1;
89 size_t start = pos;
90 pos++;
91 while (pos < json.length() && depth > 0) {
92 if (json[pos] == '[')
93 depth++;
94 else if (json[pos] == ']')
95 depth--;
96 pos++;
97 }
98 return json.substr(start, pos - start);
99 } else {
100 // Number or boolean
101 size_t start = pos;
102 while (pos < json.length() && json[pos] != ',' && json[pos] != '}' &&
103 json[pos] != ']') {
104 pos++;
105 }
106 return json.substr(start, pos - start);
107 }
108}
109
110int64_t ExtractJsonInt(const std::string& json, const std::string& key,
111 int64_t default_value = 0) {
112 std::string value = ExtractJsonString(json, key);
113 if (value.empty())
114 return default_value;
115
116 // Handle hex strings like "0x7E0000"
117 if (value.length() > 2 && value[0] == '0' &&
118 (value[1] == 'x' || value[1] == 'X')) {
119 int64_t result;
120 std::string hex_str = value.substr(2);
121 auto [ptr, ec] = std::from_chars(
122 hex_str.data(), hex_str.data() + hex_str.size(), result, 16);
123 if (ec == std::errc()) {
124 return result;
125 }
126 }
127
128 int64_t result;
129 if (absl::SimpleAtoi(value, &result)) {
130 return result;
131 }
132 return default_value;
133}
134
135double ExtractJsonDouble(const std::string& json, const std::string& key,
136 double default_value = 0.0) {
137 std::string value = ExtractJsonString(json, key);
138 if (value.empty())
139 return default_value;
140
141 double result;
142 if (absl::SimpleAtod(value, &result)) {
143 return result;
144 }
145 return default_value;
146}
147
148bool ExtractJsonBool(const std::string& json, const std::string& key,
149 bool default_value = false) {
150 std::string value = ExtractJsonString(json, key);
151 if (value.empty())
152 return default_value;
153 return value == "true";
154}
155
156std::string BuildJsonCommand(const std::string& type) {
157 return absl::StrFormat("{\"type\":\"%s\"}\n", type);
158}
159
160std::string EscapeJsonValue(const std::string& value) {
161 std::string escaped;
162 escaped.reserve(value.size());
163 for (char c : value) {
164 switch (c) {
165 case '\\':
166 escaped += "\\\\";
167 break;
168 case '"':
169 escaped += "\\\"";
170 break;
171 case '\n':
172 escaped += "\\n";
173 break;
174 case '\r':
175 escaped += "\\r";
176 break;
177 case '\t':
178 escaped += "\\t";
179 break;
180 default:
181 escaped += c;
182 break;
183 }
184 }
185 return escaped;
186}
187
189 const std::string& type,
190 const std::vector<std::pair<std::string, std::string>>& params) {
191 std::stringstream ss;
192 ss << "{\"type\":\"" << type << "\"";
193 for (const auto& [key, value] : params) {
194 ss << ",\"" << key << "\":\"" << EscapeJsonValue(value) << "\"";
195 }
196 ss << "}\n";
197 return ss.str();
198}
199
200constexpr size_t kMaxResponseSize = 4 * 1024 * 1024;
201constexpr int kConnectTimeoutMs = 2000;
202
204#ifdef _WIN32
205 return WSAGetLastError();
206#else
207 return errno;
208#endif
209}
210
211std::string SocketErrorMessage(int error) {
212#ifdef _WIN32
213 return absl::StrCat("Winsock error ", error);
214#else
215 return strerror(error);
216#endif
217}
218
219// Commands must not block a UI thread forever, but the event stream is
220// long-lived and idles between emulator events.
221constexpr int kDefaultSendTimeoutMs = 5000;
222
223// Tests override this to exercise the write deadline without stalling for
224// the production timeout.
226 // Read on each connect rather than caching, so a test that sets the
227 // override is not affected by whichever test connected first.
228 const char* raw = std::getenv("YAZE_MESEN_SEND_TIMEOUT_MS");
229 int parsed = 0;
230 if (raw != nullptr && absl::SimpleAtoi(raw, &parsed) && parsed > 0) {
231 return parsed;
232 }
234}
235
236// The event socket needs a bounded wakeup so Unsubscribe()/Disconnect() can
237// join the event thread promptly; a receive timeout there means "no event
238// yet", which EventLoop treats as a retry rather than an error.
239constexpr int kDefaultEventPollMs = 1000;
240
242 const char* raw = std::getenv("YAZE_MESEN_EVENT_POLL_MS");
243 int parsed = 0;
244 if (raw != nullptr && absl::SimpleAtoi(raw, &parsed) && parsed > 0) {
245 return parsed;
246 }
247 return kDefaultEventPollMs;
248}
249
250// A closed peer must surface as a status, never as a process-level signal.
251// Linux carries this per send(); BSD/macOS carry it per socket.
252#if defined(MSG_NOSIGNAL)
253constexpr int kSendFlags = MSG_NOSIGNAL;
254#else
255constexpr int kSendFlags = 0;
256#endif
257
259#if defined(SO_NOSIGPIPE)
260 const int enable = 1;
261 (void)setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE,
262 reinterpret_cast<const char*>(&enable), sizeof(enable));
263#else
264 (void)fd;
265#endif
266}
267
268void SetSocketSendTimeout(SocketHandle fd, int timeout_ms) {
269#ifdef _WIN32
270 const DWORD timeout = static_cast<DWORD>(timeout_ms);
271 (void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO,
272 reinterpret_cast<const char*>(&timeout), sizeof(timeout));
273#else
274 timeval timeout{};
275 timeout.tv_sec = timeout_ms / 1000;
276 timeout.tv_usec = (timeout_ms % 1000) * 1000;
277 (void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
278#endif
279}
280
281void SetSocketReceiveTimeout(SocketHandle fd, int timeout_ms) {
282#ifdef _WIN32
283 const DWORD timeout = static_cast<DWORD>(timeout_ms);
284 (void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO,
285 reinterpret_cast<const char*>(&timeout), sizeof(timeout));
286#else
287 timeval timeout{};
288 timeout.tv_sec = timeout_ms / 1000;
289 timeout.tv_usec = (timeout_ms % 1000) * 1000;
290 (void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
291#endif
292}
293
294bool LooksLikeTcpEndpoint(const std::string& path) {
295 return path.rfind("tcp://", 0) == 0 || path.rfind("tcp:", 0) == 0;
296}
297
298absl::StatusOr<std::pair<std::string, uint16_t>> ParseTcpEndpoint(
299 const std::string& path) {
300 std::string raw = path;
301 if (raw.rfind("tcp://", 0) == 0) {
302 raw = raw.substr(6);
303 } else if (raw.rfind("tcp:", 0) == 0) {
304 raw = raw.substr(4);
305 } else {
306 return absl::InvalidArgumentError(
307 absl::StrCat("Not a tcp:// endpoint: ", path));
308 }
309
310 std::string host = "127.0.0.1";
311 std::string port_str = raw;
312 const auto colon = raw.rfind(':');
313 if (colon != std::string::npos) {
314 host = raw.substr(0, colon);
315 port_str = raw.substr(colon + 1);
316 if (host.empty()) {
317 host = "127.0.0.1";
318 }
319 }
320 int port = 0;
321 if (!absl::SimpleAtoi(port_str, &port) || port <= 0 || port > 65535) {
322 return absl::InvalidArgumentError(
323 absl::StrCat("Invalid TCP port in ", path));
324 }
325 return std::make_pair(host, static_cast<uint16_t>(port));
326}
327
328bool SetSocketBlocking(SocketHandle fd, bool blocking) {
329#ifdef _WIN32
330 u_long mode = blocking ? 0UL : 1UL;
331 return ioctlsocket(fd, FIONBIO, &mode) == 0;
332#else
333 const int flags = fcntl(fd, F_GETFL, 0);
334 if (flags < 0) {
335 return false;
336 }
337 const int next_flags =
338 blocking ? (flags & ~O_NONBLOCK) : (flags | O_NONBLOCK);
339 return fcntl(fd, F_SETFL, next_flags) == 0;
340#endif
341}
342
343// Waits until the socket accepts more data or the budget expires.
344// Returns Ok when writable, DeadlineExceeded on timeout.
345absl::Status WaitForWritable(SocketHandle fd, int timeout_ms) {
346#ifdef _WIN32
347 fd_set write_fds;
348 FD_ZERO(&write_fds);
349 FD_SET(fd, &write_fds);
350 timeval timeout;
351 timeout.tv_sec = timeout_ms / 1000;
352 timeout.tv_usec = (timeout_ms % 1000) * 1000;
353 const int ready = select(0, nullptr, &write_fds, nullptr, &timeout);
354#else
355 pollfd descriptor{};
356 descriptor.fd = fd;
357 descriptor.events = POLLOUT;
358 const int ready = poll(&descriptor, 1, timeout_ms);
359#endif
360 if (ready == 0) {
361 return absl::DeadlineExceededError("Socket did not become writable");
362 }
363 if (ready < 0) {
364 const int error = LastSocketError();
365#ifdef _WIN32
366 if (error == WSAEINTR) {
367 return absl::OkStatus();
368 }
369#else
370 if (error == EINTR) {
371 return absl::OkStatus();
372 }
373#endif
374 return absl::InternalError(
375 absl::StrCat("Failed waiting to send: ", SocketErrorMessage(error)));
376 }
377 return absl::OkStatus();
378}
379
380// send() may accept fewer bytes than requested, and a blocking send() can
381// park indefinitely when the peer stops reading: SO_SNDTIMEO is not honored
382// for Unix-domain sockets on macOS. Send non-blocking and bound the whole
383// command with one deadline.
384absl::Status SendAll(SocketHandle fd, const std::string& data) {
385 const auto deadline = std::chrono::steady_clock::now() +
386 std::chrono::milliseconds(SendTimeoutMs());
387 const bool was_blocking = SetSocketBlocking(fd, false);
388 absl::Cleanup restore = [fd, was_blocking] {
389 if (was_blocking) {
390 (void)SetSocketBlocking(fd, true);
391 }
392 };
393
394 size_t offset = 0;
395 while (offset < data.size()) {
396 const auto now = std::chrono::steady_clock::now();
397 const auto remaining =
398 std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now)
399 .count();
400 if (remaining <= 0) {
401 return absl::DeadlineExceededError(
402 absl::StrCat("Timed out sending command after ", SendTimeoutMs(),
403 "ms (", offset, " of ", data.size(), " bytes)"));
404 }
405 const ssize_t sent =
406 send(fd, data.c_str() + offset, static_cast<int>(data.size() - offset),
407 kSendFlags);
408 if (sent > 0) {
409 offset += static_cast<size_t>(sent);
410 continue;
411 }
412 const int error = LastSocketError();
413#ifdef _WIN32
414 if (error == WSAEINTR) {
415 continue;
416 }
417 if (error == WSAEWOULDBLOCK || error == WSAETIMEDOUT) {
418#else
419 if (error == EINTR) {
420 continue;
421 }
422 if (error == EAGAIN || error == EWOULDBLOCK) {
423#endif
424 const absl::Status writable =
425 WaitForWritable(fd, static_cast<int>(remaining));
426 if (absl::IsDeadlineExceeded(writable)) {
427 return absl::DeadlineExceededError(
428 absl::StrCat("Timed out sending command after ", SendTimeoutMs(),
429 "ms (", offset, " of ", data.size(), " bytes)"));
430 }
431 if (!writable.ok()) {
432 return writable;
433 }
434 continue;
435 }
436 return absl::InternalError(
437 absl::StrCat("Failed to send command: ", SocketErrorMessage(error)));
438 }
439 return absl::OkStatus();
440}
441
443 const std::string& socket_path) {
444#ifdef _WIN32
445 fd_set write_fds;
446 fd_set exception_fds;
447 FD_ZERO(&write_fds);
448 FD_ZERO(&exception_fds);
449 FD_SET(fd, &write_fds);
450 FD_SET(fd, &exception_fds);
451 timeval timeout;
452 timeout.tv_sec = kConnectTimeoutMs / 1000;
453 timeout.tv_usec = (kConnectTimeoutMs % 1000) * 1000;
454 // Winsock reports successful nonblocking connects through writefds and
455 // failed connects through exceptfds. SO_ERROR below determines the result.
456 const int ready = select(0, nullptr, &write_fds, &exception_fds, &timeout);
457#else
458 pollfd descriptor{};
459 descriptor.fd = fd;
460 descriptor.events = POLLOUT;
461 const int ready = poll(&descriptor, 1, kConnectTimeoutMs);
462#endif
463 if (ready == 0) {
464 return absl::DeadlineExceededError(absl::StrCat("Timed out connecting to ",
465 socket_path, " after ",
466 kConnectTimeoutMs, "ms"));
467 }
468 if (ready < 0) {
469 const int error = LastSocketError();
470 return absl::UnavailableError(
471 absl::StrCat("Failed while waiting to connect to ", socket_path, ": ",
472 SocketErrorMessage(error)));
473 }
474
475 int connect_error = 0;
476#ifdef _WIN32
477 int error_length = sizeof(connect_error);
478 const int get_error =
479 getsockopt(fd, SOL_SOCKET, SO_ERROR,
480 reinterpret_cast<char*>(&connect_error), &error_length);
481#else
482 socklen_t error_length = sizeof(connect_error);
483 const int get_error =
484 getsockopt(fd, SOL_SOCKET, SO_ERROR, &connect_error, &error_length);
485#endif
486 if (get_error != 0) {
487 const int error = LastSocketError();
488 return absl::UnavailableError(
489 absl::StrCat("Failed to read connect status for ", socket_path, ": ",
490 SocketErrorMessage(error)));
491 }
492 if (connect_error != 0) {
493 return absl::UnavailableError(
494 absl::StrCat("Failed to connect to ", socket_path, ": ",
495 SocketErrorMessage(connect_error)));
496 }
497 return absl::OkStatus();
498}
499
500absl::Status ConnectTcpEndpoint(const std::string& socket_path,
501 SocketHandle* socket_fd) {
502 auto parsed = ParseTcpEndpoint(socket_path);
503 if (!parsed.ok()) {
504 return parsed.status();
505 }
506 const std::string& host = parsed->first;
507 const uint16_t port = parsed->second;
508
509 SocketHandle fd = socket(AF_INET, SOCK_STREAM, 0);
510 if (fd == kInvalidSocketHandle) {
511 const int error = LastSocketError();
512 return absl::InternalError(absl::StrCat("Failed to create TCP socket: ",
513 SocketErrorMessage(error)));
514 }
515 SuppressSigpipe(fd);
517
518 if (!SetSocketBlocking(fd, false)) {
519 const int error = LastSocketError();
520 close(fd);
521 return absl::InternalError(
522 absl::StrCat("Failed to set non-blocking mode on TCP socket: ",
523 SocketErrorMessage(error)));
524 }
525
526 sockaddr_in addr;
527 memset(&addr, 0, sizeof(addr));
528 addr.sin_family = AF_INET;
529 addr.sin_port = htons(port);
530 if (inet_pton(AF_INET, host.c_str(), &addr.sin_addr) != 1) {
531 addrinfo hints;
532 memset(&hints, 0, sizeof(hints));
533 hints.ai_family = AF_INET;
534 hints.ai_socktype = SOCK_STREAM;
535 addrinfo* result = nullptr;
536 const int gai = getaddrinfo(host.c_str(), nullptr, &hints, &result);
537 if (gai != 0 || result == nullptr) {
538 close(fd);
539 return absl::UnavailableError(
540 absl::StrCat("Failed to resolve ", host, ": ",
541 gai != 0 ? gai_strerror(gai) : "no address"));
542 }
543 addr.sin_addr = reinterpret_cast<sockaddr_in*>(result->ai_addr)->sin_addr;
544 freeaddrinfo(result);
545 }
546
547 const int result =
548 connect(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
549 const int connect_error = result < 0 ? LastSocketError() : 0;
550#ifdef _WIN32
551 const bool in_progress = result < 0 && (connect_error == WSAEWOULDBLOCK ||
552 connect_error == WSAEINPROGRESS ||
553 connect_error == WSAEALREADY);
554#else
555 const bool in_progress = result < 0 && connect_error == EINPROGRESS;
556#endif
557 if (result < 0 && !in_progress) {
558 close(fd);
559 return absl::UnavailableError(
560 absl::StrCat("Failed to connect to ", socket_path, ": ",
561 SocketErrorMessage(connect_error)));
562 }
563 if (in_progress) {
564 auto wait_status = WaitForConnectComplete(fd, socket_path);
565 if (!wait_status.ok()) {
566 close(fd);
567 return wait_status;
568 }
569 }
570
571 if (!SetSocketBlocking(fd, true)) {
572 const int error = LastSocketError();
573 close(fd);
574 return absl::InternalError(
575 absl::StrCat("Failed to restore blocking mode on TCP socket: ",
576 SocketErrorMessage(error)));
577 }
578
579 *socket_fd = fd;
580 return absl::OkStatus();
581}
582
583absl::Status ConnectUnixEndpoint(const std::string& socket_path,
584 SocketHandle* socket_fd) {
585 SocketHandle fd = socket(AF_UNIX, SOCK_STREAM, 0);
586 if (fd == kInvalidSocketHandle) {
587 const int error = LastSocketError();
588 return absl::InternalError(
589 absl::StrCat("Failed to create socket: ", SocketErrorMessage(error)));
590 }
591 SuppressSigpipe(fd);
593
594 struct sockaddr_un addr;
595 memset(&addr, 0, sizeof(addr));
596 addr.sun_family = AF_UNIX;
597 strncpy(addr.sun_path, socket_path.c_str(), sizeof(addr.sun_path) - 1);
598
599 if (connect(fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) <
600 0) {
601 const int error = LastSocketError();
602 close(fd);
603 return absl::UnavailableError(absl::StrCat(
604 "Failed to connect to ", socket_path, ": ", SocketErrorMessage(error)));
605 }
606
607 *socket_fd = fd;
608 return absl::OkStatus();
609}
610
611absl::Status ConnectSocketToPath(const std::string& socket_path,
612 SocketHandle* socket_fd) {
613 if (socket_fd == nullptr) {
614 return absl::InvalidArgumentError("socket_fd output pointer is null");
615 }
616 if (LooksLikeTcpEndpoint(socket_path)) {
617 return ConnectTcpEndpoint(socket_path, socket_fd);
618 }
619 return ConnectUnixEndpoint(socket_path, socket_fd);
620}
621
623 if (fd == kInvalidSocketHandle) {
624 return;
625 }
626#ifdef _WIN32
627 shutdown(fd, SD_BOTH);
628#else
629 shutdown(fd, SHUT_RDWR);
630#endif
631}
632
633} // namespace
634
636#ifdef _WIN32
637 WSADATA winsock_data{};
638 winsock_startup_error_ = WSAStartup(MAKEWORD(2, 2), &winsock_data);
639 winsock_started_ = winsock_startup_error_ == 0;
640#endif
641}
642
644 Disconnect();
645#ifdef _WIN32
646 if (winsock_started_) {
647 WSACleanup();
648 }
649#endif
650}
651
653 auto paths = FindSocketPaths();
654 if (paths.empty()) {
655 return absl::NotFoundError(
656 "No Mesen2 socket found. Is Mesen2-OoS running?");
657 }
658 return Connect(paths[0]);
659}
660
661absl::Status MesenSocketClient::Connect(const std::string& socket_path) {
662#ifdef _WIN32
663 if (!winsock_started_) {
664 return absl::InternalError(
665 absl::StrCat("Failed to initialize Winsock: ",
666 SocketErrorMessage(winsock_startup_error_)));
667 }
668#endif
669
670 // A mid-command failure clears connected_ but leaves socket_fd_ open, so
671 // IsConnected() is not a reliable cleanup gate. Disconnect() is idempotent.
672 Disconnect();
673
675 auto connect_status = ConnectSocketToPath(socket_path, &fd);
676 if (!connect_status.ok()) {
677 return connect_status;
678 }
679
680 socket_fd_ = fd;
681 socket_path_ = socket_path;
682 connected_ = true;
683
684 // Verify connection with a ping
685 auto status = Ping();
686 if (!status.ok()) {
687 Disconnect();
688 return status;
689 }
690
691 return absl::OkStatus();
692}
693
695 // Best-effort: stop event stream first so background thread exits cleanly.
696 (void)Unsubscribe();
697
699 close(socket_fd_);
701 }
702 socket_path_.clear();
703 connected_ = false;
704}
705
707 return connected_;
708}
709
710std::vector<std::string> MesenSocketClient::FindSocketPaths() {
711 const char* env_path = std::getenv("MESEN2_SOCKET_PATH");
712 if (env_path && env_path[0] != '\0') {
713 std::string env(env_path);
714 if (LooksLikeTcpEndpoint(env)) {
715 // An explicit TCP target is authoritative. Preserve malformed values so
716 // Connect() reports the configuration error instead of attaching to an
717 // unrelated local emulator discovered in the temporary directory.
718 return {env};
719 }
720#ifdef _WIN32
721 // Windows AF_UNIX sockets don't report S_IFSOCK via stat; trust the env var
722 if (std::filesystem::exists(env_path)) {
723 return {env};
724 }
725#else
726 struct stat st;
727 if (stat(env_path, &st) == 0 && (st.st_mode & S_IFMT) == S_IFSOCK) {
728 return {env};
729 }
730#endif
731 }
732
733 std::vector<std::string> paths;
734 namespace fs = std::filesystem;
735 std::vector<fs::path> search_paths;
736
737#ifdef _WIN32
738 search_paths.push_back(fs::temp_directory_path());
739#else
740 search_paths.push_back("/tmp");
741#endif
742
743 std::regex socket_pattern("mesen2-\\d+\\.sock");
744
745 for (const auto& search_path : search_paths) {
746 std::error_code ec;
747 if (!fs::exists(search_path, ec))
748 continue;
749
750 for (const auto& entry : fs::directory_iterator(search_path, ec)) {
751 if (ec)
752 break;
753 // On Windows, checking is_socket might be unreliable or not supported for
754 // AF_UNIX files, so we mainly rely on the filename pattern.
755 std::string filename = entry.path().filename().string();
756 if (std::regex_match(filename, socket_pattern)) {
757 paths.push_back(entry.path().string());
758 }
759 }
760 }
761
762 return paths;
763}
764
765std::vector<std::string> MesenSocketClient::ListAvailableSockets() {
766 return FindSocketPaths();
767}
768
769absl::StatusOr<std::string> MesenSocketClient::SendCommandOnSocket(
770 SocketHandle fd, const std::string& json, bool update_connection_state) {
771 if (fd == kInvalidSocketHandle) {
772 return absl::FailedPreconditionError("Socket is not connected");
773 }
774
775 // Send command
776 if (const absl::Status sent = SendAll(fd, json); !sent.ok()) {
777 if (update_connection_state) {
778 connected_ = false;
779 }
780 return sent;
781 }
782
783 // Receive response (with timeout)
784 SetSocketReceiveTimeout(fd, 5000);
785
786 std::string response;
787 char buffer[4096];
788 while (true) {
789 ssize_t received = recv(fd, buffer, sizeof(buffer), 0);
790 if (received < 0) {
791 const int error = LastSocketError();
792 // SO_RCVTIMEO makes recv() non-restartable, so a caught signal arrives
793 // here as EINTR and must be retried rather than failing the command.
794#ifdef _WIN32
795 if (error == WSAEINTR) {
796 continue;
797 }
798 if (error == WSAEWOULDBLOCK || error == WSAETIMEDOUT) {
799#else
800 if (error == EINTR) {
801 continue;
802 }
803 if (error == EAGAIN || error == EWOULDBLOCK) {
804#endif
805 if (response.empty()) {
806 return absl::DeadlineExceededError("Timeout waiting for response");
807 }
808 break;
809 }
810 if (update_connection_state) {
811 connected_ = false;
812 }
813 return absl::InternalError(absl::StrCat("Failed to receive response: ",
814 SocketErrorMessage(error)));
815 }
816 if (received == 0) {
817 break;
818 }
819 response.append(buffer, static_cast<size_t>(received));
820 if (response.size() > kMaxResponseSize) {
821 return absl::ResourceExhaustedError("Mesen2 response too large");
822 }
823 if (response.find('\n') != std::string::npos) {
824 break;
825 }
826 }
827 if (response.empty()) {
828 return absl::DeadlineExceededError("Empty response from Mesen2");
829 }
830 auto newline_pos = response.find('\n');
831 if (newline_pos != std::string::npos) {
832 response = response.substr(0, newline_pos);
833 }
834
835 return ParseResponse(response);
836}
837
838absl::StatusOr<std::string> MesenSocketClient::SendCommand(
839 const std::string& json) {
840 if (!IsConnected()) {
841 return absl::FailedPreconditionError("Not connected to Mesen2");
842 }
843
844 std::lock_guard<std::mutex> lock(command_mutex_);
845 return SendCommandOnSocket(socket_fd_, json,
846 /*update_connection_state=*/true);
847}
848
849absl::StatusOr<std::string> MesenSocketClient::ParseResponse(
850 const std::string& response) {
851 bool success = ExtractJsonBool(response, "success");
852 if (!success) {
853 std::string error = ExtractJsonString(response, "error");
854 if (error.empty())
855 error = "Unknown Mesen2 error";
856 return absl::InternalError(error);
857 }
858
859 std::string data = ExtractJsonString(response, "data");
860 return data.empty() ? response : data;
861}
862
863// ─────────────────────────────────────────────────────────────────────────────
864// Control Commands
865// ─────────────────────────────────────────────────────────────────────────────
866
868 auto result = SendCommand(BuildJsonCommand("PING"));
869 if (!result.ok())
870 return result.status();
871 return absl::OkStatus();
872}
873
874absl::StatusOr<MesenState> MesenSocketClient::GetState() {
875 auto result = SendCommand(BuildJsonCommand("STATE"));
876 if (!result.ok())
877 return result.status();
878
879 MesenState state;
880 state.running = ExtractJsonBool(*result, "running");
881 state.paused = ExtractJsonBool(*result, "paused");
882 state.debugging = ExtractJsonBool(*result, "debugging");
883 state.frame = ExtractJsonInt(*result, "frame");
884 state.fps = ExtractJsonDouble(*result, "fps");
885 state.console_type = ExtractJsonInt(*result, "consoleType");
886 return state;
887}
888
890 auto result = SendCommand(BuildJsonCommand("PAUSE"));
891 return result.status();
892}
893
895 auto result = SendCommand(BuildJsonCommand("RESUME"));
896 return result.status();
897}
898
900 auto result = SendCommand(BuildJsonCommand("RESET"));
901 return result.status();
902}
903
905 auto result = SendCommand(BuildJsonCommand("FRAME"));
906 return result.status();
907}
908
909absl::Status MesenSocketClient::Step(int count, const std::string& mode) {
910 auto result = SendCommand(BuildJsonCommand(
911 "STEP", {{"count", std::to_string(count)}, {"mode", mode}}));
912 return result.status();
913}
914
915// ─────────────────────────────────────────────────────────────────────────────
916// Input Commands
917// ─────────────────────────────────────────────────────────────────────────────
918
920 bool pressed) {
921 current_input_.SetButton(button, pressed);
923}
924
926 const emu::input::ControllerState& state) {
927 current_input_ = state;
928
929 std::vector<std::string> buttons;
931 buttons.push_back("a");
933 buttons.push_back("b");
935 buttons.push_back("x");
937 buttons.push_back("y");
939 buttons.push_back("l");
941 buttons.push_back("r");
943 buttons.push_back("select");
945 buttons.push_back("start");
947 buttons.push_back("up");
949 buttons.push_back("down");
951 buttons.push_back("left");
953 buttons.push_back("right");
954
955 std::string buttons_str;
956 for (size_t i = 0; i < buttons.size(); ++i) {
957 if (i > 0)
958 buttons_str += ",";
959 buttons_str += buttons[i];
960 }
961
962 auto result =
963 SendCommand(BuildJsonCommand("INPUT", {{"buttons", buttons_str}}));
964 return result.status();
965}
966
967// ─────────────────────────────────────────────────────────────────────────────
968// Memory Commands
969// ─────────────────────────────────────────────────────────────────────────────
970
971absl::StatusOr<uint8_t> MesenSocketClient::ReadByte(uint32_t addr) {
972 auto result = SendCommand(
973 BuildJsonCommand("READ", {{"addr", absl::StrFormat("0x%06X", addr)}}));
974 if (!result.ok())
975 return result.status();
976 return static_cast<uint8_t>(ExtractJsonInt(*result, "data", 0));
977}
978
979absl::StatusOr<uint16_t> MesenSocketClient::ReadWord(uint32_t addr) {
980 auto result = SendCommand(
981 BuildJsonCommand("READ16", {{"addr", absl::StrFormat("0x%06X", addr)}}));
982 if (!result.ok())
983 return result.status();
984 return static_cast<uint16_t>(ExtractJsonInt(*result, "data", 0));
985}
986
987absl::StatusOr<std::vector<uint8_t>> MesenSocketClient::ReadBlock(uint32_t addr,
988 size_t len) {
989 auto result = SendCommand(
990 BuildJsonCommand("READBLOCK", {{"addr", absl::StrFormat("0x%06X", addr)},
991 {"len", std::to_string(len)}}));
992 if (!result.ok())
993 return result.status();
994
995 // Response is hex string
996 std::string hex = ExtractJsonString(*result, "data");
997 if (hex.empty()) {
998 // Try raw data field
999 hex = *result;
1000 }
1001
1002 std::vector<uint8_t> data;
1003 data.reserve(len);
1004
1005 for (size_t i = 0; i + 1 < hex.length(); i += 2) {
1006 int byte;
1007 std::string byte_hex = hex.substr(i, 2);
1008 auto [ptr, ec] = std::from_chars(
1009 byte_hex.data(), byte_hex.data() + byte_hex.size(), byte, 16);
1010 if (ec == std::errc()) {
1011 data.push_back(static_cast<uint8_t>(byte));
1012 }
1013 }
1014 return data;
1015}
1016
1017absl::Status MesenSocketClient::WriteByte(uint32_t addr, uint8_t value) {
1018 auto result = SendCommand(
1019 BuildJsonCommand("WRITE", {{"addr", absl::StrFormat("0x%06X", addr)},
1020 {"value", absl::StrFormat("0x%02X", value)}}));
1021 return result.status();
1022}
1023
1024absl::Status MesenSocketClient::WriteWord(uint32_t addr, uint16_t value) {
1025 auto result = SendCommand(BuildJsonCommand(
1026 "WRITE16", {{"addr", absl::StrFormat("0x%06X", addr)},
1027 {"value", absl::StrFormat("0x%04X", value)}}));
1028 return result.status();
1029}
1030
1031absl::Status MesenSocketClient::WriteBlock(uint32_t addr,
1032 const std::vector<uint8_t>& data) {
1033 std::stringstream hex;
1034 for (uint8_t byte : data) {
1035 hex << absl::StrFormat("%02X", byte);
1036 }
1037 auto result = SendCommand(BuildJsonCommand(
1038 "WRITEBLOCK",
1039 {{"addr", absl::StrFormat("0x%06X", addr)}, {"hex", hex.str()}}));
1040 return result.status();
1041}
1042
1043// ─────────────────────────────────────────────────────────────────────────────
1044// Debugging Commands
1045// ─────────────────────────────────────────────────────────────────────────────
1046
1047absl::StatusOr<CpuState> MesenSocketClient::GetCpuState() {
1048 auto result = SendCommand(BuildJsonCommand("CPU"));
1049 if (!result.ok())
1050 return result.status();
1051
1052 CpuState state{};
1053 state.A = static_cast<uint16_t>(ExtractJsonInt(*result, "a"));
1054 state.X = static_cast<uint16_t>(ExtractJsonInt(*result, "x"));
1055 state.Y = static_cast<uint16_t>(ExtractJsonInt(*result, "y"));
1056 state.SP = static_cast<uint16_t>(ExtractJsonInt(*result, "sp"));
1057 state.D = static_cast<uint16_t>(ExtractJsonInt(*result, "d"));
1058 state.PC = static_cast<uint32_t>(ExtractJsonInt(*result, "pc"));
1059 state.K = static_cast<uint8_t>(ExtractJsonInt(*result, "k"));
1060 state.DBR = static_cast<uint8_t>(ExtractJsonInt(*result, "dbr"));
1061 state.P = static_cast<uint8_t>(ExtractJsonInt(*result, "p"));
1062 state.emulation_mode = ExtractJsonBool(*result, "emulationMode");
1063 return state;
1064}
1065
1066absl::StatusOr<std::string> MesenSocketClient::Disassemble(uint32_t addr,
1067 int count) {
1068 auto result = SendCommand(
1069 BuildJsonCommand("DISASM", {{"addr", absl::StrFormat("0x%06X", addr)},
1070 {"count", std::to_string(count)}}));
1071 if (!result.ok())
1072 return result.status();
1073 return *result;
1074}
1075
1077 uint32_t addr, BreakpointType type, const std::string& condition) {
1078 std::string type_str;
1079 switch (type) {
1081 type_str = "exec";
1082 break;
1084 type_str = "read";
1085 break;
1087 type_str = "write";
1088 break;
1090 type_str = "rw";
1091 break;
1092 }
1093
1094 std::vector<std::pair<std::string, std::string>> params = {
1095 {"action", "add"},
1096 {"addr", absl::StrFormat("0x%06X", addr)},
1097 {"bptype", type_str}};
1098
1099 if (!condition.empty()) {
1100 params.push_back({"condition", condition});
1101 }
1102
1103 auto result = SendCommand(BuildJsonCommand("BREAKPOINT", params));
1104 if (!result.ok())
1105 return result.status();
1106
1107 return static_cast<int>(ExtractJsonInt(*result, "id", -1));
1108}
1109
1111 auto result = SendCommand(BuildJsonCommand(
1112 "BREAKPOINT", {{"action", "remove"}, {"id", std::to_string(id)}}));
1113 return result.status();
1114}
1115
1117 auto result =
1118 SendCommand(BuildJsonCommand("BREAKPOINT", {{"action", "clear"}}));
1119 return result.status();
1120}
1121
1122absl::StatusOr<std::string> MesenSocketClient::GetTrace(int count) {
1123 auto result = SendCommand(
1124 BuildJsonCommand("TRACE", {{"count", std::to_string(count)}}));
1125 if (!result.ok())
1126 return result.status();
1127 return *result;
1128}
1129
1130// ─────────────────────────────────────────────────────────────────────────────
1131// ALTTP Commands
1132// ─────────────────────────────────────────────────────────────────────────────
1133
1134absl::StatusOr<GameState> MesenSocketClient::GetGameState() {
1135 auto result = SendCommand(BuildJsonCommand("GAMESTATE"));
1136 if (!result.ok())
1137 return result.status();
1138
1139 GameState state;
1140
1141 // Parse link state
1142 std::string link_json = ExtractJsonString(*result, "link");
1143 state.link.x = static_cast<uint16_t>(ExtractJsonInt(link_json, "x"));
1144 state.link.y = static_cast<uint16_t>(ExtractJsonInt(link_json, "y"));
1145 state.link.layer = static_cast<uint8_t>(ExtractJsonInt(link_json, "layer"));
1146 state.link.direction =
1147 static_cast<uint8_t>(ExtractJsonInt(link_json, "direction"));
1148 state.link.state = static_cast<uint8_t>(ExtractJsonInt(link_json, "state"));
1149 state.link.pose = static_cast<uint8_t>(ExtractJsonInt(link_json, "pose"));
1150
1151 // Parse health
1152 std::string health_json = ExtractJsonString(*result, "health");
1153 state.items.current_health =
1154 static_cast<uint8_t>(ExtractJsonInt(health_json, "current"));
1155 state.items.max_health =
1156 static_cast<uint8_t>(ExtractJsonInt(health_json, "max"));
1157
1158 // Parse items
1159 std::string items_json = ExtractJsonString(*result, "items");
1160 state.items.magic = static_cast<uint8_t>(ExtractJsonInt(items_json, "magic"));
1161 state.items.rupees =
1162 static_cast<uint16_t>(ExtractJsonInt(items_json, "rupees"));
1163 state.items.bombs = static_cast<uint8_t>(ExtractJsonInt(items_json, "bombs"));
1164 state.items.arrows =
1165 static_cast<uint8_t>(ExtractJsonInt(items_json, "arrows"));
1166
1167 // Parse game mode
1168 std::string game_json = ExtractJsonString(*result, "game");
1169 state.game.mode = static_cast<uint8_t>(ExtractJsonInt(game_json, "mode"));
1170 state.game.submode =
1171 static_cast<uint8_t>(ExtractJsonInt(game_json, "submode"));
1172 state.game.indoors = ExtractJsonBool(game_json, "indoors");
1173 state.game.room_id =
1174 static_cast<uint16_t>(ExtractJsonInt(game_json, "room_id"));
1175 state.game.overworld_area =
1176 static_cast<uint8_t>(ExtractJsonInt(game_json, "overworld_area"));
1177
1178 return state;
1179}
1180
1181absl::StatusOr<std::vector<SpriteInfo>> MesenSocketClient::GetSprites(
1182 bool all) {
1183 std::vector<std::pair<std::string, std::string>> params;
1184 if (all) {
1185 params.push_back({"all", "true"});
1186 }
1187
1188 auto result =
1189 SendCommand(params.empty() ? BuildJsonCommand("SPRITES")
1190 : BuildJsonCommand("SPRITES", params));
1191 if (!result.ok())
1192 return result.status();
1193
1194 std::vector<SpriteInfo> sprites;
1195
1196 // Find sprites array in response
1197 size_t array_start = result->find("[");
1198 size_t array_end = result->rfind("]");
1199 if (array_start == std::string::npos || array_end == std::string::npos) {
1200 return sprites;
1201 }
1202
1203 std::string array_content =
1204 result->substr(array_start + 1, array_end - array_start - 1);
1205
1206 // Parse each sprite object
1207 size_t pos = 0;
1208 while ((pos = array_content.find("{", pos)) != std::string::npos) {
1209 size_t end = array_content.find("}", pos);
1210 if (end == std::string::npos)
1211 break;
1212
1213 std::string sprite_json = array_content.substr(pos, end - pos + 1);
1214
1215 SpriteInfo sprite;
1216 sprite.slot = static_cast<int>(ExtractJsonInt(sprite_json, "slot"));
1217 sprite.type = static_cast<uint8_t>(ExtractJsonInt(sprite_json, "type"));
1218 sprite.state = static_cast<uint8_t>(ExtractJsonInt(sprite_json, "state"));
1219 sprite.x = static_cast<uint16_t>(ExtractJsonInt(sprite_json, "x"));
1220 sprite.y = static_cast<uint16_t>(ExtractJsonInt(sprite_json, "y"));
1221 sprite.health = static_cast<uint8_t>(ExtractJsonInt(sprite_json, "health"));
1222 sprite.subtype =
1223 static_cast<uint8_t>(ExtractJsonInt(sprite_json, "subtype"));
1224
1225 sprites.push_back(sprite);
1226 pos = end + 1;
1227 }
1228
1229 return sprites;
1230}
1231
1233 const std::string& colmap) {
1234 auto result = SendCommand(BuildJsonCommand(
1235 "COLLISION_OVERLAY",
1236 {{"action", enable ? "enable" : "disable"}, {"colmap", colmap}}));
1237 return result.status();
1238}
1239
1240// ─────────────────────────────────────────────────────────────────────────────
1241// Save State Commands
1242// ─────────────────────────────────────────────────────────────────────────────
1243
1244absl::Status MesenSocketClient::SaveState(int slot) {
1245 auto result = SendCommand(
1246 BuildJsonCommand("SAVESTATE", {{"slot", std::to_string(slot)}}));
1247 return result.status();
1248}
1249
1250absl::Status MesenSocketClient::LoadState(int slot) {
1251 auto result = SendCommand(
1252 BuildJsonCommand("LOADSTATE", {{"slot", std::to_string(slot)}}));
1253 return result.status();
1254}
1255
1256absl::StatusOr<std::string> MesenSocketClient::Screenshot() {
1257 auto result = SendCommand(BuildJsonCommand("SCREENSHOT"));
1258 if (!result.ok())
1259 return result.status();
1260 return *result; // Base64 PNG data
1261}
1262
1263// ─────────────────────────────────────────────────────────────────────────────
1264// Event Subscription
1265// ─────────────────────────────────────────────────────────────────────────────
1266
1268 const std::vector<std::string>& events) {
1269 if (!IsConnected()) {
1270 return absl::FailedPreconditionError("Not connected to Mesen2");
1271 }
1272 if (events.empty()) {
1273 return absl::InvalidArgumentError("At least one event must be requested");
1274 }
1275
1276 // Ensure we don't leak a previous subscription socket/thread.
1277 (void)Unsubscribe();
1278
1279 std::stringstream ss;
1280 for (size_t i = 0; i < events.size(); ++i) {
1281 if (i > 0)
1282 ss << ",";
1283 ss << events[i];
1284 }
1285
1287 auto connect_status = ConnectSocketToPath(socket_path_, &event_fd);
1288 if (!connect_status.ok()) {
1289 return connect_status;
1290 }
1291
1292 const std::string subscribe_command =
1293 BuildJsonCommand("SUBSCRIBE", {{"events", ss.str()}});
1294 if (const absl::Status sent = SendAll(event_fd, subscribe_command);
1295 !sent.ok()) {
1296 close(event_fd);
1297 return sent;
1298 }
1299
1300 SetSocketReceiveTimeout(event_fd, 5000);
1301
1302 std::string response;
1303 char buffer[4096];
1304 while (true) {
1305 const ssize_t received = recv(event_fd, buffer, sizeof(buffer), 0);
1306 if (received < 0) {
1307 const int error = LastSocketError();
1308#ifdef _WIN32
1309 if (error == WSAEINTR) {
1310 continue;
1311 }
1312 if (error == WSAEWOULDBLOCK || error == WSAETIMEDOUT) {
1313#else
1314 if (error == EINTR) {
1315 continue;
1316 }
1317 if (error == EAGAIN || error == EWOULDBLOCK) {
1318#endif
1319 close(event_fd);
1320 return absl::DeadlineExceededError(
1321 "Timeout waiting for subscribe response");
1322 }
1323 close(event_fd);
1324 return absl::InternalError(absl::StrCat("Failed to receive response: ",
1325 SocketErrorMessage(error)));
1326 }
1327 if (received == 0) {
1328 break;
1329 }
1330 response.append(buffer, static_cast<size_t>(received));
1331 if (response.size() > kMaxResponseSize) {
1332 close(event_fd);
1333 return absl::ResourceExhaustedError("Mesen2 response too large");
1334 }
1335 if (response.find('\n') != std::string::npos) {
1336 break;
1337 }
1338 }
1339
1340 const size_t newline_pos = response.find('\n');
1341 if (newline_pos == std::string::npos) {
1342 close(event_fd);
1343 return absl::DeadlineExceededError("Malformed subscribe response");
1344 }
1345
1346 auto parse_status = ParseResponse(response.substr(0, newline_pos));
1347 if (!parse_status.ok()) {
1348 close(event_fd);
1349 return parse_status.status();
1350 }
1351
1352 // The handshake deadline must not outlive the handshake: the event stream
1353 // idles between emulator events. Swap it for a short poll interval so a
1354 // quiet stream never looks like a failure, while Unsubscribe() still has a
1355 // bounded wakeup to join the event thread (shutdown() alone does not
1356 // interrupt a blocking recv() on Windows).
1357 SetSocketReceiveTimeout(event_fd, EventPollMs());
1358
1359 pending_event_payload_ = response.substr(newline_pos + 1);
1360 event_socket_fd_ = event_fd;
1361 event_thread_running_ = true;
1362 event_thread_ = std::thread(&MesenSocketClient::EventLoop, this);
1363 return absl::OkStatus();
1364}
1365
1367 event_thread_running_ = false;
1368 ShutdownSocketFd(event_socket_fd_);
1369 if (event_thread_.joinable()) {
1370 event_thread_.join();
1371 }
1372
1374 close(event_socket_fd_);
1376 }
1377 pending_event_payload_.clear();
1378
1379 return absl::OkStatus();
1380}
1381
1383 std::lock_guard<std::mutex> lock(event_callback_mutex_);
1384 event_callback_ = std::move(callback);
1385}
1386
1388 if (!callback) {
1389 return 0;
1390 }
1391 std::lock_guard<std::mutex> lock(event_callback_mutex_);
1393 event_listeners_[id] = std::move(callback);
1394 return id;
1395}
1396
1398 if (id == 0) {
1399 return;
1400 }
1401 std::lock_guard<std::mutex> lock(event_callback_mutex_);
1402 event_listeners_.erase(id);
1403}
1404
1406 const SocketHandle event_fd = event_socket_fd_;
1407 if (event_fd == kInvalidSocketHandle) {
1408 return;
1409 }
1410
1411 std::string pending = pending_event_payload_;
1412 pending_event_payload_.clear();
1413 char buffer[4096];
1414
1415 auto dispatch_pending_lines = [&]() {
1416 size_t newline_pos = pending.find('\n');
1417 while (newline_pos != std::string::npos) {
1418 std::string line = pending.substr(0, newline_pos);
1419 pending.erase(0, newline_pos + 1);
1420
1421 if (!line.empty() && line.find("\"success\"") == std::string::npos) {
1422 MesenEvent event;
1423 event.raw_json = line;
1424 event.type = ExtractJsonString(line, "event");
1425 if (event.type.empty()) {
1426 event.type = ExtractJsonString(line, "type");
1427 }
1428 event.address = static_cast<uint32_t>(
1429 ExtractJsonInt(line, "address", ExtractJsonInt(line, "addr", 0)));
1430 event.frame = static_cast<uint64_t>(ExtractJsonInt(line, "frame", 0));
1431
1432 EventCallback callback;
1433 std::vector<EventCallback> listeners;
1434 {
1435 std::lock_guard<std::mutex> lock(event_callback_mutex_);
1436 callback = event_callback_;
1437 listeners.reserve(event_listeners_.size());
1438 for (const auto& [id, listener] : event_listeners_) {
1439 if (listener) {
1440 listeners.push_back(listener);
1441 }
1442 }
1443 }
1444 if (callback && !event.type.empty()) {
1445 callback(event);
1446 }
1447 if (!event.type.empty()) {
1448 for (const auto& listener : listeners) {
1449 listener(event);
1450 }
1451 }
1452 }
1453
1454 newline_pos = pending.find('\n');
1455 }
1456 };
1457
1458 while (event_thread_running_) {
1459 dispatch_pending_lines();
1460
1461 const ssize_t received = recv(event_fd, buffer, sizeof(buffer), 0);
1462 if (received < 0) {
1463 // A receive timeout means "no event yet", not "stream over"; only a
1464 // real socket error or a closed peer ends the loop.
1465#ifdef _WIN32
1466 const int err = WSAGetLastError();
1467 if (err == WSAEINTR || err == WSAEWOULDBLOCK || err == WSAETIMEDOUT) {
1468 continue;
1469 }
1470#else
1471 if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK ||
1472 errno == ETIMEDOUT) {
1473 continue;
1474 }
1475#endif
1476 break;
1477 }
1478
1479 if (received == 0) {
1480 break;
1481 }
1482
1483 pending.append(buffer, static_cast<size_t>(received));
1484 dispatch_pending_lines();
1485 }
1486}
1487
1488} // namespace mesen
1489} // namespace emu
1490} // namespace yaze
absl::Status ClearBreakpoints()
Clear all breakpoints.
absl::Status WriteWord(uint32_t addr, uint16_t value)
Write a 16-bit word to memory.
absl::StatusOr< uint8_t > ReadByte(uint32_t addr)
Read a single byte from memory.
void RemoveEventListener(EventListenerId id)
Remove a previously added event listener.
absl::Status Step(int count=1, const std::string &mode="into")
Step N CPU instructions (default 1)
void EventLoop()
Event listening thread function.
void SetEventCallback(EventCallback callback)
Set callback for received events.
absl::Status Ping()
Ping Mesen2 to check connectivity.
absl::Status Resume()
Resume emulation.
absl::StatusOr< CpuState > GetCpuState()
Get CPU register state.
absl::StatusOr< std::string > Screenshot()
Take a screenshot.
absl::StatusOr< std::string > ParseResponse(const std::string &response)
Parse JSON response for success/error.
absl::Status LoadState(int slot)
Load state from slot.
std::unordered_map< EventListenerId, EventCallback > event_listeners_
absl::StatusOr< uint16_t > ReadWord(uint32_t addr)
Read a 16-bit word from memory.
absl::StatusOr< int > AddBreakpoint(uint32_t addr, BreakpointType type, const std::string &condition="")
Add a breakpoint.
emu::input::ControllerState current_input_
static std::vector< std::string > ListAvailableSockets()
List available Mesen2 sockets on the system.
absl::Status SetButtons(const emu::input::ControllerState &state)
Set all buttons at once.
absl::StatusOr< std::string > GetTrace(int count=20)
Get execution trace log.
absl::StatusOr< GameState > GetGameState()
Get comprehensive ALTTP game state.
absl::Status SetButton(emu::input::SnesButton button, bool pressed)
Set controller button state.
absl::StatusOr< MesenState > GetState()
Get current emulation state.
absl::Status Frame()
Run exactly one frame.
absl::Status SetCollisionOverlay(bool enable, const std::string &colmap="A")
Enable/disable collision overlay.
bool IsConnected() const
Check if connected to Mesen2.
absl::StatusOr< std::string > SendCommand(const std::string &json)
Send a raw JSON command and get raw response.
EventListenerId AddEventListener(EventCallback callback)
Add an event listener without replacing existing listeners.
absl::StatusOr< std::vector< SpriteInfo > > GetSprites(bool all=false)
Get active sprites.
absl::Status RemoveBreakpoint(int id)
Remove a breakpoint by ID.
absl::Status Unsubscribe()
Unsubscribe from events.
absl::Status Subscribe(const std::vector< std::string > &events)
Subscribe to events.
absl::StatusOr< std::string > Disassemble(uint32_t addr, int count=10)
Disassemble instructions at address.
absl::Status Pause()
Pause emulation.
absl::Status Reset()
Reset the console.
absl::Status SaveState(int slot)
Save state to slot.
void Disconnect()
Disconnect from Mesen2.
absl::StatusOr< std::string > SendCommandOnSocket(SocketHandle fd, const std::string &json, bool update_connection_state)
Send a command using a specific socket descriptor.
static std::vector< std::string > FindSocketPaths()
Find available Mesen2 socket paths.
absl::Status WriteByte(uint32_t addr, uint8_t value)
Write a single byte to memory.
absl::StatusOr< std::vector< uint8_t > > ReadBlock(uint32_t addr, size_t len)
Read a block of bytes from memory.
absl::Status Connect()
Auto-discover and connect to first available Mesen2 socket.
absl::Status WriteBlock(uint32_t addr, const std::vector< uint8_t > &data)
Write a block of bytes to memory.
SnesButton
SNES controller button mapping (platform-agnostic)
absl::Status ConnectSocketToPath(const std::string &socket_path, SocketHandle *socket_fd)
std::string ExtractJsonString(const std::string &json, const std::string &key)
absl::Status ConnectTcpEndpoint(const std::string &socket_path, SocketHandle *socket_fd)
int64_t ExtractJsonInt(const std::string &json, const std::string &key, int64_t default_value=0)
absl::Status SendAll(SocketHandle fd, const std::string &data)
absl::Status ConnectUnixEndpoint(const std::string &socket_path, SocketHandle *socket_fd)
absl::Status WaitForConnectComplete(SocketHandle fd, const std::string &socket_path)
bool ExtractJsonBool(const std::string &json, const std::string &key, bool default_value=false)
double ExtractJsonDouble(const std::string &json, const std::string &key, double default_value=0.0)
absl::Status WaitForWritable(SocketHandle fd, int timeout_ms)
absl::StatusOr< std::pair< std::string, uint16_t > > ParseTcpEndpoint(const std::string &path)
std::function< void(const MesenEvent &)> EventCallback
BreakpointType
Breakpoint types.
constexpr SocketHandle kInvalidSocketHandle
Controller state (16-bit SNES controller format)
void SetButton(SnesButton button, bool pressed)
bool IsPressed(SnesButton button) const
CPU register state from Mesen2.
Complete ALTTP game state from GAMESTATE command.
Event from Mesen2 subscription.
Emulation state from Mesen2.
Sprite information from SPRITES command.