backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
#include "absl/status/status.h"
|
||||
#include "app/net/rom_version_manager.h"
|
||||
#include "app/net/websocket_client.h"
|
||||
#include "app/rom.h"
|
||||
#include "rom/rom.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
|
||||
123
src/app/net/http_client.h
Normal file
123
src/app/net/http_client.h
Normal file
@@ -0,0 +1,123 @@
|
||||
#ifndef YAZE_APP_NET_HTTP_CLIENT_H_
|
||||
#define YAZE_APP_NET_HTTP_CLIENT_H_
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @brief HTTP headers type definition
|
||||
*/
|
||||
using Headers = std::map<std::string, std::string>;
|
||||
|
||||
/**
|
||||
* @struct HttpResponse
|
||||
* @brief HTTP response structure containing status, body, and headers
|
||||
*/
|
||||
struct HttpResponse {
|
||||
int status_code = 0;
|
||||
std::string body;
|
||||
Headers headers;
|
||||
|
||||
bool IsSuccess() const {
|
||||
return status_code >= 200 && status_code < 300;
|
||||
}
|
||||
|
||||
bool IsClientError() const {
|
||||
return status_code >= 400 && status_code < 500;
|
||||
}
|
||||
|
||||
bool IsServerError() const {
|
||||
return status_code >= 500 && status_code < 600;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @class IHttpClient
|
||||
* @brief Abstract interface for HTTP client implementations
|
||||
*
|
||||
* This interface abstracts HTTP operations to support both native
|
||||
* (using cpp-httplib) and WASM (using emscripten fetch) implementations.
|
||||
* All methods return absl::Status or absl::StatusOr for consistent error handling.
|
||||
*/
|
||||
class IHttpClient {
|
||||
public:
|
||||
virtual ~IHttpClient() = default;
|
||||
|
||||
/**
|
||||
* @brief Perform an HTTP GET request
|
||||
* @param url The URL to request
|
||||
* @param headers Optional HTTP headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
virtual absl::StatusOr<HttpResponse> Get(
|
||||
const std::string& url,
|
||||
const Headers& headers = {}) = 0;
|
||||
|
||||
/**
|
||||
* @brief Perform an HTTP POST request
|
||||
* @param url The URL to post to
|
||||
* @param body The request body
|
||||
* @param headers Optional HTTP headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
virtual absl::StatusOr<HttpResponse> Post(
|
||||
const std::string& url,
|
||||
const std::string& body,
|
||||
const Headers& headers = {}) = 0;
|
||||
|
||||
/**
|
||||
* @brief Perform an HTTP PUT request
|
||||
* @param url The URL to put to
|
||||
* @param body The request body
|
||||
* @param headers Optional HTTP headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
virtual absl::StatusOr<HttpResponse> Put(
|
||||
const std::string& url,
|
||||
const std::string& body,
|
||||
const Headers& headers = {}) {
|
||||
// Default implementation returns not implemented
|
||||
return absl::UnimplementedError("PUT method not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Perform an HTTP DELETE request
|
||||
* @param url The URL to delete
|
||||
* @param headers Optional HTTP headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
virtual absl::StatusOr<HttpResponse> Delete(
|
||||
const std::string& url,
|
||||
const Headers& headers = {}) {
|
||||
// Default implementation returns not implemented
|
||||
return absl::UnimplementedError("DELETE method not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set a timeout for HTTP requests
|
||||
* @param timeout_seconds Timeout in seconds
|
||||
*/
|
||||
virtual void SetTimeout(int timeout_seconds) {
|
||||
timeout_seconds_ = timeout_seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the current timeout setting
|
||||
* @return Timeout in seconds
|
||||
*/
|
||||
int GetTimeout() const { return timeout_seconds_; }
|
||||
|
||||
protected:
|
||||
int timeout_seconds_ = 30; // Default 30 second timeout
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_NET_HTTP_CLIENT_H_
|
||||
267
src/app/net/native/httplib_client.cc
Normal file
267
src/app/net/native/httplib_client.cc
Normal file
@@ -0,0 +1,267 @@
|
||||
#include "app/net/native/httplib_client.h"
|
||||
|
||||
#include <regex>
|
||||
|
||||
#include "util/macro.h" // For RETURN_IF_ERROR and ASSIGN_OR_RETURN
|
||||
|
||||
// Include httplib with appropriate settings
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#endif
|
||||
#include "httplib.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace net {
|
||||
|
||||
HttpLibClient::HttpLibClient() {
|
||||
// Constructor
|
||||
}
|
||||
|
||||
HttpLibClient::~HttpLibClient() {
|
||||
// Cleanup cached clients
|
||||
client_cache_.clear();
|
||||
}
|
||||
|
||||
absl::Status HttpLibClient::ParseUrl(const std::string& url,
|
||||
std::string& scheme,
|
||||
std::string& host,
|
||||
int& port,
|
||||
std::string& path) const {
|
||||
// Basic URL regex pattern
|
||||
std::regex url_regex(R"(^(https?):\/\/([^:\/\s]+)(?::(\d+))?(\/.*)?$)");
|
||||
std::smatch matches;
|
||||
|
||||
if (!std::regex_match(url, matches, url_regex)) {
|
||||
return absl::InvalidArgumentError("Invalid URL format: " + url);
|
||||
}
|
||||
|
||||
scheme = matches[1].str();
|
||||
host = matches[2].str();
|
||||
|
||||
// Parse port or use defaults
|
||||
if (matches[3].matched) {
|
||||
port = std::stoi(matches[3].str());
|
||||
} else {
|
||||
port = (scheme == "https") ? 443 : 80;
|
||||
}
|
||||
|
||||
// Parse path (default to "/" if empty)
|
||||
path = matches[4].matched ? matches[4].str() : "/";
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<std::shared_ptr<httplib::Client>> HttpLibClient::GetOrCreateClient(
|
||||
const std::string& scheme,
|
||||
const std::string& host,
|
||||
int port) {
|
||||
|
||||
// Create cache key
|
||||
std::string cache_key = scheme + "://" + host + ":" + std::to_string(port);
|
||||
|
||||
// Check if client exists in cache
|
||||
auto it = client_cache_.find(cache_key);
|
||||
if (it != client_cache_.end() && it->second) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Create new client
|
||||
std::shared_ptr<httplib::Client> client;
|
||||
|
||||
if (scheme == "https") {
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
client = std::make_shared<httplib::Client>(host, port);
|
||||
client->enable_server_certificate_verification(false); // For development
|
||||
#else
|
||||
return absl::UnimplementedError(
|
||||
"HTTPS not supported: OpenSSL support not compiled in");
|
||||
#endif
|
||||
} else if (scheme == "http") {
|
||||
client = std::make_shared<httplib::Client>(host, port);
|
||||
} else {
|
||||
return absl::InvalidArgumentError("Unsupported URL scheme: " + scheme);
|
||||
}
|
||||
|
||||
if (!client) {
|
||||
return absl::InternalError("Failed to create HTTP client");
|
||||
}
|
||||
|
||||
// Set timeout from base class
|
||||
client->set_connection_timeout(timeout_seconds_);
|
||||
client->set_read_timeout(timeout_seconds_);
|
||||
client->set_write_timeout(timeout_seconds_);
|
||||
|
||||
// Cache the client
|
||||
client_cache_[cache_key] = client;
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
Headers HttpLibClient::ConvertHeaders(const void* httplib_headers) const {
|
||||
Headers result;
|
||||
|
||||
if (httplib_headers) {
|
||||
const auto& headers = *static_cast<const httplib::Headers*>(httplib_headers);
|
||||
for (const auto& header : headers) {
|
||||
result[header.first] = header.second;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<HttpResponse> HttpLibClient::Get(
|
||||
const std::string& url,
|
||||
const Headers& headers) {
|
||||
|
||||
std::string scheme, host, path;
|
||||
int port;
|
||||
|
||||
RETURN_IF_ERROR(ParseUrl(url, scheme, host, port, path));
|
||||
|
||||
auto client_or = GetOrCreateClient(scheme, host, port);
|
||||
ASSIGN_OR_RETURN(auto client, client_or);
|
||||
|
||||
// Convert headers to httplib format
|
||||
httplib::Headers httplib_headers;
|
||||
for (const auto& [key, value] : headers) {
|
||||
httplib_headers.emplace(key, value);
|
||||
}
|
||||
|
||||
// Perform GET request
|
||||
auto res = client->Get(path.c_str(), httplib_headers);
|
||||
|
||||
if (!res) {
|
||||
return absl::UnavailableError("HTTP GET request failed: " + url);
|
||||
}
|
||||
|
||||
HttpResponse response;
|
||||
response.status_code = res->status;
|
||||
response.body = res->body;
|
||||
response.headers = ConvertHeaders(&res->headers);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
absl::StatusOr<HttpResponse> HttpLibClient::Post(
|
||||
const std::string& url,
|
||||
const std::string& body,
|
||||
const Headers& headers) {
|
||||
|
||||
std::string scheme, host, path;
|
||||
int port;
|
||||
|
||||
RETURN_IF_ERROR(ParseUrl(url, scheme, host, port, path));
|
||||
|
||||
auto client_or = GetOrCreateClient(scheme, host, port);
|
||||
ASSIGN_OR_RETURN(auto client, client_or);
|
||||
|
||||
// Convert headers to httplib format
|
||||
httplib::Headers httplib_headers;
|
||||
for (const auto& [key, value] : headers) {
|
||||
httplib_headers.emplace(key, value);
|
||||
}
|
||||
|
||||
// Set Content-Type if not provided
|
||||
if (httplib_headers.find("Content-Type") == httplib_headers.end()) {
|
||||
httplib_headers.emplace("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
// Perform POST request
|
||||
auto res = client->Post(path.c_str(), httplib_headers, body,
|
||||
httplib_headers.find("Content-Type")->second);
|
||||
|
||||
if (!res) {
|
||||
return absl::UnavailableError("HTTP POST request failed: " + url);
|
||||
}
|
||||
|
||||
HttpResponse response;
|
||||
response.status_code = res->status;
|
||||
response.body = res->body;
|
||||
response.headers = ConvertHeaders(&res->headers);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
absl::StatusOr<HttpResponse> HttpLibClient::Put(
|
||||
const std::string& url,
|
||||
const std::string& body,
|
||||
const Headers& headers) {
|
||||
|
||||
std::string scheme, host, path;
|
||||
int port;
|
||||
|
||||
RETURN_IF_ERROR(ParseUrl(url, scheme, host, port, path));
|
||||
|
||||
auto client_or = GetOrCreateClient(scheme, host, port);
|
||||
ASSIGN_OR_RETURN(auto client, client_or);
|
||||
|
||||
// Convert headers to httplib format
|
||||
httplib::Headers httplib_headers;
|
||||
for (const auto& [key, value] : headers) {
|
||||
httplib_headers.emplace(key, value);
|
||||
}
|
||||
|
||||
// Set Content-Type if not provided
|
||||
if (httplib_headers.find("Content-Type") == httplib_headers.end()) {
|
||||
httplib_headers.emplace("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
// Perform PUT request
|
||||
auto res = client->Put(path.c_str(), httplib_headers, body,
|
||||
httplib_headers.find("Content-Type")->second);
|
||||
|
||||
if (!res) {
|
||||
return absl::UnavailableError("HTTP PUT request failed: " + url);
|
||||
}
|
||||
|
||||
HttpResponse response;
|
||||
response.status_code = res->status;
|
||||
response.body = res->body;
|
||||
response.headers = ConvertHeaders(&res->headers);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
absl::StatusOr<HttpResponse> HttpLibClient::Delete(
|
||||
const std::string& url,
|
||||
const Headers& headers) {
|
||||
|
||||
std::string scheme, host, path;
|
||||
int port;
|
||||
|
||||
RETURN_IF_ERROR(ParseUrl(url, scheme, host, port, path));
|
||||
|
||||
auto client_or = GetOrCreateClient(scheme, host, port);
|
||||
ASSIGN_OR_RETURN(auto client, client_or);
|
||||
|
||||
// Convert headers to httplib format
|
||||
httplib::Headers httplib_headers;
|
||||
for (const auto& [key, value] : headers) {
|
||||
httplib_headers.emplace(key, value);
|
||||
}
|
||||
|
||||
// Perform DELETE request
|
||||
auto res = client->Delete(path.c_str(), httplib_headers);
|
||||
|
||||
if (!res) {
|
||||
return absl::UnavailableError("HTTP DELETE request failed: " + url);
|
||||
}
|
||||
|
||||
HttpResponse response;
|
||||
response.status_code = res->status;
|
||||
response.body = res->body;
|
||||
response.headers = ConvertHeaders(&res->headers);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
void HttpLibClient::SetTimeout(int timeout_seconds) {
|
||||
IHttpClient::SetTimeout(timeout_seconds);
|
||||
|
||||
// Clear client cache to force recreation with new timeout
|
||||
client_cache_.clear();
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace yaze
|
||||
121
src/app/net/native/httplib_client.h
Normal file
121
src/app/net/native/httplib_client.h
Normal file
@@ -0,0 +1,121 @@
|
||||
#ifndef YAZE_APP_NET_NATIVE_HTTPLIB_CLIENT_H_
|
||||
#define YAZE_APP_NET_NATIVE_HTTPLIB_CLIENT_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "app/net/http_client.h"
|
||||
|
||||
// Forward declaration to avoid including httplib.h in header
|
||||
namespace httplib {
|
||||
class Client;
|
||||
}
|
||||
|
||||
namespace yaze {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @class HttpLibClient
|
||||
* @brief Native HTTP client implementation using cpp-httplib
|
||||
*
|
||||
* This implementation wraps the cpp-httplib library for native builds,
|
||||
* providing HTTP/HTTPS support with optional SSL/TLS via OpenSSL.
|
||||
*/
|
||||
class HttpLibClient : public IHttpClient {
|
||||
public:
|
||||
HttpLibClient();
|
||||
~HttpLibClient() override;
|
||||
|
||||
/**
|
||||
* @brief Perform an HTTP GET request
|
||||
* @param url The URL to request
|
||||
* @param headers Optional HTTP headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
absl::StatusOr<HttpResponse> Get(
|
||||
const std::string& url,
|
||||
const Headers& headers = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Perform an HTTP POST request
|
||||
* @param url The URL to post to
|
||||
* @param body The request body
|
||||
* @param headers Optional HTTP headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
absl::StatusOr<HttpResponse> Post(
|
||||
const std::string& url,
|
||||
const std::string& body,
|
||||
const Headers& headers = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Perform an HTTP PUT request
|
||||
* @param url The URL to put to
|
||||
* @param body The request body
|
||||
* @param headers Optional HTTP headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
absl::StatusOr<HttpResponse> Put(
|
||||
const std::string& url,
|
||||
const std::string& body,
|
||||
const Headers& headers = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Perform an HTTP DELETE request
|
||||
* @param url The URL to delete
|
||||
* @param headers Optional HTTP headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
absl::StatusOr<HttpResponse> Delete(
|
||||
const std::string& url,
|
||||
const Headers& headers = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Set a timeout for HTTP requests
|
||||
* @param timeout_seconds Timeout in seconds
|
||||
*/
|
||||
void SetTimeout(int timeout_seconds) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Parse URL into components
|
||||
* @param url The URL to parse
|
||||
* @param scheme Output: URL scheme (http/https)
|
||||
* @param host Output: Host name
|
||||
* @param port Output: Port number
|
||||
* @param path Output: Path component
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status ParseUrl(const std::string& url,
|
||||
std::string& scheme,
|
||||
std::string& host,
|
||||
int& port,
|
||||
std::string& path) const;
|
||||
|
||||
/**
|
||||
* @brief Create or get cached httplib client for a host
|
||||
* @param scheme URL scheme (http/https)
|
||||
* @param host Host name
|
||||
* @param port Port number
|
||||
* @return httplib::Client pointer or error
|
||||
*/
|
||||
absl::StatusOr<std::shared_ptr<httplib::Client>> GetOrCreateClient(
|
||||
const std::string& scheme,
|
||||
const std::string& host,
|
||||
int port);
|
||||
|
||||
/**
|
||||
* @brief Convert httplib headers to our Headers type
|
||||
* @param httplib_headers httplib header structure
|
||||
* @return Headers map
|
||||
*/
|
||||
Headers ConvertHeaders(const void* httplib_headers) const;
|
||||
|
||||
// Cache clients per host to avoid reconnection overhead
|
||||
std::map<std::string, std::shared_ptr<httplib::Client>> client_cache_;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_NET_NATIVE_HTTPLIB_CLIENT_H_
|
||||
273
src/app/net/native/httplib_websocket.cc
Normal file
273
src/app/net/native/httplib_websocket.cc
Normal file
@@ -0,0 +1,273 @@
|
||||
#include "app/net/native/httplib_websocket.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <regex>
|
||||
|
||||
#include "util/macro.h" // For RETURN_IF_ERROR
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#endif
|
||||
#include "httplib.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace net {
|
||||
|
||||
HttpLibWebSocket::HttpLibWebSocket() : stop_receive_(false) {
|
||||
state_ = WebSocketState::kDisconnected;
|
||||
}
|
||||
|
||||
HttpLibWebSocket::~HttpLibWebSocket() {
|
||||
if (state_ != WebSocketState::kDisconnected) {
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status HttpLibWebSocket::ParseWebSocketUrl(const std::string& ws_url,
|
||||
std::string& http_url) {
|
||||
// Convert ws:// to http:// and wss:// to https://
|
||||
std::regex ws_regex(R"(^(wss?)://(.+)$)");
|
||||
std::smatch matches;
|
||||
|
||||
if (!std::regex_match(ws_url, matches, ws_regex)) {
|
||||
return absl::InvalidArgumentError("Invalid WebSocket URL: " + ws_url);
|
||||
}
|
||||
|
||||
std::string scheme = matches[1].str();
|
||||
std::string rest = matches[2].str();
|
||||
|
||||
if (scheme == "ws") {
|
||||
http_url = "http://" + rest;
|
||||
} else if (scheme == "wss") {
|
||||
http_url = "https://" + rest;
|
||||
} else {
|
||||
return absl::InvalidArgumentError("Invalid WebSocket scheme: " + scheme);
|
||||
}
|
||||
|
||||
url_ = ws_url;
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status HttpLibWebSocket::Connect(const std::string& url) {
|
||||
if (state_ != WebSocketState::kDisconnected) {
|
||||
return absl::FailedPreconditionError(
|
||||
"WebSocket already connected or connecting");
|
||||
}
|
||||
|
||||
state_ = WebSocketState::kConnecting;
|
||||
|
||||
// Convert WebSocket URL to HTTP URL
|
||||
RETURN_IF_ERROR(ParseWebSocketUrl(url, http_endpoint_));
|
||||
|
||||
// Parse HTTP URL to extract host and port
|
||||
std::regex url_regex(R"(^(https?)://([^:/\s]+)(?::(\d+))?(/.*)?)$)");
|
||||
std::smatch matches;
|
||||
|
||||
if (!std::regex_match(http_endpoint_, matches, url_regex)) {
|
||||
state_ = WebSocketState::kError;
|
||||
return absl::InvalidArgumentError("Invalid HTTP URL: " + http_endpoint_);
|
||||
}
|
||||
|
||||
std::string scheme = matches[1].str();
|
||||
std::string host = matches[2].str();
|
||||
int port = matches[3].matched ? std::stoi(matches[3].str())
|
||||
: (scheme == "https" ? 443 : 80);
|
||||
|
||||
// Create HTTP client
|
||||
if (scheme == "https") {
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
client_ = std::make_shared<httplib::Client>(host, port);
|
||||
client_->enable_server_certificate_verification(false); // For development
|
||||
#else
|
||||
state_ = WebSocketState::kError;
|
||||
return absl::UnimplementedError(
|
||||
"WSS not supported: OpenSSL support not compiled in");
|
||||
#endif
|
||||
} else {
|
||||
client_ = std::make_shared<httplib::Client>(host, port);
|
||||
}
|
||||
|
||||
if (!client_) {
|
||||
state_ = WebSocketState::kError;
|
||||
return absl::InternalError("Failed to create HTTP client");
|
||||
}
|
||||
|
||||
// Set reasonable timeouts
|
||||
client_->set_connection_timeout(10);
|
||||
client_->set_read_timeout(30);
|
||||
|
||||
// Note: This is a simplified implementation. A real WebSocket implementation
|
||||
// would perform the WebSocket handshake here. For now, we'll use HTTP
|
||||
// long-polling as a fallback.
|
||||
|
||||
// Generate session ID for this connection
|
||||
session_id_ = "session_" + std::to_string(
|
||||
std::chrono::system_clock::now().time_since_epoch().count());
|
||||
|
||||
state_ = WebSocketState::kConnected;
|
||||
|
||||
// Call open callback if set
|
||||
if (open_callback_) {
|
||||
open_callback_();
|
||||
}
|
||||
|
||||
// Start receive loop in background thread
|
||||
stop_receive_ = false;
|
||||
receive_thread_ = std::thread([this]() { ReceiveLoop(); });
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status HttpLibWebSocket::Send(const std::string& message) {
|
||||
if (state_ != WebSocketState::kConnected) {
|
||||
return absl::FailedPreconditionError("WebSocket not connected");
|
||||
}
|
||||
|
||||
if (!client_) {
|
||||
return absl::InternalError("HTTP client not initialized");
|
||||
}
|
||||
|
||||
// Note: This is a simplified implementation using HTTP POST
|
||||
// A real WebSocket would send frames over the persistent connection
|
||||
|
||||
httplib::Headers headers = {
|
||||
{"Content-Type", "text/plain"},
|
||||
{"X-Session-Id", session_id_}
|
||||
};
|
||||
|
||||
auto res = client_->Post("/send", headers, message, "text/plain");
|
||||
|
||||
if (!res) {
|
||||
return absl::UnavailableError("Failed to send message");
|
||||
}
|
||||
|
||||
if (res->status != 200) {
|
||||
return absl::InternalError("Server returned status " +
|
||||
std::to_string(res->status));
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status HttpLibWebSocket::SendBinary(const uint8_t* data, size_t length) {
|
||||
if (state_ != WebSocketState::kConnected) {
|
||||
return absl::FailedPreconditionError("WebSocket not connected");
|
||||
}
|
||||
|
||||
if (!client_) {
|
||||
return absl::InternalError("HTTP client not initialized");
|
||||
}
|
||||
|
||||
// Convert binary data to string for HTTP transport
|
||||
std::string body(reinterpret_cast<const char*>(data), length);
|
||||
|
||||
httplib::Headers headers = {
|
||||
{"Content-Type", "application/octet-stream"},
|
||||
{"X-Session-Id", session_id_}
|
||||
};
|
||||
|
||||
auto res = client_->Post("/send-binary", headers, body,
|
||||
"application/octet-stream");
|
||||
|
||||
if (!res) {
|
||||
return absl::UnavailableError("Failed to send binary data");
|
||||
}
|
||||
|
||||
if (res->status != 200) {
|
||||
return absl::InternalError("Server returned status " +
|
||||
std::to_string(res->status));
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status HttpLibWebSocket::Close(int code, const std::string& reason) {
|
||||
if (state_ == WebSocketState::kDisconnected ||
|
||||
state_ == WebSocketState::kClosed) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
state_ = WebSocketState::kClosing;
|
||||
|
||||
// Stop receive loop
|
||||
StopReceiveLoop();
|
||||
|
||||
if (client_) {
|
||||
// Send close notification to server
|
||||
httplib::Headers headers = {
|
||||
{"X-Session-Id", session_id_},
|
||||
{"X-Close-Code", std::to_string(code)},
|
||||
{"X-Close-Reason", reason}
|
||||
};
|
||||
|
||||
client_->Post("/close", headers, "", "text/plain");
|
||||
client_.reset();
|
||||
}
|
||||
|
||||
state_ = WebSocketState::kClosed;
|
||||
|
||||
// Call close callback if set
|
||||
if (close_callback_) {
|
||||
close_callback_(code, reason);
|
||||
}
|
||||
|
||||
state_ = WebSocketState::kDisconnected;
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void HttpLibWebSocket::ReceiveLoop() {
|
||||
while (!stop_receive_ && state_ == WebSocketState::kConnected) {
|
||||
if (!client_) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Long-polling: make a request that blocks until there's a message
|
||||
httplib::Headers headers = {
|
||||
{"X-Session-Id", session_id_}
|
||||
};
|
||||
|
||||
auto res = client_->Get("/poll", headers);
|
||||
|
||||
if (stop_receive_) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
// Connection error
|
||||
if (error_callback_) {
|
||||
error_callback_("Connection lost");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (res->status == 200 && !res->body.empty()) {
|
||||
// Received a message
|
||||
if (message_callback_) {
|
||||
message_callback_(res->body);
|
||||
}
|
||||
} else if (res->status == 204) {
|
||||
// No content - continue polling
|
||||
continue;
|
||||
} else if (res->status >= 400) {
|
||||
// Error from server
|
||||
if (error_callback_) {
|
||||
error_callback_("Server error: " + std::to_string(res->status));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Small delay to prevent tight loop
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}
|
||||
|
||||
void HttpLibWebSocket::StopReceiveLoop() {
|
||||
stop_receive_ = true;
|
||||
if (receive_thread_.joinable()) {
|
||||
receive_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace yaze
|
||||
144
src/app/net/native/httplib_websocket.h
Normal file
144
src/app/net/native/httplib_websocket.h
Normal file
@@ -0,0 +1,144 @@
|
||||
#ifndef YAZE_APP_NET_NATIVE_HTTPLIB_WEBSOCKET_H_
|
||||
#define YAZE_APP_NET_NATIVE_HTTPLIB_WEBSOCKET_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
#include "app/net/websocket_interface.h"
|
||||
|
||||
// Forward declaration
|
||||
namespace httplib {
|
||||
class Client;
|
||||
}
|
||||
|
||||
namespace yaze {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @class HttpLibWebSocket
|
||||
* @brief Native WebSocket implementation using HTTP fallback
|
||||
*
|
||||
* Note: cpp-httplib doesn't have full WebSocket support, so this
|
||||
* implementation uses HTTP long-polling as a fallback. For production
|
||||
* use, consider integrating a proper WebSocket library like websocketpp
|
||||
* or libwebsockets.
|
||||
*/
|
||||
class HttpLibWebSocket : public IWebSocket {
|
||||
public:
|
||||
HttpLibWebSocket();
|
||||
~HttpLibWebSocket() override;
|
||||
|
||||
/**
|
||||
* @brief Connect to a WebSocket server
|
||||
* @param url The WebSocket URL (ws:// or wss://)
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status Connect(const std::string& url) override;
|
||||
|
||||
/**
|
||||
* @brief Send a text message
|
||||
* @param message The text message to send
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status Send(const std::string& message) override;
|
||||
|
||||
/**
|
||||
* @brief Send a binary message
|
||||
* @param data The binary data to send
|
||||
* @param length The length of the data
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status SendBinary(const uint8_t* data, size_t length) override;
|
||||
|
||||
/**
|
||||
* @brief Close the WebSocket connection
|
||||
* @param code Optional close code
|
||||
* @param reason Optional close reason
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status Close(int code = 1000,
|
||||
const std::string& reason = "") override;
|
||||
|
||||
/**
|
||||
* @brief Get the current connection state
|
||||
* @return Current WebSocket state
|
||||
*/
|
||||
WebSocketState GetState() const override { return state_; }
|
||||
|
||||
/**
|
||||
* @brief Set callback for text message events
|
||||
* @param callback Function to call when a text message is received
|
||||
*/
|
||||
void OnMessage(MessageCallback callback) override {
|
||||
message_callback_ = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set callback for binary message events
|
||||
* @param callback Function to call when binary data is received
|
||||
*/
|
||||
void OnBinaryMessage(BinaryMessageCallback callback) override {
|
||||
binary_message_callback_ = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set callback for connection open events
|
||||
* @param callback Function to call when connection is established
|
||||
*/
|
||||
void OnOpen(OpenCallback callback) override {
|
||||
open_callback_ = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set callback for connection close events
|
||||
* @param callback Function to call when connection is closed
|
||||
*/
|
||||
void OnClose(CloseCallback callback) override {
|
||||
close_callback_ = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set callback for error events
|
||||
* @param callback Function to call when an error occurs
|
||||
*/
|
||||
void OnError(ErrorCallback callback) override {
|
||||
error_callback_ = callback;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Parse WebSocket URL into HTTP components
|
||||
* @param ws_url WebSocket URL (ws:// or wss://)
|
||||
* @param http_url Output: Converted HTTP URL
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status ParseWebSocketUrl(const std::string& ws_url,
|
||||
std::string& http_url);
|
||||
|
||||
/**
|
||||
* @brief Background thread for receiving messages (polling)
|
||||
*/
|
||||
void ReceiveLoop();
|
||||
|
||||
/**
|
||||
* @brief Stop the receive loop
|
||||
*/
|
||||
void StopReceiveLoop();
|
||||
|
||||
// HTTP client for fallback implementation
|
||||
std::shared_ptr<httplib::Client> client_;
|
||||
|
||||
// Background receive thread
|
||||
std::thread receive_thread_;
|
||||
std::atomic<bool> stop_receive_;
|
||||
|
||||
// Connection details
|
||||
std::string session_id_;
|
||||
std::string http_endpoint_;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_NET_NATIVE_HTTPLIB_WEBSOCKET_H_
|
||||
@@ -2,6 +2,8 @@
|
||||
# Yaze Net Library
|
||||
# ==============================================================================
|
||||
# This library contains networking and collaboration functionality:
|
||||
# - Network abstraction layer (HTTP/WebSocket interfaces)
|
||||
# - Platform-specific implementations (native/WASM)
|
||||
# - ROM version management
|
||||
# - Proposal approval system
|
||||
# - Collaboration utilities
|
||||
@@ -9,13 +11,37 @@
|
||||
# Dependencies: yaze_util, absl
|
||||
# ==============================================================================
|
||||
|
||||
# Base network sources (always included)
|
||||
set(
|
||||
YAZE_NET_SRC
|
||||
YAZE_NET_BASE_SRC
|
||||
app/net/rom_version_manager.cc
|
||||
app/net/websocket_client.cc
|
||||
app/net/collaboration_service.cc
|
||||
app/net/network_factory.cc
|
||||
)
|
||||
|
||||
# Platform-specific network implementation
|
||||
if(EMSCRIPTEN)
|
||||
# WASM/Emscripten implementation
|
||||
set(
|
||||
YAZE_NET_PLATFORM_SRC
|
||||
app/net/wasm/emscripten_http_client.cc
|
||||
app/net/wasm/emscripten_websocket.cc
|
||||
)
|
||||
message(STATUS " - Using Emscripten network implementation (Fetch API & WebSocket)")
|
||||
else()
|
||||
# Native implementation
|
||||
set(
|
||||
YAZE_NET_PLATFORM_SRC
|
||||
app/net/native/httplib_client.cc
|
||||
app/net/native/httplib_websocket.cc
|
||||
)
|
||||
message(STATUS " - Using native network implementation (cpp-httplib)")
|
||||
endif()
|
||||
|
||||
# Combine all sources
|
||||
set(YAZE_NET_SRC ${YAZE_NET_BASE_SRC} ${YAZE_NET_PLATFORM_SRC})
|
||||
|
||||
if(YAZE_WITH_GRPC)
|
||||
# Add ROM service implementation (disabled - proto field mismatch)
|
||||
# list(APPEND YAZE_NET_SRC app/net/rom_service_impl.cc)
|
||||
@@ -43,6 +69,20 @@ target_link_libraries(yaze_net PUBLIC
|
||||
${YAZE_SDL2_TARGETS}
|
||||
)
|
||||
|
||||
# Add Emscripten-specific flags for WASM builds
|
||||
if(EMSCRIPTEN)
|
||||
# Enable Fetch API for HTTP requests
|
||||
target_compile_options(yaze_net PUBLIC "-sFETCH=1")
|
||||
target_link_options(yaze_net PUBLIC "-sFETCH=1")
|
||||
|
||||
# WebSocket support requires linking websocket.js library
|
||||
# The <emscripten/websocket.h> header provides the API, but the
|
||||
# implementation is in the websocket.js library
|
||||
target_link_libraries(yaze_net PUBLIC websocket.js)
|
||||
|
||||
message(STATUS " - Emscripten Fetch API and WebSocket enabled")
|
||||
endif()
|
||||
|
||||
# Add JSON and httplib support if enabled
|
||||
if(YAZE_WITH_JSON)
|
||||
# Link nlohmann_json which provides the include directories automatically
|
||||
@@ -90,12 +130,6 @@ if(YAZE_WITH_JSON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Add gRPC support for ROM service
|
||||
if(YAZE_WITH_GRPC)
|
||||
target_link_libraries(yaze_net PUBLIC yaze_grpc_support)
|
||||
message(STATUS " - gRPC ROM service enabled")
|
||||
endif()
|
||||
|
||||
set_target_properties(yaze_net PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
|
||||
|
||||
53
src/app/net/network_factory.cc
Normal file
53
src/app/net/network_factory.cc
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "app/net/network_factory.h"
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include "app/net/wasm/emscripten_http_client.h"
|
||||
#include "app/net/wasm/emscripten_websocket.h"
|
||||
#else
|
||||
#include "app/net/native/httplib_client.h"
|
||||
#include "app/net/native/httplib_websocket.h"
|
||||
#endif
|
||||
|
||||
namespace yaze {
|
||||
namespace net {
|
||||
|
||||
std::unique_ptr<IHttpClient> CreateHttpClient() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
return std::make_unique<EmscriptenHttpClient>();
|
||||
#else
|
||||
return std::make_unique<HttpLibClient>();
|
||||
#endif
|
||||
}
|
||||
|
||||
std::unique_ptr<IWebSocket> CreateWebSocket() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
return std::make_unique<EmscriptenWebSocket>();
|
||||
#else
|
||||
return std::make_unique<HttpLibWebSocket>();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool IsSSLSupported() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// WASM in browser always supports SSL/TLS through browser APIs
|
||||
return true;
|
||||
#else
|
||||
// Native builds depend on OpenSSL availability
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string GetNetworkPlatform() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
return "wasm";
|
||||
#else
|
||||
return "native";
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace yaze
|
||||
56
src/app/net/network_factory.h
Normal file
56
src/app/net/network_factory.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#ifndef YAZE_APP_NET_NETWORK_FACTORY_H_
|
||||
#define YAZE_APP_NET_NETWORK_FACTORY_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "app/net/http_client.h"
|
||||
#include "app/net/websocket_interface.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @brief Factory functions for creating network clients
|
||||
*
|
||||
* These functions return the appropriate implementation based on the
|
||||
* build platform (native or WASM). This allows the rest of the codebase
|
||||
* to use networking features without worrying about platform differences.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Create an HTTP client for the current platform
|
||||
* @return Unique pointer to IHttpClient implementation
|
||||
*
|
||||
* Returns:
|
||||
* - HttpLibClient for native builds
|
||||
* - EmscriptenHttpClient for WASM builds
|
||||
*/
|
||||
std::unique_ptr<IHttpClient> CreateHttpClient();
|
||||
|
||||
/**
|
||||
* @brief Create a WebSocket client for the current platform
|
||||
* @return Unique pointer to IWebSocket implementation
|
||||
*
|
||||
* Returns:
|
||||
* - HttpLibWebSocket (or native WebSocket) for native builds
|
||||
* - EmscriptenWebSocket for WASM builds
|
||||
*/
|
||||
std::unique_ptr<IWebSocket> CreateWebSocket();
|
||||
|
||||
/**
|
||||
* @brief Check if the current platform supports SSL/TLS
|
||||
* @return true if SSL/TLS is available, false otherwise
|
||||
*/
|
||||
bool IsSSLSupported();
|
||||
|
||||
/**
|
||||
* @brief Get the platform name for debugging
|
||||
* @return Platform string ("native", "wasm", etc.)
|
||||
*/
|
||||
std::string GetNetworkPlatform();
|
||||
|
||||
} // namespace net
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_NET_NETWORK_FACTORY_H_
|
||||
@@ -1,186 +0,0 @@
|
||||
#include "app/net/rom_service_impl.h"
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "app/net/rom_version_manager.h"
|
||||
#include "app/rom.h"
|
||||
|
||||
// Proto namespace alias for convenience
|
||||
namespace rom_svc = ::yaze::proto;
|
||||
|
||||
namespace yaze {
|
||||
|
||||
namespace net {
|
||||
|
||||
RomServiceImpl::RomServiceImpl(Rom* rom, RomVersionManager* version_manager,
|
||||
ProposalApprovalManager* approval_manager)
|
||||
: rom_(rom),
|
||||
version_mgr_(version_manager),
|
||||
approval_mgr_(approval_manager) {}
|
||||
|
||||
void RomServiceImpl::SetConfig(const Config& config) {
|
||||
config_ = config;
|
||||
}
|
||||
|
||||
grpc::Status RomServiceImpl::ReadBytes(grpc::ServerContext* context,
|
||||
const rom_svc::ReadBytesRequest* request,
|
||||
rom_svc::ReadBytesResponse* response) {
|
||||
if (!rom_ || !rom_->is_loaded()) {
|
||||
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
|
||||
"ROM not loaded");
|
||||
}
|
||||
|
||||
uint32_t address = request->address();
|
||||
uint32_t length = request->length();
|
||||
|
||||
// Validate range
|
||||
if (address + length > rom_->size()) {
|
||||
return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
|
||||
absl::StrFormat("Read beyond ROM: 0x%X+%d > %d",
|
||||
address, length, rom_->size()));
|
||||
}
|
||||
|
||||
// Read data
|
||||
const auto* data = rom_->data() + address;
|
||||
response->set_data(data, length);
|
||||
response->set_success(true);
|
||||
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status RomServiceImpl::WriteBytes(
|
||||
grpc::ServerContext* context, const rom_svc::WriteBytesRequest* request,
|
||||
rom_svc::WriteBytesResponse* response) {
|
||||
if (!rom_ || !rom_->is_loaded()) {
|
||||
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
|
||||
"ROM not loaded");
|
||||
}
|
||||
|
||||
uint32_t address = request->address();
|
||||
const std::string& data = request->data();
|
||||
|
||||
// Validate range
|
||||
if (address + data.size() > rom_->size()) {
|
||||
return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
|
||||
absl::StrFormat("Write beyond ROM: 0x%X+%zu > %d",
|
||||
address, data.size(), rom_->size()));
|
||||
}
|
||||
|
||||
// Check if approval required
|
||||
if (config_.require_approval_for_writes && approval_mgr_) {
|
||||
// Create a proposal for this write
|
||||
std::string proposal_id =
|
||||
absl::StrFormat("write_0x%X_%zu_bytes", address, data.size());
|
||||
|
||||
if (request->has_proposal_id()) {
|
||||
proposal_id = request->proposal_id();
|
||||
}
|
||||
|
||||
// Check if proposal is approved
|
||||
auto status = approval_mgr_->GetProposalStatus(proposal_id);
|
||||
if (status != ProposalApprovalManager::ApprovalStatus::kApproved) {
|
||||
response->set_success(false);
|
||||
response->set_message("Write requires approval");
|
||||
response->set_proposal_id(proposal_id);
|
||||
return grpc::Status::OK; // Not an error, just needs approval
|
||||
}
|
||||
}
|
||||
|
||||
// Create snapshot before write
|
||||
if (version_mgr_) {
|
||||
std::string snapshot_desc = absl::StrFormat(
|
||||
"Before write to 0x%X (%zu bytes)", address, data.size());
|
||||
auto snapshot_result = version_mgr_->CreateSnapshot(snapshot_desc);
|
||||
if (snapshot_result.ok()) {
|
||||
response->set_snapshot_id(std::to_string(snapshot_result.value()));
|
||||
}
|
||||
}
|
||||
|
||||
// Perform write
|
||||
std::memcpy(rom_->mutable_data() + address, data.data(), data.size());
|
||||
|
||||
response->set_success(true);
|
||||
response->set_message("Write successful");
|
||||
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status RomServiceImpl::GetRomInfo(
|
||||
grpc::ServerContext* context, const rom_svc::GetRomInfoRequest* request,
|
||||
rom_svc::GetRomInfoResponse* response) {
|
||||
if (!rom_ || !rom_->is_loaded()) {
|
||||
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
|
||||
"ROM not loaded");
|
||||
}
|
||||
|
||||
auto* info = response->mutable_info();
|
||||
info->set_title(rom_->title());
|
||||
info->set_size(rom_->size());
|
||||
info->set_is_loaded(rom_->is_loaded());
|
||||
info->set_filename(rom_->filename());
|
||||
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status RomServiceImpl::GetTileData(
|
||||
grpc::ServerContext* context, const rom_svc::GetTileDataRequest* request,
|
||||
rom_svc::GetTileDataResponse* response) {
|
||||
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
|
||||
"GetTileData not yet implemented");
|
||||
}
|
||||
|
||||
grpc::Status RomServiceImpl::SetTileData(
|
||||
grpc::ServerContext* context, const rom_svc::SetTileDataRequest* request,
|
||||
rom_svc::SetTileDataResponse* response) {
|
||||
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
|
||||
"SetTileData not yet implemented");
|
||||
}
|
||||
|
||||
grpc::Status RomServiceImpl::GetMapData(
|
||||
grpc::ServerContext* context, const rom_svc::GetMapDataRequest* request,
|
||||
rom_svc::GetMapDataResponse* response) {
|
||||
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
|
||||
"GetMapData not yet implemented");
|
||||
}
|
||||
|
||||
grpc::Status RomServiceImpl::SetMapData(
|
||||
grpc::ServerContext* context, const rom_svc::SetMapDataRequest* request,
|
||||
rom_svc::SetMapDataResponse* response) {
|
||||
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
|
||||
"SetMapData not yet implemented");
|
||||
}
|
||||
|
||||
grpc::Status RomServiceImpl::GetSpriteData(
|
||||
grpc::ServerContext* context, const rom_svc::GetSpriteDataRequest* request,
|
||||
rom_svc::GetSpriteDataResponse* response) {
|
||||
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
|
||||
"GetSpriteData not yet implemented");
|
||||
}
|
||||
|
||||
grpc::Status RomServiceImpl::SetSpriteData(
|
||||
grpc::ServerContext* context, const rom_svc::SetSpriteDataRequest* request,
|
||||
rom_svc::SetSpriteDataResponse* response) {
|
||||
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
|
||||
"SetSpriteData not yet implemented");
|
||||
}
|
||||
|
||||
grpc::Status RomServiceImpl::GetDialogue(
|
||||
grpc::ServerContext* context, const rom_svc::GetDialogueRequest* request,
|
||||
rom_svc::GetDialogueResponse* response) {
|
||||
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
|
||||
"GetDialogue not yet implemented");
|
||||
}
|
||||
|
||||
grpc::Status RomServiceImpl::SetDialogue(
|
||||
grpc::ServerContext* context, const rom_svc::SetDialogueRequest* request,
|
||||
rom_svc::SetDialogueResponse* response) {
|
||||
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
|
||||
"SetDialogue not yet implemented");
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_WITH_GRPC
|
||||
@@ -1,175 +0,0 @@
|
||||
#ifndef YAZE_APP_NET_ROM_SERVICE_IMPL_H_
|
||||
#define YAZE_APP_NET_ROM_SERVICE_IMPL_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
#ifdef _WIN32
|
||||
#pragma push_macro("DWORD")
|
||||
#pragma push_macro("ERROR")
|
||||
#undef DWORD
|
||||
#undef ERROR
|
||||
#endif // _WIN32
|
||||
#include <grpcpp/grpcpp.h>
|
||||
|
||||
#include "protos/rom_service.grpc.pb.h"
|
||||
#ifdef _WIN32
|
||||
#pragma pop_macro("DWORD")
|
||||
#pragma pop_macro("ERROR")
|
||||
#endif // _WIN32
|
||||
// Note: Proto files will be generated to build directory
|
||||
#endif
|
||||
|
||||
#include "app/net/rom_version_manager.h"
|
||||
#include "app/rom.h"
|
||||
|
||||
namespace yaze {
|
||||
|
||||
namespace net {
|
||||
|
||||
#ifdef YAZE_WITH_GRPC
|
||||
|
||||
/**
|
||||
* @brief gRPC service implementation for remote ROM manipulation
|
||||
*
|
||||
* Enables remote clients (like z3ed CLI) to:
|
||||
* - Read/write ROM data
|
||||
* - Submit proposals for collaborative editing
|
||||
* - Manage ROM versions and snapshots
|
||||
* - Query ROM structures (overworld, dungeons, sprites)
|
||||
*
|
||||
* Thread-safe and designed for concurrent access.
|
||||
*/
|
||||
class RomServiceImpl final : public proto::RomService::Service {
|
||||
public:
|
||||
/**
|
||||
* @brief Configuration for the ROM service
|
||||
*/
|
||||
struct Config {
|
||||
bool require_approval_for_writes = true; // Submit writes as proposals
|
||||
bool enable_version_management = true; // Auto-snapshot before changes
|
||||
int max_read_size_bytes = 1024 * 1024; // 1MB max per read
|
||||
bool allow_raw_rom_access = true; // Allow direct byte access
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Construct ROM service
|
||||
* @param rom Pointer to ROM instance (not owned)
|
||||
* @param version_mgr Pointer to version manager (not owned, optional)
|
||||
* @param approval_mgr Pointer to approval manager (not owned, optional)
|
||||
*/
|
||||
RomServiceImpl(Rom* rom, RomVersionManager* version_mgr = nullptr,
|
||||
ProposalApprovalManager* approval_mgr = nullptr);
|
||||
|
||||
~RomServiceImpl() override = default;
|
||||
|
||||
// Initialize with configuration
|
||||
void SetConfig(const Config& config);
|
||||
|
||||
// =========================================================================
|
||||
// Basic ROM Operations
|
||||
// =========================================================================
|
||||
|
||||
grpc::Status ReadBytes(grpc::ServerContext* context,
|
||||
const proto::ReadBytesRequest* request,
|
||||
proto::ReadBytesResponse* response) override;
|
||||
|
||||
grpc::Status WriteBytes(grpc::ServerContext* context,
|
||||
const proto::WriteBytesRequest* request,
|
||||
proto::WriteBytesResponse* response) override;
|
||||
|
||||
grpc::Status GetRomInfo(grpc::ServerContext* context,
|
||||
const proto::GetRomInfoRequest* request,
|
||||
proto::GetRomInfoResponse* response) override;
|
||||
|
||||
// =========================================================================
|
||||
// Overworld Operations
|
||||
// =========================================================================
|
||||
|
||||
grpc::Status ReadOverworldMap(
|
||||
grpc::ServerContext* context,
|
||||
const proto::ReadOverworldMapRequest* request,
|
||||
proto::ReadOverworldMapResponse* response) override;
|
||||
|
||||
grpc::Status WriteOverworldTile(
|
||||
grpc::ServerContext* context,
|
||||
const proto::WriteOverworldTileRequest* request,
|
||||
proto::WriteOverworldTileResponse* response) override;
|
||||
|
||||
// =========================================================================
|
||||
// Dungeon Operations
|
||||
// =========================================================================
|
||||
|
||||
grpc::Status ReadDungeonRoom(
|
||||
grpc::ServerContext* context,
|
||||
const proto::ReadDungeonRoomRequest* request,
|
||||
proto::ReadDungeonRoomResponse* response) override;
|
||||
|
||||
grpc::Status WriteDungeonTile(
|
||||
grpc::ServerContext* context,
|
||||
const proto::WriteDungeonTileRequest* request,
|
||||
proto::WriteDungeonTileResponse* response) override;
|
||||
|
||||
// =========================================================================
|
||||
// Sprite Operations
|
||||
// =========================================================================
|
||||
|
||||
grpc::Status ReadSprite(grpc::ServerContext* context,
|
||||
const proto::ReadSpriteRequest* request,
|
||||
proto::ReadSpriteResponse* response) override;
|
||||
|
||||
// =========================================================================
|
||||
// Proposal System
|
||||
// =========================================================================
|
||||
|
||||
grpc::Status SubmitRomProposal(
|
||||
grpc::ServerContext* context,
|
||||
const proto::SubmitRomProposalRequest* request,
|
||||
proto::SubmitRomProposalResponse* response) override;
|
||||
|
||||
grpc::Status GetProposalStatus(
|
||||
grpc::ServerContext* context,
|
||||
const proto::GetProposalStatusRequest* request,
|
||||
proto::GetProposalStatusResponse* response) override;
|
||||
|
||||
// =========================================================================
|
||||
// Version Management
|
||||
// =========================================================================
|
||||
|
||||
grpc::Status CreateSnapshot(grpc::ServerContext* context,
|
||||
const proto::CreateSnapshotRequest* request,
|
||||
proto::CreateSnapshotResponse* response) override;
|
||||
|
||||
grpc::Status RestoreSnapshot(
|
||||
grpc::ServerContext* context,
|
||||
const proto::RestoreSnapshotRequest* request,
|
||||
proto::RestoreSnapshotResponse* response) override;
|
||||
|
||||
grpc::Status ListSnapshots(grpc::ServerContext* context,
|
||||
const proto::ListSnapshotsRequest* request,
|
||||
proto::ListSnapshotsResponse* response) override;
|
||||
|
||||
private:
|
||||
Config config_;
|
||||
Rom* rom_; // Not owned
|
||||
RomVersionManager* version_mgr_; // Not owned, may be null
|
||||
ProposalApprovalManager* approval_mgr_; // Not owned, may be null
|
||||
|
||||
// Helper to check if ROM is loaded
|
||||
grpc::Status ValidateRomLoaded();
|
||||
|
||||
// Helper to create snapshot before write operations
|
||||
absl::Status MaybeCreateSnapshot(const std::string& description);
|
||||
};
|
||||
|
||||
#endif // YAZE_WITH_GRPC
|
||||
|
||||
} // namespace net
|
||||
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_NET_ROM_SERVICE_IMPL_H_
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "app/rom.h"
|
||||
#include "rom/rom.h"
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
195
src/app/net/wasm/emscripten_http_client.cc
Normal file
195
src/app/net/wasm/emscripten_http_client.cc
Normal file
@@ -0,0 +1,195 @@
|
||||
#ifdef __EMSCRIPTEN__
|
||||
|
||||
#include "app/net/wasm/emscripten_http_client.h"
|
||||
|
||||
#include <emscripten/fetch.h>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace yaze {
|
||||
namespace net {
|
||||
|
||||
EmscriptenHttpClient::EmscriptenHttpClient() {
|
||||
// Constructor
|
||||
}
|
||||
|
||||
EmscriptenHttpClient::~EmscriptenHttpClient() {
|
||||
// Destructor
|
||||
}
|
||||
|
||||
void EmscriptenHttpClient::OnFetchSuccess(emscripten_fetch_t* fetch) {
|
||||
FetchResult* result = static_cast<FetchResult*>(fetch->userData);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(result->mutex);
|
||||
result->success = true;
|
||||
result->status_code = fetch->status;
|
||||
result->body = std::string(fetch->data, fetch->numBytes);
|
||||
|
||||
// Parse response headers if available
|
||||
// Note: Emscripten fetch API has limited header access due to CORS
|
||||
// Only headers exposed by Access-Control-Expose-Headers are available
|
||||
|
||||
result->completed = true;
|
||||
}
|
||||
result->cv.notify_one();
|
||||
|
||||
emscripten_fetch_close(fetch);
|
||||
}
|
||||
|
||||
void EmscriptenHttpClient::OnFetchError(emscripten_fetch_t* fetch) {
|
||||
FetchResult* result = static_cast<FetchResult*>(fetch->userData);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(result->mutex);
|
||||
result->success = false;
|
||||
result->status_code = fetch->status;
|
||||
|
||||
if (fetch->status == 0) {
|
||||
result->error_message = "Network error or CORS blocking";
|
||||
} else {
|
||||
result->error_message = "HTTP error: " + std::to_string(fetch->status);
|
||||
}
|
||||
|
||||
result->completed = true;
|
||||
}
|
||||
result->cv.notify_one();
|
||||
|
||||
emscripten_fetch_close(fetch);
|
||||
}
|
||||
|
||||
void EmscriptenHttpClient::OnFetchProgress(emscripten_fetch_t* fetch) {
|
||||
// Progress callback - can be used for download progress
|
||||
// Not implemented for now
|
||||
(void)fetch; // Suppress unused parameter warning
|
||||
}
|
||||
|
||||
absl::StatusOr<HttpResponse> EmscriptenHttpClient::PerformFetch(
|
||||
const std::string& method,
|
||||
const std::string& url,
|
||||
const std::string& body,
|
||||
const Headers& headers) {
|
||||
|
||||
// Create result structure
|
||||
FetchResult result;
|
||||
|
||||
// Initialize fetch attributes
|
||||
emscripten_fetch_attr_t attr;
|
||||
emscripten_fetch_attr_init(&attr);
|
||||
|
||||
// Set HTTP method
|
||||
strncpy(attr.requestMethod, method.c_str(), sizeof(attr.requestMethod) - 1);
|
||||
attr.requestMethod[sizeof(attr.requestMethod) - 1] = '\0';
|
||||
|
||||
// Set attributes for synchronous-style operation
|
||||
// We use async fetch with callbacks but wait for completion
|
||||
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
|
||||
|
||||
// Set callbacks
|
||||
attr.onsuccess = OnFetchSuccess;
|
||||
attr.onerror = OnFetchError;
|
||||
attr.onprogress = OnFetchProgress;
|
||||
attr.userData = &result;
|
||||
|
||||
// Set timeout
|
||||
attr.timeoutMSecs = timeout_seconds_ * 1000;
|
||||
|
||||
// Prepare headers
|
||||
std::vector<const char*> header_strings;
|
||||
std::vector<std::string> header_storage;
|
||||
|
||||
for (const auto& [key, value] : headers) {
|
||||
header_storage.push_back(key);
|
||||
header_storage.push_back(value);
|
||||
}
|
||||
|
||||
// Add Content-Type for POST/PUT if not provided
|
||||
if ((method == "POST" || method == "PUT") && !body.empty()) {
|
||||
bool has_content_type = false;
|
||||
for (const auto& [key, value] : headers) {
|
||||
if (key == "Content-Type") {
|
||||
has_content_type = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has_content_type) {
|
||||
header_storage.push_back("Content-Type");
|
||||
header_storage.push_back("application/json");
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to C-style array
|
||||
for (const auto& str : header_storage) {
|
||||
header_strings.push_back(str.c_str());
|
||||
}
|
||||
header_strings.push_back(nullptr); // Null-terminate
|
||||
|
||||
if (!header_strings.empty() && header_strings.size() > 1) {
|
||||
attr.requestHeaders = header_strings.data();
|
||||
}
|
||||
|
||||
// Set request body for POST/PUT
|
||||
if (!body.empty() && (method == "POST" || method == "PUT")) {
|
||||
attr.requestData = body.c_str();
|
||||
attr.requestDataSize = body.length();
|
||||
}
|
||||
|
||||
// Perform the fetch
|
||||
emscripten_fetch_t* fetch = emscripten_fetch(&attr, url.c_str());
|
||||
|
||||
if (!fetch) {
|
||||
return absl::InternalError("Failed to initiate fetch request");
|
||||
}
|
||||
|
||||
// Wait for completion (convert async to sync)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(result.mutex);
|
||||
result.cv.wait(lock, [&result] { return result.completed; });
|
||||
}
|
||||
|
||||
// Check result
|
||||
if (!result.success) {
|
||||
return absl::UnavailableError(result.error_message.empty()
|
||||
? "Fetch request failed"
|
||||
: result.error_message);
|
||||
}
|
||||
|
||||
// Build response
|
||||
HttpResponse response;
|
||||
response.status_code = result.status_code;
|
||||
response.body = result.body;
|
||||
response.headers = result.headers;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
absl::StatusOr<HttpResponse> EmscriptenHttpClient::Get(
|
||||
const std::string& url,
|
||||
const Headers& headers) {
|
||||
return PerformFetch("GET", url, "", headers);
|
||||
}
|
||||
|
||||
absl::StatusOr<HttpResponse> EmscriptenHttpClient::Post(
|
||||
const std::string& url,
|
||||
const std::string& body,
|
||||
const Headers& headers) {
|
||||
return PerformFetch("POST", url, body, headers);
|
||||
}
|
||||
|
||||
absl::StatusOr<HttpResponse> EmscriptenHttpClient::Put(
|
||||
const std::string& url,
|
||||
const std::string& body,
|
||||
const Headers& headers) {
|
||||
return PerformFetch("PUT", url, body, headers);
|
||||
}
|
||||
|
||||
absl::StatusOr<HttpResponse> EmscriptenHttpClient::Delete(
|
||||
const std::string& url,
|
||||
const Headers& headers) {
|
||||
return PerformFetch("DELETE", url, "", headers);
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace yaze
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
122
src/app/net/wasm/emscripten_http_client.h
Normal file
122
src/app/net/wasm/emscripten_http_client.h
Normal file
@@ -0,0 +1,122 @@
|
||||
#ifndef YAZE_APP_NET_WASM_EMSCRIPTEN_HTTP_CLIENT_H_
|
||||
#define YAZE_APP_NET_WASM_EMSCRIPTEN_HTTP_CLIENT_H_
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
|
||||
#include <emscripten/fetch.h>
|
||||
|
||||
#include "app/net/http_client.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @class EmscriptenHttpClient
|
||||
* @brief WASM HTTP client implementation using Emscripten Fetch API
|
||||
*
|
||||
* This implementation wraps the Emscripten fetch API for browser-based
|
||||
* HTTP/HTTPS requests. All requests are subject to browser CORS policies.
|
||||
*/
|
||||
class EmscriptenHttpClient : public IHttpClient {
|
||||
public:
|
||||
EmscriptenHttpClient();
|
||||
~EmscriptenHttpClient() override;
|
||||
|
||||
/**
|
||||
* @brief Perform an HTTP GET request
|
||||
* @param url The URL to request
|
||||
* @param headers Optional HTTP headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
absl::StatusOr<HttpResponse> Get(
|
||||
const std::string& url,
|
||||
const Headers& headers = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Perform an HTTP POST request
|
||||
* @param url The URL to post to
|
||||
* @param body The request body
|
||||
* @param headers Optional HTTP headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
absl::StatusOr<HttpResponse> Post(
|
||||
const std::string& url,
|
||||
const std::string& body,
|
||||
const Headers& headers = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Perform an HTTP PUT request
|
||||
* @param url The URL to put to
|
||||
* @param body The request body
|
||||
* @param headers Optional HTTP headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
absl::StatusOr<HttpResponse> Put(
|
||||
const std::string& url,
|
||||
const std::string& body,
|
||||
const Headers& headers = {}) override;
|
||||
|
||||
/**
|
||||
* @brief Perform an HTTP DELETE request
|
||||
* @param url The URL to delete
|
||||
* @param headers Optional HTTP headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
absl::StatusOr<HttpResponse> Delete(
|
||||
const std::string& url,
|
||||
const Headers& headers = {}) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Structure to hold fetch result data
|
||||
*/
|
||||
struct FetchResult {
|
||||
bool completed = false;
|
||||
bool success = false;
|
||||
int status_code = 0;
|
||||
std::string body;
|
||||
Headers headers;
|
||||
std::string error_message;
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Perform a fetch request
|
||||
* @param method HTTP method
|
||||
* @param url Request URL
|
||||
* @param body Request body (for POST/PUT)
|
||||
* @param headers Request headers
|
||||
* @return HttpResponse or error status
|
||||
*/
|
||||
absl::StatusOr<HttpResponse> PerformFetch(
|
||||
const std::string& method,
|
||||
const std::string& url,
|
||||
const std::string& body = "",
|
||||
const Headers& headers = {});
|
||||
|
||||
/**
|
||||
* @brief Success callback for fetch operations
|
||||
*/
|
||||
static void OnFetchSuccess(emscripten_fetch_t* fetch);
|
||||
|
||||
/**
|
||||
* @brief Error callback for fetch operations
|
||||
*/
|
||||
static void OnFetchError(emscripten_fetch_t* fetch);
|
||||
|
||||
/**
|
||||
* @brief Progress callback for fetch operations
|
||||
*/
|
||||
static void OnFetchProgress(emscripten_fetch_t* fetch);
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace yaze
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
||||
#endif // YAZE_APP_NET_WASM_EMSCRIPTEN_HTTP_CLIENT_H_
|
||||
224
src/app/net/wasm/emscripten_websocket.cc
Normal file
224
src/app/net/wasm/emscripten_websocket.cc
Normal file
@@ -0,0 +1,224 @@
|
||||
#ifdef __EMSCRIPTEN__
|
||||
|
||||
#include "app/net/wasm/emscripten_websocket.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace yaze {
|
||||
namespace net {
|
||||
|
||||
EmscriptenWebSocket::EmscriptenWebSocket()
|
||||
: socket_(0), socket_valid_(false) {
|
||||
state_ = WebSocketState::kDisconnected;
|
||||
}
|
||||
|
||||
EmscriptenWebSocket::~EmscriptenWebSocket() {
|
||||
if (socket_valid_ && state_ != WebSocketState::kDisconnected) {
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
EM_BOOL EmscriptenWebSocket::OnOpenCallback(
|
||||
int eventType,
|
||||
const EmscriptenWebSocketOpenEvent* websocketEvent,
|
||||
void* userData) {
|
||||
|
||||
EmscriptenWebSocket* self = static_cast<EmscriptenWebSocket*>(userData);
|
||||
|
||||
self->state_ = WebSocketState::kConnected;
|
||||
|
||||
if (self->open_callback_) {
|
||||
self->open_callback_();
|
||||
}
|
||||
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
EM_BOOL EmscriptenWebSocket::OnCloseCallback(
|
||||
int eventType,
|
||||
const EmscriptenWebSocketCloseEvent* websocketEvent,
|
||||
void* userData) {
|
||||
|
||||
EmscriptenWebSocket* self = static_cast<EmscriptenWebSocket*>(userData);
|
||||
|
||||
self->state_ = WebSocketState::kClosed;
|
||||
self->socket_valid_ = false;
|
||||
|
||||
if (self->close_callback_) {
|
||||
self->close_callback_(websocketEvent->code,
|
||||
websocketEvent->reason ? websocketEvent->reason : "");
|
||||
}
|
||||
|
||||
self->state_ = WebSocketState::kDisconnected;
|
||||
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
EM_BOOL EmscriptenWebSocket::OnErrorCallback(
|
||||
int eventType,
|
||||
const EmscriptenWebSocketErrorEvent* websocketEvent,
|
||||
void* userData) {
|
||||
|
||||
EmscriptenWebSocket* self = static_cast<EmscriptenWebSocket*>(userData);
|
||||
|
||||
self->state_ = WebSocketState::kError;
|
||||
|
||||
if (self->error_callback_) {
|
||||
self->error_callback_("WebSocket error occurred");
|
||||
}
|
||||
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
EM_BOOL EmscriptenWebSocket::OnMessageCallback(
|
||||
int eventType,
|
||||
const EmscriptenWebSocketMessageEvent* websocketEvent,
|
||||
void* userData) {
|
||||
|
||||
EmscriptenWebSocket* self = static_cast<EmscriptenWebSocket*>(userData);
|
||||
|
||||
if (websocketEvent->isText) {
|
||||
// Text message
|
||||
if (self->message_callback_) {
|
||||
// Convert UTF-8 data to string
|
||||
std::string message(reinterpret_cast<const char*>(websocketEvent->data),
|
||||
websocketEvent->numBytes);
|
||||
self->message_callback_(message);
|
||||
}
|
||||
} else {
|
||||
// Binary message
|
||||
if (self->binary_message_callback_) {
|
||||
self->binary_message_callback_(websocketEvent->data,
|
||||
websocketEvent->numBytes);
|
||||
}
|
||||
}
|
||||
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
absl::Status EmscriptenWebSocket::Connect(const std::string& url) {
|
||||
if (state_ != WebSocketState::kDisconnected) {
|
||||
return absl::FailedPreconditionError(
|
||||
"WebSocket already connected or connecting");
|
||||
}
|
||||
|
||||
state_ = WebSocketState::kConnecting;
|
||||
url_ = url;
|
||||
|
||||
// Create WebSocket attributes
|
||||
EmscriptenWebSocketCreateAttributes attrs = {
|
||||
url.c_str(),
|
||||
nullptr, // protocols (NULL = default)
|
||||
EM_TRUE // createOnMainThread
|
||||
};
|
||||
|
||||
// Create the WebSocket
|
||||
socket_ = emscripten_websocket_new(&attrs);
|
||||
|
||||
if (socket_ <= 0) {
|
||||
state_ = WebSocketState::kError;
|
||||
return absl::InternalError("Failed to create WebSocket");
|
||||
}
|
||||
|
||||
socket_valid_ = true;
|
||||
|
||||
// Set callbacks
|
||||
EMSCRIPTEN_RESULT result;
|
||||
|
||||
result = emscripten_websocket_set_onopen_callback(
|
||||
socket_, this, OnOpenCallback);
|
||||
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
|
||||
state_ = WebSocketState::kError;
|
||||
socket_valid_ = false;
|
||||
return absl::InternalError("Failed to set onopen callback");
|
||||
}
|
||||
|
||||
result = emscripten_websocket_set_onclose_callback(
|
||||
socket_, this, OnCloseCallback);
|
||||
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
|
||||
state_ = WebSocketState::kError;
|
||||
socket_valid_ = false;
|
||||
return absl::InternalError("Failed to set onclose callback");
|
||||
}
|
||||
|
||||
result = emscripten_websocket_set_onerror_callback(
|
||||
socket_, this, OnErrorCallback);
|
||||
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
|
||||
state_ = WebSocketState::kError;
|
||||
socket_valid_ = false;
|
||||
return absl::InternalError("Failed to set onerror callback");
|
||||
}
|
||||
|
||||
result = emscripten_websocket_set_onmessage_callback(
|
||||
socket_, this, OnMessageCallback);
|
||||
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
|
||||
state_ = WebSocketState::kError;
|
||||
socket_valid_ = false;
|
||||
return absl::InternalError("Failed to set onmessage callback");
|
||||
}
|
||||
|
||||
// Connection is asynchronous in the browser
|
||||
// The OnOpenCallback will be called when connected
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status EmscriptenWebSocket::Send(const std::string& message) {
|
||||
if (state_ != WebSocketState::kConnected || !socket_valid_) {
|
||||
return absl::FailedPreconditionError("WebSocket not connected");
|
||||
}
|
||||
|
||||
EMSCRIPTEN_RESULT result = emscripten_websocket_send_utf8_text(
|
||||
socket_, message.c_str());
|
||||
|
||||
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
|
||||
return absl::InternalError("Failed to send text message");
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status EmscriptenWebSocket::SendBinary(const uint8_t* data,
|
||||
size_t length) {
|
||||
if (state_ != WebSocketState::kConnected || !socket_valid_) {
|
||||
return absl::FailedPreconditionError("WebSocket not connected");
|
||||
}
|
||||
|
||||
EMSCRIPTEN_RESULT result = emscripten_websocket_send_binary(
|
||||
socket_, const_cast<void*>(static_cast<const void*>(data)), length);
|
||||
|
||||
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
|
||||
return absl::InternalError("Failed to send binary message");
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status EmscriptenWebSocket::Close(int code, const std::string& reason) {
|
||||
if (state_ == WebSocketState::kDisconnected ||
|
||||
state_ == WebSocketState::kClosed ||
|
||||
!socket_valid_) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
state_ = WebSocketState::kClosing;
|
||||
|
||||
EMSCRIPTEN_RESULT result = emscripten_websocket_close(
|
||||
socket_, code, reason.c_str());
|
||||
|
||||
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
|
||||
// Force state to closed even if close fails
|
||||
state_ = WebSocketState::kClosed;
|
||||
socket_valid_ = false;
|
||||
return absl::InternalError("Failed to close WebSocket");
|
||||
}
|
||||
|
||||
// OnCloseCallback will be called when the close completes
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace yaze
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
132
src/app/net/wasm/emscripten_websocket.h
Normal file
132
src/app/net/wasm/emscripten_websocket.h
Normal file
@@ -0,0 +1,132 @@
|
||||
#ifndef YAZE_APP_NET_WASM_EMSCRIPTEN_WEBSOCKET_H_
|
||||
#define YAZE_APP_NET_WASM_EMSCRIPTEN_WEBSOCKET_H_
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
|
||||
#include <emscripten/websocket.h>
|
||||
|
||||
#include "app/net/websocket_interface.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @class EmscriptenWebSocket
|
||||
* @brief WASM WebSocket implementation using Emscripten WebSocket API
|
||||
*
|
||||
* This implementation wraps the Emscripten WebSocket API which provides
|
||||
* direct access to the browser's native WebSocket implementation.
|
||||
*/
|
||||
class EmscriptenWebSocket : public IWebSocket {
|
||||
public:
|
||||
EmscriptenWebSocket();
|
||||
~EmscriptenWebSocket() override;
|
||||
|
||||
/**
|
||||
* @brief Connect to a WebSocket server
|
||||
* @param url The WebSocket URL (ws:// or wss://)
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status Connect(const std::string& url) override;
|
||||
|
||||
/**
|
||||
* @brief Send a text message
|
||||
* @param message The text message to send
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status Send(const std::string& message) override;
|
||||
|
||||
/**
|
||||
* @brief Send a binary message
|
||||
* @param data The binary data to send
|
||||
* @param length The length of the data
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status SendBinary(const uint8_t* data, size_t length) override;
|
||||
|
||||
/**
|
||||
* @brief Close the WebSocket connection
|
||||
* @param code Optional close code
|
||||
* @param reason Optional close reason
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
absl::Status Close(int code = 1000,
|
||||
const std::string& reason = "") override;
|
||||
|
||||
/**
|
||||
* @brief Get the current connection state
|
||||
* @return Current WebSocket state
|
||||
*/
|
||||
WebSocketState GetState() const override { return state_; }
|
||||
|
||||
/**
|
||||
* @brief Set callback for text message events
|
||||
* @param callback Function to call when a text message is received
|
||||
*/
|
||||
void OnMessage(MessageCallback callback) override {
|
||||
message_callback_ = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set callback for binary message events
|
||||
* @param callback Function to call when binary data is received
|
||||
*/
|
||||
void OnBinaryMessage(BinaryMessageCallback callback) override {
|
||||
binary_message_callback_ = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set callback for connection open events
|
||||
* @param callback Function to call when connection is established
|
||||
*/
|
||||
void OnOpen(OpenCallback callback) override {
|
||||
open_callback_ = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set callback for connection close events
|
||||
* @param callback Function to call when connection is closed
|
||||
*/
|
||||
void OnClose(CloseCallback callback) override {
|
||||
close_callback_ = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set callback for error events
|
||||
* @param callback Function to call when an error occurs
|
||||
*/
|
||||
void OnError(ErrorCallback callback) override {
|
||||
error_callback_ = callback;
|
||||
}
|
||||
|
||||
private:
|
||||
// Emscripten WebSocket callbacks (static, with user data)
|
||||
static EM_BOOL OnOpenCallback(int eventType,
|
||||
const EmscriptenWebSocketOpenEvent* websocketEvent,
|
||||
void* userData);
|
||||
|
||||
static EM_BOOL OnCloseCallback(int eventType,
|
||||
const EmscriptenWebSocketCloseEvent* websocketEvent,
|
||||
void* userData);
|
||||
|
||||
static EM_BOOL OnErrorCallback(int eventType,
|
||||
const EmscriptenWebSocketErrorEvent* websocketEvent,
|
||||
void* userData);
|
||||
|
||||
static EM_BOOL OnMessageCallback(int eventType,
|
||||
const EmscriptenWebSocketMessageEvent* websocketEvent,
|
||||
void* userData);
|
||||
|
||||
// Emscripten WebSocket handle
|
||||
EMSCRIPTEN_WEBSOCKET_T socket_;
|
||||
|
||||
// Track if socket is valid
|
||||
bool socket_valid_;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace yaze
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
||||
#endif // YAZE_APP_NET_WASM_EMSCRIPTEN_WEBSOCKET_H_
|
||||
@@ -8,7 +8,8 @@
|
||||
#include "absl/strings/str_format.h"
|
||||
|
||||
// Cross-platform WebSocket support using httplib
|
||||
#ifdef YAZE_WITH_JSON
|
||||
// Skip httplib in WASM builds - use Emscripten WebSocket API instead
|
||||
#if defined(YAZE_WITH_JSON) && !defined(__EMSCRIPTEN__)
|
||||
#ifndef _WIN32
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#endif
|
||||
@@ -19,7 +20,8 @@ namespace yaze {
|
||||
|
||||
namespace net {
|
||||
|
||||
#ifdef YAZE_WITH_JSON
|
||||
// Native (non-WASM) implementation using httplib
|
||||
#if defined(YAZE_WITH_JSON) && !defined(__EMSCRIPTEN__)
|
||||
|
||||
// Platform-independent WebSocket implementation using httplib
|
||||
class WebSocketClient::Impl {
|
||||
@@ -151,6 +153,25 @@ class WebSocketClient::Impl {
|
||||
std::function<void(const std::string&)> error_callback_;
|
||||
};
|
||||
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
|
||||
// WASM stub - uses EmscriptenWebSocket from wasm/ directory instead
|
||||
class WebSocketClient::Impl {
|
||||
public:
|
||||
absl::Status Connect(const std::string&, int) {
|
||||
return absl::UnimplementedError(
|
||||
"Use EmscriptenWebSocket for WASM WebSocket connections");
|
||||
}
|
||||
void Disconnect() {}
|
||||
absl::Status Send(const std::string&) {
|
||||
return absl::UnimplementedError(
|
||||
"Use EmscriptenWebSocket for WASM WebSocket connections");
|
||||
}
|
||||
void SetMessageCallback(std::function<void(const std::string&)>) {}
|
||||
void SetErrorCallback(std::function<void(const std::string&)>) {}
|
||||
bool IsConnected() const { return false; }
|
||||
};
|
||||
|
||||
#else
|
||||
|
||||
// Stub implementation when JSON is not available
|
||||
@@ -168,7 +189,7 @@ class WebSocketClient::Impl {
|
||||
bool IsConnected() const { return false; }
|
||||
};
|
||||
|
||||
#endif // YAZE_WITH_JSON
|
||||
#endif // YAZE_WITH_JSON && !__EMSCRIPTEN__
|
||||
|
||||
// ============================================================================
|
||||
// WebSocketClient Implementation
|
||||
|
||||
160
src/app/net/websocket_interface.h
Normal file
160
src/app/net/websocket_interface.h
Normal file
@@ -0,0 +1,160 @@
|
||||
#ifndef YAZE_APP_NET_WEBSOCKET_INTERFACE_H_
|
||||
#define YAZE_APP_NET_WEBSOCKET_INTERFACE_H_
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace net {
|
||||
|
||||
/**
|
||||
* @enum WebSocketState
|
||||
* @brief WebSocket connection states
|
||||
*/
|
||||
enum class WebSocketState {
|
||||
kDisconnected, // Not connected
|
||||
kConnecting, // Connection in progress
|
||||
kConnected, // Successfully connected
|
||||
kClosing, // Close handshake in progress
|
||||
kClosed, // Connection closed
|
||||
kError // Error state
|
||||
};
|
||||
|
||||
/**
|
||||
* @class IWebSocket
|
||||
* @brief Abstract interface for WebSocket client implementations
|
||||
*
|
||||
* This interface abstracts WebSocket operations to support both native
|
||||
* (using various libraries) and WASM (using emscripten WebSocket) implementations.
|
||||
* All methods use absl::Status for consistent error handling.
|
||||
*/
|
||||
class IWebSocket {
|
||||
public:
|
||||
// Callback types for WebSocket events
|
||||
using MessageCallback = std::function<void(const std::string& message)>;
|
||||
using BinaryMessageCallback = std::function<void(const uint8_t* data, size_t length)>;
|
||||
using OpenCallback = std::function<void()>;
|
||||
using CloseCallback = std::function<void(int code, const std::string& reason)>;
|
||||
using ErrorCallback = std::function<void(const std::string& error)>;
|
||||
|
||||
virtual ~IWebSocket() = default;
|
||||
|
||||
/**
|
||||
* @brief Connect to a WebSocket server
|
||||
* @param url The WebSocket URL (ws:// or wss://)
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
virtual absl::Status Connect(const std::string& url) = 0;
|
||||
|
||||
/**
|
||||
* @brief Send a text message
|
||||
* @param message The text message to send
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
virtual absl::Status Send(const std::string& message) = 0;
|
||||
|
||||
/**
|
||||
* @brief Send a binary message
|
||||
* @param data The binary data to send
|
||||
* @param length The length of the data
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
virtual absl::Status SendBinary(const uint8_t* data, size_t length) {
|
||||
// Default implementation - can be overridden if binary is supported
|
||||
return absl::UnimplementedError("Binary messages not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Close the WebSocket connection
|
||||
* @param code Optional close code (default: 1000 for normal closure)
|
||||
* @param reason Optional close reason
|
||||
* @return Status indicating success or failure
|
||||
*/
|
||||
virtual absl::Status Close(int code = 1000,
|
||||
const std::string& reason = "") = 0;
|
||||
|
||||
/**
|
||||
* @brief Get the current connection state
|
||||
* @return Current WebSocket state
|
||||
*/
|
||||
virtual WebSocketState GetState() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Check if the WebSocket is connected
|
||||
* @return true if connected, false otherwise
|
||||
*/
|
||||
virtual bool IsConnected() const {
|
||||
return GetState() == WebSocketState::kConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set callback for text message events
|
||||
* @param callback Function to call when a text message is received
|
||||
*/
|
||||
virtual void OnMessage(MessageCallback callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for binary message events
|
||||
* @param callback Function to call when binary data is received
|
||||
*/
|
||||
virtual void OnBinaryMessage(BinaryMessageCallback callback) {
|
||||
// Default implementation - can be overridden if binary is supported
|
||||
binary_message_callback_ = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set callback for connection open events
|
||||
* @param callback Function to call when connection is established
|
||||
*/
|
||||
virtual void OnOpen(OpenCallback callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for connection close events
|
||||
* @param callback Function to call when connection is closed
|
||||
*/
|
||||
virtual void OnClose(CloseCallback callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Set callback for error events
|
||||
* @param callback Function to call when an error occurs
|
||||
*/
|
||||
virtual void OnError(ErrorCallback callback) = 0;
|
||||
|
||||
/**
|
||||
* @brief Get the WebSocket URL
|
||||
* @return The URL this socket is connected/connecting to
|
||||
*/
|
||||
virtual std::string GetUrl() const { return url_; }
|
||||
|
||||
/**
|
||||
* @brief Set automatic reconnection
|
||||
* @param enable Enable or disable auto-reconnect
|
||||
* @param delay_seconds Delay between reconnection attempts
|
||||
*/
|
||||
virtual void SetAutoReconnect(bool enable, int delay_seconds = 5) {
|
||||
auto_reconnect_ = enable;
|
||||
reconnect_delay_seconds_ = delay_seconds;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::string url_;
|
||||
WebSocketState state_ = WebSocketState::kDisconnected;
|
||||
|
||||
// Callbacks (may be used by implementations)
|
||||
MessageCallback message_callback_;
|
||||
BinaryMessageCallback binary_message_callback_;
|
||||
OpenCallback open_callback_;
|
||||
CloseCallback close_callback_;
|
||||
ErrorCallback error_callback_;
|
||||
|
||||
// Auto-reconnect settings
|
||||
bool auto_reconnect_ = false;
|
||||
int reconnect_delay_seconds_ = 5;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_NET_WEBSOCKET_INTERFACE_H_
|
||||
Reference in New Issue
Block a user