Files
pebble/src/fw/applib/rockyjs/api/rocky_api_app_message.c
T

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;
}