mirror of
https://github.com/google/pebble
synced 2026-10-17 08:44:11 +02:00
1071 lines
39 KiB
C
1071 lines
39 KiB
C
/*
|
|
* Copyright 2024 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "jerry-api.h"
|
|
#include "rocky_api_app_message.h"
|
|
#include "rocky_api_errors.h"
|
|
#include "rocky_api_global.h"
|
|
#include "rocky_api_util.h"
|
|
|
|
#include "applib/app_logging.h"
|
|
#include "applib/app_message/app_message.h"
|
|
#include "applib/app_timer.h"
|
|
#include "applib/event_service_client.h"
|
|
#include "kernel/pbl_malloc.h"
|
|
#include "services/common/comm_session/session.h"
|
|
#include "system/passert.h"
|
|
#include "syscall/syscall.h"
|
|
#include "util/attributes.h"
|
|
#include "util/math.h"
|
|
#include "util/string.h"
|
|
|
|
#define DEBUG_ROCKY_APPMESSAGE 1
|
|
#if DEBUG_ROCKY_APPMESSAGE
|
|
#define DBG(fmt, ...) PBL_LOG(LOG_LEVEL_DEBUG, fmt, ## __VA_ARGS__)
|
|
#else
|
|
#define DBG(fmt, ...)
|
|
#endif
|
|
|
|
#define DEBUG_VERBOSE_ROCKY_APPMESSAGE 1
|
|
#if DEBUG_VERBOSE_ROCKY_APPMESSAGE
|
|
#define DBG_VERBOSE(fmt, ...) PBL_LOG(LOG_LEVEL_DEBUG, fmt, ## __VA_ARGS__)
|
|
#else
|
|
#define DBG_VERBOSE(fmt, ...)
|
|
#endif
|
|
|
|
|
|
#define ROCKY_EVENT_MESSAGE "message"
|
|
#define ROCKY_EVENT_MESSAGE_DATA "data"
|
|
#define ROCKY_EVENT_CONNECTED "postmessageconnected"
|
|
#define ROCKY_EVENT_DISCONNECTED "postmessagedisconnected"
|
|
#define ROCKY_EVENT_ERROR "postmessageerror"
|
|
#define ROCKY_POSTMESSAGE "postMessage"
|
|
|
|
#define GLOBAL_JSON "JSON"
|
|
#define GLOBAL_JSON_STRINGIFY "stringify"
|
|
#define GLOBAL_JSON_PARSE "parse"
|
|
|
|
#define CONTROL_MESSAGE_MAX_FAILURES (3)
|
|
#define CHUNK_MESSAGE_MAX_FAILURES (3)
|
|
#define RETRY_DELAY_MS (1000)
|
|
#define SESSION_CLOSED_TIMEOUT_MS (3000)
|
|
|
|
typedef struct {
|
|
ListNode node;
|
|
uint32_t key;
|
|
size_t length;
|
|
uint8_t data[0];
|
|
} MessageNode;
|
|
|
|
typedef struct OutgoingObject {
|
|
ListNode node;
|
|
|
|
//! Working buffer containing the JSON string respresentation of the object.
|
|
char *data_buffer;
|
|
|
|
//! The next offset in bytes, into the JSON string (excluding the PostMessageChunkPayload
|
|
//! header) that the next chunk's payload will start at.
|
|
uint32_t offset_bytes;
|
|
} OutgoingObject;
|
|
|
|
typedef enum {
|
|
OutboxMsgTypeNone,
|
|
OutboxMsgTypeControl,
|
|
OutboxMsgTypeChunk
|
|
} OutboxMsgType;
|
|
|
|
// TODO: PBL-35780 make this part of app_state_get_rocky_runtime_context()
|
|
SECTION(".rocky_bss") static struct {
|
|
PostMessageState state;
|
|
EventServiceInfo comm_session_event_info;
|
|
|
|
//! NOTE: Negotiated values are only valid if state == PostMessageStateSessionOpen
|
|
uint8_t protocol_version; //!< Negotiated protocol version being used
|
|
uint16_t tx_chunk_size_bytes; //!< Negotiated outgoing chunk size
|
|
uint16_t rx_chunk_size_bytes; //!< Negotiated incoming chunk size
|
|
|
|
struct {
|
|
//! Queue with pending control messages (head == oldest message)
|
|
MessageNode *control_msg_queue;
|
|
|
|
//! Queue with objects to send
|
|
OutgoingObject *object_queue;
|
|
|
|
//! This timer runs when not in SessionOpen.
|
|
//! Upon timeout, the head of the object queue is error'd out.
|
|
AppTimer *session_closed_object_queue_timer;
|
|
|
|
//! Type of message that is currently in the outbox, being sent out.
|
|
OutboxMsgType msg_type;
|
|
|
|
//! Number of failures for the current AppMessage.
|
|
int failure_count;
|
|
|
|
AppTimer *app_msg_retry_timer;
|
|
} out;
|
|
|
|
struct {
|
|
uint8_t *reassembly_buffer;
|
|
size_t received_size_bytes;
|
|
size_t total_size_bytes;
|
|
} in;
|
|
} s_state;
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Forward Declarations
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void prv_session_open__enter(const PostMessageResetCompletePayload *rc);
|
|
static void prv_awaiting_reset_request__enter(void);
|
|
static void prv_awaiting_reset_complete_remote_initiated_enter(void);
|
|
static void prv_awaiting_reset_complete_local_initiated__enter(bool should_send_reset_request);
|
|
static void prv_send_reset_request(void);
|
|
static void prv_session_open__after_exit(void);
|
|
static void prv_object_queue_pop_head_and_emit_error_event_and_own_json_buffer(void);
|
|
static void prv_start_session_closed_object_queue_timer(void);
|
|
static void prv_stop_session_closed_object_queue_timer(void);
|
|
|
|
static bool prv_is_outbox_busy(void);
|
|
static void prv_outbox_try_send_next(void);
|
|
T_STATIC jerry_value_t prv_json_parse(const char *object);
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Comm Session Handling
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
T_STATIC void prv_handle_connection(void) {
|
|
if (s_state.state != PostMessageStateDisconnected) {
|
|
// Handle the race. See comment in prv_init_apis().
|
|
return;
|
|
}
|
|
DBG("Transport Connected");
|
|
prv_awaiting_reset_request__enter();
|
|
}
|
|
|
|
T_STATIC void prv_handle_disconnection(void) {
|
|
if (s_state.state == PostMessageStateDisconnected) {
|
|
// Handle the race. See comment in prv_init_apis().
|
|
return;
|
|
}
|
|
DBG("Transport Disconnected");
|
|
const bool did_exit_session_open = (s_state.state == PostMessageStateSessionOpen);
|
|
s_state.state = PostMessageStateDisconnected;
|
|
if (did_exit_session_open) {
|
|
prv_session_open__after_exit();
|
|
}
|
|
}
|
|
|
|
static void prv_handle_comm_session_event(PebbleEvent *e, void *unused) {
|
|
PebbleCommSessionEvent *pcse = &e->bluetooth.comm_session_event;
|
|
if (!pcse->is_system) { // Need pkjs, which runs inside the Pebble app, so need system session.
|
|
return;
|
|
}
|
|
if (pcse->is_open) { // Connection event
|
|
prv_handle_connection();
|
|
} else { // Disconnect event
|
|
prv_handle_disconnection();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Outbound Object Queue
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void prv_object_queue_send_current_chunk(void);
|
|
|
|
static void prv_object_queue_pop_head(bool should_free_data_buffer) {
|
|
OutgoingObject *obj = s_state.out.object_queue;
|
|
list_remove((ListNode *)obj, (ListNode **)&s_state.out.object_queue, NULL);
|
|
|
|
if (should_free_data_buffer) {
|
|
task_free(obj->data_buffer);
|
|
}
|
|
task_free(obj);
|
|
}
|
|
|
|
static void prv_calc_current_chunk_size(size_t *out_bytes_remaining,
|
|
size_t *out_chunk_payload_size) {
|
|
OutgoingObject *obj = s_state.out.object_queue;
|
|
const size_t bytes_remaining = strlen(obj->data_buffer + obj->offset_bytes) + 1;
|
|
*out_bytes_remaining = bytes_remaining;
|
|
*out_chunk_payload_size = MIN(bytes_remaining, s_state.tx_chunk_size_bytes);
|
|
}
|
|
|
|
static void prv_object_queue_handle_chunk_sent(void) {
|
|
DBG("Sent Chunk Successfully.");
|
|
|
|
size_t bytes_remaining_before_sent_chunk;
|
|
size_t sent_chunk_payload_size;
|
|
prv_calc_current_chunk_size(&bytes_remaining_before_sent_chunk, &sent_chunk_payload_size);
|
|
|
|
const bool is_object_complete = (sent_chunk_payload_size == bytes_remaining_before_sent_chunk);
|
|
if (is_object_complete) {
|
|
DBG("Object Send Complete.");
|
|
prv_object_queue_pop_head(true /* should_free_data_buffer */);
|
|
} else {
|
|
OutgoingObject * const obj = s_state.out.object_queue;
|
|
obj->offset_bytes += sent_chunk_payload_size;
|
|
}
|
|
}
|
|
|
|
static void prv_object_queue_send_current_chunk(void) {
|
|
PBL_ASSERTN(s_state.out.object_queue);
|
|
|
|
DictionaryIterator *it = NULL;
|
|
app_message_outbox_begin(&it);
|
|
if (!it) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Failed to outbox_begin");
|
|
return;
|
|
}
|
|
|
|
size_t bytes_remaining;
|
|
size_t payload_size;
|
|
prv_calc_current_chunk_size(&bytes_remaining, &payload_size);
|
|
|
|
OutgoingObject *obj = s_state.out.object_queue;
|
|
|
|
// There is no dict_write_... API that lets us write a Tuple's byte array in multiple calls,
|
|
// so we're just going to poke in the data "manually" here:
|
|
it->dictionary->count = 1;
|
|
const size_t tuple_data_length = (sizeof(PostMessageChunkPayload) + payload_size);
|
|
*it->dictionary->head = (Tuple) {
|
|
.key = PostMessageKeyChunk,
|
|
.type = TUPLE_BYTE_ARRAY,
|
|
.length = tuple_data_length,
|
|
};
|
|
|
|
// Write the PostMessageChunkPayload header into obj->data_buffer (overwriting part of the JSON
|
|
// string that has been sent out):
|
|
const bool is_first = (obj->offset_bytes == 0);
|
|
PostMessageChunkPayload *next_chunk =
|
|
(PostMessageChunkPayload *) it->dictionary->head->value[0].data;
|
|
if (is_first) {
|
|
*next_chunk = (PostMessageChunkPayload) {
|
|
.total_size_bytes = bytes_remaining,
|
|
.is_first = true,
|
|
};
|
|
} else {
|
|
*next_chunk = (PostMessageChunkPayload) {
|
|
.offset_bytes = obj->offset_bytes,
|
|
.continuation_is_first = false,
|
|
};
|
|
}
|
|
// Copy the JSON fragment:
|
|
memcpy(next_chunk->chunk_data, &obj->data_buffer[obj->offset_bytes], payload_size);
|
|
|
|
// Move the cursor just like a dict_write_data() call would.
|
|
// app_message_outbox_send() is expecting this!
|
|
it->cursor = (Tuple *)((uint8_t *)it->cursor + sizeof(Tuple) + tuple_data_length);
|
|
|
|
PBL_ASSERTN(s_state.out.msg_type == OutboxMsgTypeNone);
|
|
s_state.out.msg_type = OutboxMsgTypeChunk;
|
|
|
|
DBG("Sending Chunk (%"PRIu32" bytes remaining)", (uint32_t) bytes_remaining);
|
|
PBL_ASSERTN(APP_MSG_OK == app_message_outbox_send());
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Handling Inbound Object Chunks
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void prv_cleanup_inbound_reassembly_buffer(void) {
|
|
task_free(s_state.in.reassembly_buffer);
|
|
s_state.in.reassembly_buffer = NULL;
|
|
}
|
|
|
|
static bool prv_handle_chunk_received(Tuple *tuple) {
|
|
if (tuple->type != TUPLE_BYTE_ARRAY) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Chunk tuple not a byte array!");
|
|
return false;
|
|
}
|
|
|
|
const PostMessageChunkPayload *const chunk = (const PostMessageChunkPayload *) tuple->value;
|
|
if (tuple->length <= sizeof(PostMessageChunkPayload)) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Chunk tuple too short to be valid!");
|
|
return false;
|
|
}
|
|
const size_t payload_size = (tuple->length - sizeof(PostMessageChunkPayload));
|
|
|
|
const bool is_expecting_first = (s_state.in.reassembly_buffer == NULL);
|
|
if (chunk->is_first != is_expecting_first) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Chunk reassembly out of sync! is_first=%u, is_expecting_first=%u",
|
|
chunk->is_first, is_expecting_first);
|
|
return false;
|
|
}
|
|
|
|
if (chunk->is_first) {
|
|
// If this is the first message, allocate buffer:
|
|
uint8_t *const buffer = task_malloc(chunk->total_size_bytes);
|
|
if (!buffer) {
|
|
APP_LOG(APP_LOG_LEVEL_ERROR, "Not enough mem to recv postMessage() of %"PRIu32" bytes",
|
|
(uint32_t) chunk->total_size_bytes);
|
|
// https://pebbletechnology.atlassian.net/browse/PBL-42466
|
|
// TODO: AppMessage NACK the message so the other side can retry later. Not doing this will
|
|
// derail the protocol and thus cause a reset of the session.
|
|
return true; // false would close the session!
|
|
}
|
|
s_state.in.reassembly_buffer = buffer;
|
|
s_state.in.total_size_bytes = chunk->total_size_bytes;
|
|
s_state.in.received_size_bytes = 0;
|
|
} else {
|
|
// If this is not the first message, sanity check the chunk:
|
|
if (s_state.in.received_size_bytes != chunk->offset_bytes) {
|
|
PBL_LOG(LOG_LEVEL_ERROR,
|
|
"Chunk reassembly out of sync! received_size_bytes=%"PRIu32", offset_bytes=%"PRIu32,
|
|
(uint32_t) s_state.in.received_size_bytes, (uint32_t) chunk->offset_bytes);
|
|
return false;
|
|
}
|
|
if (s_state.in.received_size_bytes + payload_size > s_state.in.total_size_bytes) {
|
|
PBL_LOG(LOG_LEVEL_ERROR,
|
|
"Chunk reassembly out of sync! recv_size=%"PRIu32", payload_size=%"PRIu32
|
|
", total_size=%"PRIu32,
|
|
(uint32_t) s_state.in.received_size_bytes, (uint32_t) payload_size,
|
|
(uint32_t) s_state.in.total_size_bytes);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Copy the received buffer over:
|
|
memcpy(s_state.in.reassembly_buffer + s_state.in.received_size_bytes,
|
|
chunk->chunk_data, payload_size);
|
|
s_state.in.received_size_bytes += payload_size;
|
|
|
|
DBG("Received (%"PRIu32" / %"PRIu32" bytes)",
|
|
(uint32_t) s_state.in.received_size_bytes, (uint32_t) s_state.in.total_size_bytes);
|
|
DBG("Payload Size: %"PRIu32, (uint32_t) payload_size);
|
|
|
|
const bool is_last_chunk = (s_state.in.received_size_bytes == s_state.in.total_size_bytes);
|
|
if (is_last_chunk) {
|
|
if (rocky_global_has_event_handlers(ROCKY_EVENT_MESSAGE)) {
|
|
// Last chunk MUST be zero terminated:
|
|
if (s_state.in.reassembly_buffer[s_state.in.total_size_bytes - 1] != '\0') {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Last Chunk MUST be zero-terminated! Dropping msg.");
|
|
} else {
|
|
// Try to parse the received JSON string:
|
|
JS_VAR object = prv_json_parse((const char *)s_state.in.reassembly_buffer);
|
|
if (jerry_value_has_error_flag(object)) {
|
|
rocky_error_print(object);
|
|
} else {
|
|
// Call the app's "message" handler:
|
|
JS_VAR event = rocky_global_create_event(ROCKY_EVENT_MESSAGE);
|
|
jerry_set_object_field(event, ROCKY_EVENT_MESSAGE_DATA, object);
|
|
rocky_global_call_event_handlers(event);
|
|
}
|
|
}
|
|
} else {
|
|
DBG("No 'message' event handlers");
|
|
}
|
|
|
|
prv_cleanup_inbound_reassembly_buffer();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Control Message Queue
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void prv_control_message_queue_pop_head(void) {
|
|
MessageNode *old_head = s_state.out.control_msg_queue;
|
|
list_remove(&old_head->node, (ListNode **)&s_state.out.control_msg_queue, NULL);
|
|
task_free(old_head);
|
|
}
|
|
|
|
static void prv_control_message_queue_send_head(void) {
|
|
MessageNode *node = s_state.out.control_msg_queue;
|
|
DictionaryIterator *it = NULL;
|
|
app_message_outbox_begin(&it);
|
|
if (!it) {
|
|
// FIXME: Handle not being able to open inbox ??
|
|
WTF;
|
|
}
|
|
dict_write_data(it, node->key, node->data, node->length);
|
|
dict_write_end(it);
|
|
|
|
PBL_ASSERTN(s_state.out.msg_type == OutboxMsgTypeNone);
|
|
s_state.out.msg_type = OutboxMsgTypeControl;
|
|
|
|
PBL_ASSERTN(APP_MSG_OK == app_message_outbox_send());
|
|
}
|
|
|
|
static void prv_control_message_queue_add(uint32_t key, const void *data, const size_t length) {
|
|
MessageNode *node = (MessageNode *) task_zalloc_check(sizeof(MessageNode) + length);
|
|
node->key = key;
|
|
node->length = length;
|
|
memcpy(node->data, data, length);
|
|
if (s_state.out.control_msg_queue) {
|
|
list_append((ListNode *)s_state.out.control_msg_queue, (ListNode *)node);
|
|
} else {
|
|
s_state.out.control_msg_queue = node;
|
|
}
|
|
|
|
prv_outbox_try_send_next();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Generic outbox handlers
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static bool prv_is_outbox_busy(void) {
|
|
return (s_state.out.msg_type != OutboxMsgTypeNone ||
|
|
s_state.out.app_msg_retry_timer != EVENTED_TIMER_INVALID_ID);
|
|
}
|
|
|
|
static void prv_outbox_try_send_next(void) {
|
|
if (prv_is_outbox_busy()) {
|
|
return;
|
|
}
|
|
|
|
// Send out the next message. Prioritize control messages over chunk messages:
|
|
if (s_state.out.control_msg_queue) {
|
|
prv_control_message_queue_send_head();
|
|
} else if ((s_state.state == PostMessageStateSessionOpen) &&
|
|
s_state.out.object_queue) {
|
|
prv_object_queue_send_current_chunk();
|
|
}
|
|
}
|
|
|
|
static void prv_outbox_try_send_next_timer_cb(void *unused) {
|
|
s_state.out.app_msg_retry_timer = EVENTED_TIMER_INVALID_ID;
|
|
prv_outbox_try_send_next();
|
|
}
|
|
|
|
static void prv_handle_outbox_result(AppMessageResult reason) {
|
|
// https://pebbletechnology.atlassian.net/browse/PBL-42467
|
|
// TODO: check reason and act upon it
|
|
|
|
const OutboxMsgType sent_msg_type = s_state.out.msg_type;
|
|
s_state.out.msg_type = OutboxMsgTypeNone;
|
|
|
|
const bool is_sent_successfully = (reason == APP_MSG_OK);
|
|
|
|
// Process the (N)ACK:
|
|
switch (sent_msg_type) {
|
|
case OutboxMsgTypeControl: {
|
|
if (is_sent_successfully) {
|
|
prv_control_message_queue_pop_head();
|
|
} else {
|
|
if (s_state.out.failure_count >= CONTROL_MESSAGE_MAX_FAILURES) {
|
|
MessageNode *node = s_state.out.control_msg_queue;
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Failed to send msg with key %"PRIu32, node ? node->key : 0);
|
|
prv_control_message_queue_pop_head();
|
|
} else {
|
|
// Retry happens below by calling prv_control_message_queue_send_head()
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OutboxMsgTypeChunk: {
|
|
if (is_sent_successfully) {
|
|
prv_object_queue_handle_chunk_sent();
|
|
} else {
|
|
if (s_state.out.failure_count >= CHUNK_MESSAGE_MAX_FAILURES) {
|
|
APP_LOG(APP_LOG_LEVEL_WARNING, "Dropping Message.");
|
|
prv_object_queue_pop_head_and_emit_error_event_and_own_json_buffer();
|
|
} else {
|
|
// Retry happens below by calling prv_object_queue_send_current_chunk()
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OutboxMsgTypeNone: {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Got (N)ACK while not expecting any. %u", reason);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
WTF;
|
|
break;
|
|
}
|
|
|
|
// https://pebbletechnology.atlassian.net/browse/PBL-42468
|
|
// Send next, or in case of error reason, delay the retry instead of sending immediately:
|
|
if (is_sent_successfully) {
|
|
prv_outbox_try_send_next();
|
|
} else {
|
|
PBL_ASSERTN(s_state.out.app_msg_retry_timer == EVENTED_TIMER_INVALID_ID);
|
|
s_state.out.app_msg_retry_timer =
|
|
app_timer_register(RETRY_DELAY_MS, prv_outbox_try_send_next_timer_cb, NULL);
|
|
}
|
|
}
|
|
|
|
static void prv_handle_outbox_sent(DictionaryIterator *it, void *context) {
|
|
s_state.out.failure_count = 0;
|
|
prv_handle_outbox_result(APP_MSG_OK);
|
|
}
|
|
|
|
static void prv_handle_outbox_failed(DictionaryIterator *it, AppMessageResult reason, void *ctx) {
|
|
APP_LOG(APP_LOG_LEVEL_WARNING, "Failed to send message: Reason %d", reason);
|
|
++s_state.out.failure_count;
|
|
prv_handle_outbox_result(reason);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Unsupported Protocol
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static bool prv_is_version_supported(const PostMessageResetCompletePayload *rc) {
|
|
const bool is_unsupported = (rc->min_supported_version > POSTMESSAGE_PROTOCOL_MAX_VERSION ||
|
|
rc->max_supported_version < POSTMESSAGE_PROTOCOL_MIN_VERSION);
|
|
if (is_unsupported) {
|
|
// We don't support any of the same versions
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Protocol version unsupported! min=%"PRIu8", max=%"PRIu8,
|
|
rc->min_supported_version, rc->max_supported_version);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void prv_send_unsupported_protocol_error_and_enter_await_reset_req(PostMessageError error) {
|
|
PBL_ASSERTN(s_state.state == PostMessageStateAwaitingResetCompleteRemoteInitiated ||
|
|
s_state.state == PostMessageStateAwaitingResetCompleteLocalInitiated);
|
|
|
|
const PostMessageUnsupportedErrorPayload error_payload = {
|
|
.error_code = error,
|
|
};
|
|
prv_control_message_queue_add(PostMessageKeyUnsupportedError,
|
|
&error_payload, sizeof(error_payload));
|
|
|
|
prv_awaiting_reset_request__enter();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Session Open
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void prv_emit_post_message_connection_event(bool is_connected) {
|
|
const char *event_type = is_connected ? ROCKY_EVENT_CONNECTED : ROCKY_EVENT_DISCONNECTED;
|
|
if (rocky_global_has_event_handlers(event_type)) {
|
|
JS_VAR event = rocky_global_create_event(event_type);
|
|
rocky_global_call_event_handlers(event);
|
|
} else {
|
|
DBG("No handler registered for %s", event_type);
|
|
}
|
|
}
|
|
|
|
static void prv_session_open__enter(const PostMessageResetCompletePayload *rc) {
|
|
PBL_ASSERTN(s_state.state == PostMessageStateAwaitingResetCompleteRemoteInitiated ||
|
|
s_state.state == PostMessageStateAwaitingResetCompleteLocalInitiated);
|
|
|
|
s_state.protocol_version = MIN(rc->max_supported_version, POSTMESSAGE_PROTOCOL_MAX_VERSION);
|
|
// NOTE: Each end communicates it's OWN TX/RX max values.
|
|
// This means that TX max on one end is bound by RX max on the other, and vice versa.
|
|
s_state.tx_chunk_size_bytes = MIN(rc->max_rx_chunk_size, POSTMESSAGE_PROTOCOL_MAX_TX_CHUNK_SIZE);
|
|
s_state.rx_chunk_size_bytes = MIN(rc->max_tx_chunk_size, POSTMESSAGE_PROTOCOL_MAX_RX_CHUNK_SIZE);
|
|
|
|
s_state.state = PostMessageStateSessionOpen;
|
|
|
|
prv_stop_session_closed_object_queue_timer();
|
|
|
|
prv_emit_post_message_connection_event(true /* is_connected */);
|
|
|
|
// Kick the object queue upon entering SessionOpen:
|
|
prv_outbox_try_send_next();
|
|
|
|
DBG("SessionOpen enter");
|
|
}
|
|
|
|
static void prv_session_open__after_exit(void) {
|
|
DBG("After SessionOpen exit");
|
|
prv_cleanup_inbound_reassembly_buffer();
|
|
|
|
if (s_state.out.object_queue) {
|
|
// Make sure we'll re-transfer the object from the start when re-opening:
|
|
OutgoingObject *head = s_state.out.object_queue;
|
|
head->offset_bytes = 0;
|
|
prv_start_session_closed_object_queue_timer();
|
|
}
|
|
|
|
PBL_ASSERTN(s_state.state != PostMessageStateSessionOpen);
|
|
prv_emit_post_message_connection_event(false /* is_connected */);
|
|
}
|
|
|
|
static void prv_session_open__exit_and_initiate_reset(void) {
|
|
prv_awaiting_reset_complete_local_initiated__enter(true /* should_send_reset_request */);
|
|
prv_session_open__after_exit();
|
|
}
|
|
|
|
static void prv_session_open__inbox_received(DictionaryIterator *it, void *context) {
|
|
Tuple *tuple = dict_find(it, PostMessageKeyChunk);
|
|
if (tuple) {
|
|
if (!prv_handle_chunk_received(tuple)) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Resetting because bad Chunk!");
|
|
prv_session_open__exit_and_initiate_reset();
|
|
}
|
|
} else if (dict_find(it, PostMessageKeyResetRequest)) {
|
|
prv_awaiting_reset_complete_remote_initiated_enter();
|
|
prv_session_open__after_exit();
|
|
} else if (dict_find(it, PostMessageKeyResetComplete)) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Resetting because got RC while open");
|
|
prv_session_open__exit_and_initiate_reset();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Awaiting Reset Complete Local Initiated
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void prv_send_reset_request(void) {
|
|
prv_control_message_queue_add(PostMessageKeyResetRequest, NULL, 0);
|
|
}
|
|
|
|
static void prv_awaiting_reset_complete_local_initiated__enter(bool should_send_reset_request) {
|
|
PBL_ASSERTN(s_state.state == PostMessageStateAwaitingResetRequest ||
|
|
s_state.state == PostMessageStateAwaitingResetCompleteRemoteInitiated ||
|
|
s_state.state == PostMessageStateSessionOpen);
|
|
if (should_send_reset_request) {
|
|
prv_send_reset_request();
|
|
}
|
|
s_state.state = PostMessageStateAwaitingResetCompleteLocalInitiated;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Awaiting Reset Complete Remote Initiated
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void prv_send_reset_complete(void) {
|
|
const PostMessageResetCompletePayload payload = {
|
|
.min_supported_version = POSTMESSAGE_PROTOCOL_MIN_VERSION,
|
|
.max_supported_version = POSTMESSAGE_PROTOCOL_MAX_VERSION,
|
|
.max_tx_chunk_size = POSTMESSAGE_PROTOCOL_MAX_TX_CHUNK_SIZE,
|
|
.max_rx_chunk_size = POSTMESSAGE_PROTOCOL_MAX_RX_CHUNK_SIZE,
|
|
};
|
|
prv_control_message_queue_add(PostMessageKeyResetComplete, &payload, sizeof(payload));
|
|
}
|
|
|
|
static void prv_awaiting_reset_complete_remote_initiated_enter(void) {
|
|
PBL_ASSERTN(s_state.state == PostMessageStateAwaitingResetRequest ||
|
|
s_state.state == PostMessageStateAwaitingResetCompleteLocalInitiated ||
|
|
s_state.state == PostMessageStateSessionOpen);
|
|
s_state.state = PostMessageStateAwaitingResetCompleteRemoteInitiated;
|
|
prv_send_reset_complete();
|
|
}
|
|
|
|
static bool prv_is_tuple_valid_reset_complete(Tuple *reset_complete) {
|
|
if (reset_complete->type != TUPLE_BYTE_ARRAY) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "ResetComplete not a byte array! %u", reset_complete->type);
|
|
return false;
|
|
}
|
|
if (reset_complete->length >= sizeof(PostMessageResetCompletePayload)) {
|
|
return true;
|
|
}
|
|
PBL_LOG(LOG_LEVEL_ERROR, "ResetComplete too small! %"PRIu16, reset_complete->length);
|
|
return false;
|
|
}
|
|
|
|
static void prv_awaiting_reset_complete_remote_initiated__inbox_received(DictionaryIterator *it,
|
|
void *context) {
|
|
Tuple *tuple = dict_find(it, PostMessageKeyResetComplete);
|
|
if (tuple) {
|
|
if (!prv_is_tuple_valid_reset_complete(tuple)) {
|
|
// TODO: document this in statechart
|
|
prv_send_unsupported_protocol_error_and_enter_await_reset_req(PostMessageErrorMalformedResetComplete);
|
|
return;
|
|
}
|
|
const PostMessageResetCompletePayload *rc =
|
|
(const PostMessageResetCompletePayload *) tuple->value[0].data;
|
|
// Check overlap in supported versions
|
|
if (!prv_is_version_supported(rc)) {
|
|
// Don't send an error here! The initiating side is supposed have detected the version
|
|
// incompatibility and not sent the ResetComplete (and send an Error message), but apparently
|
|
// we did get the ResetComplete somehow?
|
|
prv_awaiting_reset_request__enter();
|
|
return;
|
|
}
|
|
prv_session_open__enter(rc);
|
|
} else if (dict_find(it, PostMessageKeyResetRequest)) {
|
|
prv_send_reset_complete();
|
|
} else {
|
|
// Anything else (i.e. Chunk), initiate Reset:
|
|
prv_awaiting_reset_complete_local_initiated__enter(true /* should_send_reset_request */);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Awaiting Reset Complete Local Initiated
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
static void prv_awaiting_reset_complete_local_initiated__inbox_received(DictionaryIterator *it,
|
|
void *context) {
|
|
Tuple *tuple = dict_find(it, PostMessageKeyResetComplete);
|
|
if (tuple) {
|
|
if (!prv_is_tuple_valid_reset_complete(tuple)) {
|
|
// TODO: document this in statechart
|
|
prv_send_unsupported_protocol_error_and_enter_await_reset_req(PostMessageErrorMalformedResetComplete);
|
|
return;
|
|
}
|
|
const PostMessageResetCompletePayload *rc =
|
|
(const PostMessageResetCompletePayload *) tuple->value[0].data;
|
|
// Check overlap in supported versions
|
|
if (!prv_is_version_supported(rc)) {
|
|
prv_send_unsupported_protocol_error_and_enter_await_reset_req(PostMessageErrorUnsupportedVersion);
|
|
return;
|
|
}
|
|
prv_send_reset_complete();
|
|
prv_session_open__enter(rc);
|
|
} else if (dict_find(it, PostMessageKeyResetRequest)) {
|
|
prv_awaiting_reset_complete_remote_initiated_enter();
|
|
} else if (dict_find(it, PostMessageKeyChunk)) {
|
|
// Ignore it.
|
|
// https://pebbletechnology.atlassian.net/browse/PBL-42466
|
|
// TODO: NACK the Chunk.
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Awaiting Reset Request
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void prv_awaiting_reset_request__enter(void) {
|
|
PBL_ASSERTN(s_state.state == PostMessageStateDisconnected ||
|
|
s_state.state == PostMessageStateAwaitingResetCompleteLocalInitiated ||
|
|
s_state.state == PostMessageStateAwaitingResetCompleteRemoteInitiated);
|
|
|
|
s_state.state = PostMessageStateAwaitingResetRequest;
|
|
}
|
|
|
|
static void prv_awaiting_reset_request__inbox_received(DictionaryIterator *it, void *context) {
|
|
if (dict_find(it, PostMessageKeyResetRequest)) {
|
|
prv_awaiting_reset_complete_remote_initiated_enter();
|
|
} else {
|
|
// This is not a request message. Drop it and initiate a request
|
|
// https://pebbletechnology.atlassian.net/browse/PBL-42466
|
|
// TODO: This should indicate to the AppMessage layer that it should NACK.
|
|
prv_awaiting_reset_complete_local_initiated__enter(true /* should_send_reset_request */);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// App Message Handling
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static const struct {
|
|
AppMessageInboxReceived inbox_received;
|
|
} s_app_message_handlers[] = {
|
|
[PostMessageStateDisconnected] = {
|
|
.inbox_received = NULL,
|
|
},
|
|
[PostMessageStateAwaitingResetRequest] = {
|
|
.inbox_received = prv_awaiting_reset_request__inbox_received,
|
|
},
|
|
[PostMessageStateAwaitingResetCompleteRemoteInitiated] = {
|
|
.inbox_received = prv_awaiting_reset_complete_remote_initiated__inbox_received,
|
|
},
|
|
[PostMessageStateAwaitingResetCompleteLocalInitiated] = {
|
|
.inbox_received = prv_awaiting_reset_complete_local_initiated__inbox_received,
|
|
},
|
|
[PostMessageStateSessionOpen] = {
|
|
.inbox_received = prv_session_open__inbox_received,
|
|
},
|
|
};
|
|
|
|
static void prv_inbox_received(DictionaryIterator *it, void *context) {
|
|
const AppMessageInboxReceived inbox_rcv = s_app_message_handlers[s_state.state].inbox_received;
|
|
if (inbox_rcv) {
|
|
inbox_rcv(it, context);
|
|
} else {
|
|
DBG_VERBOSE("No inbox_received handler for state %d", s_state.state);
|
|
}
|
|
}
|
|
|
|
static void prv_inbox_dropped(AppMessageResult reason, void *context) {
|
|
// Q: We don't know what got dropped here. Should we send/initiate a ResetRequest?
|
|
// A: No, a drop will be a NACK to the other side, so the other side should retry.
|
|
PBL_LOG(LOG_LEVEL_WARNING, "inbox dropped msg in state %u because %u", s_state.state, reason);
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Object (de)serialization and (de)chunking
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Call the JSON.<function_name> function with the given args, and return the result.
|
|
// Returned jerry_value_t must be released after use.
|
|
static jerry_value_t prv_call_json_function(const char *function_name,
|
|
const jerry_value_t * const args,
|
|
jerry_size_t args_count) {
|
|
JS_VAR JSON = jerry_get_global_builtin((const jerry_char_t *)GLOBAL_JSON);
|
|
PBL_ASSERTN(!jerry_value_is_undefined(JSON));
|
|
|
|
JS_VAR func = jerry_get_object_field(JSON, function_name);
|
|
PBL_ASSERTN(jerry_value_is_function(func));
|
|
|
|
return jerry_call_function(func, JSON, args, args_count);
|
|
}
|
|
|
|
T_STATIC jerry_value_t prv_json_stringify(jerry_value_t object) {
|
|
return prv_call_json_function(GLOBAL_JSON_STRINGIFY, &object, 1);
|
|
}
|
|
|
|
T_STATIC jerry_value_t prv_json_parse(const char *object) {
|
|
JS_VAR string_obj = jerry_create_string_utf8((const jerry_char_t *)object);
|
|
return prv_call_json_function(GLOBAL_JSON_PARSE, &string_obj, 1);
|
|
}
|
|
|
|
#define BOX_SIZE (APP_MESSAGE_OUTBOX_SIZE_MINIMUM)
|
|
#define postMessage_HEADER_SIZE_BYTES (2 * sizeof(Tuple))
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// API: "postmessageerror" event
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void prv_free_json_buffer_associated_with_postmessageerror_event(const uintptr_t ptr) {
|
|
task_free((char *)ptr);
|
|
}
|
|
|
|
JERRY_FUNCTION(prv_postmessageerror_data_getter) {
|
|
char *json_buffer = NULL;
|
|
PBL_ASSERTN(jerry_get_object_native_handle(this_val, (uintptr_t *)&json_buffer));
|
|
PBL_ASSERTN(json_buffer);
|
|
return prv_json_parse(json_buffer);
|
|
}
|
|
|
|
static void prv_object_queue_pop_head_and_emit_error_event_and_own_json_buffer(void) {
|
|
DBG("postmessageerror event");
|
|
OutgoingObject *old_head = s_state.out.object_queue;
|
|
char *const json_data_buffer = old_head->data_buffer;
|
|
|
|
// Don't free the JSON data buffer, ownership is about to be passed to the error event.
|
|
prv_object_queue_pop_head(false /* should_free_data_buffer */);
|
|
|
|
JS_VAR event = rocky_global_create_event(ROCKY_EVENT_ERROR);
|
|
jerry_set_object_native_handle(event, (uintptr_t)json_data_buffer,
|
|
prv_free_json_buffer_associated_with_postmessageerror_event);
|
|
rocky_define_property(event, ROCKY_EVENT_MESSAGE_DATA, prv_postmessageerror_data_getter, NULL);
|
|
rocky_global_call_event_handlers(event);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// API: postMessage()
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void prv_start_session_closed_object_queue_timer(void);
|
|
|
|
static void prv_stop_session_closed_object_queue_timer(void) {
|
|
if (EVENTED_TIMER_INVALID_ID != s_state.out.session_closed_object_queue_timer) {
|
|
AppTimer *t = s_state.out.session_closed_object_queue_timer;
|
|
s_state.out.session_closed_object_queue_timer = EVENTED_TIMER_INVALID_ID;
|
|
app_timer_cancel(t);
|
|
DBG("Cancelled 3s timeout");
|
|
}
|
|
}
|
|
|
|
static void prv_session_closed_object_queue_timer_cb(void *unused) {
|
|
if (EVENTED_TIMER_INVALID_ID == s_state.out.session_closed_object_queue_timer) {
|
|
// handle race: timer was cancelled but event was already in the queue
|
|
// Unfortunately, app_timer_cancel() doesn't tell us about this.
|
|
return;
|
|
}
|
|
|
|
PBL_ASSERTN(s_state.state != PostMessageStateSessionOpen);
|
|
|
|
DBG("Erroring out head object, 3s passed!");
|
|
|
|
s_state.out.session_closed_object_queue_timer = EVENTED_TIMER_INVALID_ID;
|
|
prv_object_queue_pop_head_and_emit_error_event_and_own_json_buffer();
|
|
|
|
if (s_state.out.object_queue) {
|
|
// Still not open and still things in the object queue, restart the timer:
|
|
prv_start_session_closed_object_queue_timer();
|
|
}
|
|
}
|
|
|
|
static void prv_start_session_closed_object_queue_timer(void) {
|
|
PBL_ASSERTN(EVENTED_TIMER_INVALID_ID == s_state.out.session_closed_object_queue_timer);
|
|
|
|
DBG("Starting 3s timeout...");
|
|
|
|
s_state.out.session_closed_object_queue_timer =
|
|
app_timer_register(SESSION_CLOSED_TIMEOUT_MS, prv_session_closed_object_queue_timer_cb, NULL);
|
|
}
|
|
|
|
static jerry_value_t prv_create_oom_error(void) {
|
|
return rocky_error_oom("can't postMessage() -- object too large");
|
|
}
|
|
|
|
static bool prv_object_queue_add(OutgoingObject *msg) {
|
|
if (s_state.out.object_queue) {
|
|
list_append((ListNode *)s_state.out.object_queue, (ListNode *)msg);
|
|
return false;
|
|
} else {
|
|
s_state.out.object_queue = msg;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
JERRY_FUNCTION(prv_post_message) {
|
|
if (argc < 1) {
|
|
return rocky_error_arguments_missing();
|
|
}
|
|
|
|
const jerry_value_t js_msg = argv[0];
|
|
|
|
JS_VAR json_string = prv_json_stringify(js_msg);
|
|
if (jerry_value_has_error_flag(json_string)) {
|
|
return jerry_acquire_value(json_string);
|
|
}
|
|
if (jerry_value_is_undefined(json_string)) {
|
|
// ECMA v5.1, 15.12.3, Note 5: Values that do not have a JSON representation (such as undefined
|
|
// and functions) do not produce a String. Instead they produce the undefined value.
|
|
return rocky_error_unexpected_type(0, "JSON.stringify()-able object");
|
|
}
|
|
|
|
const uint32_t str_size = jerry_get_utf8_string_size(json_string) + 1 /* trailing zero */;
|
|
char *const data_buffer = task_zalloc(str_size);
|
|
if (!data_buffer) {
|
|
return prv_create_oom_error();
|
|
}
|
|
jerry_string_to_utf8_char_buffer(json_string, (jerry_char_t *)data_buffer, str_size);
|
|
|
|
OutgoingObject * const obj = task_zalloc(sizeof(*obj));
|
|
if (!obj) {
|
|
task_free(data_buffer);
|
|
return prv_create_oom_error();
|
|
}
|
|
*obj = (OutgoingObject) {
|
|
.data_buffer = data_buffer,
|
|
};
|
|
|
|
const bool is_first = prv_object_queue_add(obj);
|
|
if (is_first) {
|
|
if (s_state.state == PostMessageStateSessionOpen) {
|
|
prv_outbox_try_send_next();
|
|
} else {
|
|
prv_start_session_closed_object_queue_timer();
|
|
}
|
|
}
|
|
|
|
return jerry_create_undefined();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Rocky boilerplate
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void prv_init_apis(void) {
|
|
memset(&s_state, 0, sizeof(s_state));
|
|
|
|
// Pebble comm session events to transition in & out of PostMessageStateDisconnected
|
|
s_state.comm_session_event_info = (EventServiceInfo) {
|
|
.type = PEBBLE_COMM_SESSION_EVENT,
|
|
.handler = prv_handle_comm_session_event,
|
|
};
|
|
event_service_client_subscribe(&s_state.comm_session_event_info);
|
|
if (sys_app_pp_get_comm_session()) {
|
|
// There is a _small_ race here if a connection occurs async on
|
|
// the BT thread after subscribing but before we check the current state.
|
|
// This could result in us transitioning to a connected state, and then
|
|
// getting an event to transition again.
|
|
// We're guarding against this by ignoring the "duplicate" state change.
|
|
prv_handle_connection();
|
|
}
|
|
|
|
// FIXME: this call can fail if there's not enough memory!
|
|
// Probably best fix this by doing https://pebbletechnology.atlassian.net/browse/PBL-42250
|
|
const size_t overhead = (sizeof(Dictionary) + sizeof(Tuple) + sizeof(PostMessageChunkPayload));
|
|
app_message_open(overhead + POSTMESSAGE_PROTOCOL_MAX_TX_CHUNK_SIZE,
|
|
overhead + POSTMESSAGE_PROTOCOL_MAX_RX_CHUNK_SIZE);
|
|
|
|
app_message_register_inbox_received(prv_inbox_received);
|
|
app_message_register_inbox_dropped(prv_inbox_dropped);
|
|
app_message_register_outbox_sent(prv_handle_outbox_sent);
|
|
app_message_register_outbox_failed(prv_handle_outbox_failed);
|
|
|
|
JS_VAR rocky = rocky_get_rocky_singleton();
|
|
rocky_add_function(rocky, ROCKY_POSTMESSAGE, prv_post_message);
|
|
}
|
|
|
|
static bool prv_free_control_msg_for_each_cb(ListNode *node, void *context) {
|
|
task_free(node);
|
|
return true;
|
|
}
|
|
|
|
static bool prv_free_outbound_object_for_each_cb(ListNode *node, void *context) {
|
|
OutgoingObject *object = (OutgoingObject *)node;
|
|
task_free(object->data_buffer);
|
|
task_free(object);
|
|
return true;
|
|
}
|
|
|
|
static void prv_deinit_apis(void) {
|
|
event_service_client_unsubscribe(&s_state.comm_session_event_info);
|
|
|
|
list_foreach((ListNode *)s_state.out.control_msg_queue, prv_free_control_msg_for_each_cb, NULL);
|
|
list_foreach((ListNode *)s_state.out.object_queue, prv_free_outbound_object_for_each_cb, NULL);
|
|
}
|
|
|
|
//! Unfortunately, we can't use the same path as when normally calling these
|
|
//! handlers since we haven't added it to the event listeners list yet.
|
|
static void prv_call_handler_when_registering(const char *event_name, jerry_value_t handler) {
|
|
JS_VAR event = rocky_global_create_event(event_name);
|
|
rocky_util_call_user_function_and_log_uncaught_error(handler, jerry_create_undefined(),
|
|
&event, 1);
|
|
}
|
|
|
|
static bool prv_handle_callback_registration(const char *event_name, jerry_value_t handler) {
|
|
const bool is_connected = (s_state.state == PostMessageStateSessionOpen);
|
|
bool call_handler = false;
|
|
if (strcmp(ROCKY_EVENT_CONNECTED, event_name) == 0) {
|
|
call_handler = is_connected;
|
|
} else if (strcmp(ROCKY_EVENT_DISCONNECTED, event_name) == 0) {
|
|
call_handler = !is_connected;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
if (call_handler) {
|
|
prv_call_handler_when_registering(event_name, handler);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool prv_add_handler(const char *event_name, jerry_value_t handler) {
|
|
if (strcmp(ROCKY_EVENT_MESSAGE, event_name) == 0 || strcmp(ROCKY_EVENT_ERROR, event_name) == 0) {
|
|
return true;
|
|
}
|
|
|
|
return prv_handle_callback_registration(event_name, handler);
|
|
}
|
|
|
|
const RockyGlobalAPI APP_MESSAGE_APIS = {
|
|
.init = prv_init_apis,
|
|
.deinit = prv_deinit_apis,
|
|
.add_handler = prv_add_handler,
|
|
};
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Unit Test Helpers
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
PostMessageState rocky_api_app_message_get_state(void) {
|
|
return s_state.state;
|
|
}
|
|
|
|
AppTimer *rocky_api_app_message_get_app_msg_retry_timer(void) {
|
|
return s_state.out.app_msg_retry_timer;
|
|
}
|
|
|
|
AppTimer *rocky_api_app_message_get_session_closed_object_queue_timer(void) {
|
|
return s_state.out.session_closed_object_queue_timer;
|
|
}
|