Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson 2024-12-12 16:43:03 -08:00 committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View file

@ -0,0 +1,100 @@
/*
* 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.
*/
#pragma once
#include "applib/data_logging.h"
#include "util/uuid.h"
#include "kernel/pebble_tasks.h"
//! This can be helpful when debugging. It changes the behavior of data logging to send any
//! stored data to the phone immediately after every dls_log()
//! Another helpful testing feature is that doing a long press on any item in the launcher menu will
//! also trigger a flush of all data logging data to the phone
// #define DLS_DEBUG_SEND_IMMEDIATELY
struct DataLoggingSession;
typedef struct DataLoggingSession DataLoggingSession;
//! List of tags used by system services. These are all registered with a uuid of UUID_SYSTEM
typedef enum {
DlsSystemTagAnalyticsDeviceHeartbeat = 78,
DlsSystemTagAnalyticsAppHeartbeat = 79,
DlsSystemTagAnalyticsEvent = 80,
DlsSystemTagActivityMinuteData = 81,
DlsSystemTagActivityAccelSamples = 82,
DlsSystemTagActivitySession = 84,
DlsSystemTagProtobufLogSession = 85,
} DlsSystemTag;
//! Init the data logging service. Called by the system at boot time.
void dls_init(void);
//! Return true if data logging initialized
bool dls_initialized(void);
//! The nuclear option! Clear out all data logging state in memory as well as on the flash storage.
void dls_clear(void);
//! Pause the data logging service
void dls_pause(void);
//! Resume the data logging service.
void dls_resume(void);
//! Find any sessions that the given task may have left in the DataLoggingStatusActive state and
//! moves them forcibly to the inactive state. This may result in data loss if the buffers haven't
//! been flushed to flash yet.
void dls_inactivate_sessions(PebbleTask task);
//! Create a new session using the UUID of the current process. This always creates a buffered
//! session. Unless this is the worker task, buffer must be allocated by the caller and must be at
//! least DLS_SESSION_BUFFER_SIZE bytes large. It will be freed by the data logging service when the
//! session is closed if this method returns no error. The worker task can optionally pass NULL for
//! buffer and the buffer will be allocated in the system heap for it by the data logging service.
DataLoggingSession* dls_create_current_process(uint32_t tag, DataLoggingItemType item_type,
uint16_t item_size, void* buffer, bool resume);
//! Create a new session
DataLoggingSession* dls_create(uint32_t tag, DataLoggingItemType item_type, uint16_t item_size,
bool buffered, bool resume, const Uuid* uuid);
//! Append data to a logging session. Buffered sessions log asynchronously. Non buffered ones block.
DataLoggingResult dls_log(DataLoggingSession *s, const void* data, uint32_t num_items);
//! Finish up a session
void dls_finish(DataLoggingSession *s);
//! Checks to see if this is an actual valid data session.
//! Note that we pass in the logging_session parameter without making sure it's the same. Make sure
//! this function handles passing in random pointers that don't actually point to valid sessions or
//! even valid memory.
bool dls_is_session_valid(DataLoggingSession *logging_session);
//! Triggers data logging to immediately send all stored data to the phone rather than wait for the
//! next regular minute heartbeat. As a testing aid, a long press on any item in the launcher menu
//! calls into this method.
void dls_send_all_sessions(void);
//! Get the send_enable setting
bool dls_get_send_enable(void);
//! Set the send_enable setting for PebbleProtocol
void dls_set_send_enable_pp(bool setting);
//! Set the send_enable setting for Run Level
void dls_set_send_enable_run_level(bool setting);

View file

@ -0,0 +1,80 @@
/*
* 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 "data_logging_service.h"
#include "dls_list.h"
#include "console/prompt.h"
#include <inttypes.h>
static bool command_dls_list_cb(DataLoggingSession *session, void *data) {
char buffer[80];
prompt_send_response_fmt(buffer, sizeof(buffer),
"session_id : %"PRIu8", tag: %"PRIu32", bytes: %"PRIu32", write_offset: %"PRIu32,
session->comm.session_id, session->tag, session->storage.num_bytes,
session->storage.write_offset);
return true;
}
void command_dls_list(void) {
dls_list_for_each_session(command_dls_list_cb, 0);
}
// Unused, comment out to avoid pulling in the string literals
#if 0
void command_dls_show(const char *id) {
uint8_t session_id = strtol(id, NULL, 0);
DataLoggingSession *logging_session = dls_list_find_by_session_id(session_id);
if (logging_session == NULL) {
prompt_send_response("LoggingSession not found");
return;
}
char uuid_b[UUID_STRING_BUFFER_LENGTH];
uuid_to_string(&logging_session->app_uuid, uuid_b);
char buffer[80];
#define WRITE_LINE(fmt, arg) \
prompt_send_response_fmt(buffer, sizeof(buffer), fmt, arg)
WRITE_LINE("session_id %u", logging_session->comm.session_id);
WRITE_LINE("uuid %s", uuid_b);
WRITE_LINE("data state %u", logging_session->status);
WRITE_LINE("tag %lu", logging_session->tag);
WRITE_LINE("item type %u", logging_session->item_type);
WRITE_LINE("item size %u", logging_session->item_size);
WRITE_LINE("storage %lu bytes", logging_session->storage.num_bytes);
WRITE_LINE(" r sector %u", logging_session->storage.read_sector);
WRITE_LINE(" r offset %"PRIu32, logging_session->storage.read_offset);
WRITE_LINE(" w sector %u", logging_session->storage.write_sector);
WRITE_LINE(" w offset %"PRIu32, logging_session->storage.write_offset);
#undef WRITE_LINE
}
#endif
void command_dls_erase_all(void) {
dls_clear();
}
void command_dls_send_all(void) {
// Use this to trigger a send of all data logging data to the phone, helpful for testing
dls_send_all_sessions();
}

View file

@ -0,0 +1,566 @@
/*
* 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 "dls_private.h"
#include "dls_endpoint.h"
#include "dls_list.h"
#include "dls_storage.h"
#include "services/common/analytics/analytics.h"
#include "services/common/comm_session/protocol.h"
#include "services/common/comm_session/session_send_buffer.h"
#include "services/common/system_task.h"
#include "services/common/new_timer/new_timer.h"
#include "services/normal/data_logging/data_logging_service.h"
#include "kernel/pbl_malloc.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/attributes.h"
#include "util/legacy_checksum.h"
#include "util/math.h"
#include <inttypes.h>
#include "FreeRTOS.h"
#include "timers.h"
typedef struct {
ListNode list_node;
DataLoggingSession *session;
// Session metadata to make sure the session pointer corresponds to
// the same session that was added to the reopen list. This guards
// against the session being destroyed and another getting allocated
// to the same address.
Uuid app_uuid;
time_t timestamp;
uint32_t tag;
} DataLoggingReopenEntry;
static struct {
PebbleMutex * mutex;
TimerID ack_timer;
bool report_in_progress;
} s_endpoint_data;
typedef struct PACKED {
uint8_t command;
uint8_t session_id;
} DataLoggingCloseSessionMessage;
typedef struct PACKED {
uint8_t command;
uint8_t session_id;
Uuid app_uuid;
uint32_t timestamp;
uint32_t logging_session_tag;
DataLoggingItemType data_item_type:8;
uint16_t data_item_size;
} DataLoggingOpenSessionMessage;
static const uint16_t ENDPOINT_ID_DATA_LOGGING = 0x1a7a;
#define ACK_NACK_TIMEOUT_TICKS (30 * RTC_TICKS_HZ)
static const uint8_t MAX_NACK_COUNT = 20;
static void reschedule_ack_timeout(void);
static void update_session_state(DataLoggingSession *session, DataLoggingSessionCommState new_state,
bool reschedule) {
session->comm.state = new_state;
switch (new_state) {
case DataLoggingSessionCommStateOpening:
case DataLoggingSessionCommStateSending:
// These states need an ack from the phone.
session->comm.ack_timeout = rtc_get_ticks() + ACK_NACK_TIMEOUT_TICKS;
break;
case DataLoggingSessionCommStateIdle:
session->comm.ack_timeout = 0;
break;
}
if (reschedule) {
reschedule_ack_timeout();
}
}
static void send_timeout_msg(void *session_id_param) {
uint8_t session_id = (uint8_t)(uintptr_t)session_id_param;
CommSession *session = comm_session_get_system_session();
if (!session) {
// timed out because of lost connection
return;
}
DataLoggingSession *logging_session = dls_list_find_by_session_id(session_id);
struct PACKED {
uint8_t command;
uint8_t session_id;
} msg = {
.command = DataLoggingEndpointCmdTimeout,
.session_id = logging_session->comm.session_id,
};
comm_session_send_data(session, ENDPOINT_ID_DATA_LOGGING, (uint8_t *)&msg,
sizeof(msg), COMM_SESSION_DEFAULT_TIMEOUT);
}
static bool check_ack_timeout_for_session(DataLoggingSession *session, void *data) {
RtcTicks *current_ticks = (RtcTicks*) data;
if (session->comm.ack_timeout != 0 && session->comm.ack_timeout <= *current_ticks) {
PBL_LOG(LOG_LEVEL_DEBUG, "session %"PRIu8" timeout", session->comm.session_id);
// Send timeout msg from system task because it could take a while and also require
// more stack space than provided by the timer task.
system_task_add_callback(send_timeout_msg, (void*)(uintptr_t)(session->comm.session_id));
// Set reschedule to false because: 1.) we don't need to reschedule the timer since all
// we did was process one that already expired, 2.) it can cause an infinite recursion
// because reschedule_ack_timeout() will call check_ack_timeout() (which we are already in) if
// any other timers have already expired.
update_session_state(session, DataLoggingSessionCommStateIdle, false /*reschedule*/);
}
return true;
}
static void check_ack_timeout(void) {
RtcTicks current_ticks = rtc_get_ticks();
dls_list_for_each_session(check_ack_timeout_for_session, &current_ticks);
reschedule_ack_timeout();
}
static void ack_timer_cb(void *cb_data) {
mutex_lock(s_endpoint_data.mutex);
check_ack_timeout();
mutex_unlock(s_endpoint_data.mutex);
}
static bool find_soonest_ack_timeout_cb(DataLoggingSession *session, void *data) {
RtcTicks *soonest_ack_timeout = (RtcTicks*) data;
if (session->comm.ack_timeout != 0
&& (session->comm.ack_timeout < *soonest_ack_timeout || *soonest_ack_timeout == 0)) {
*soonest_ack_timeout = session->comm.ack_timeout;
}
return true;
}
static void reschedule_ack_timeout(void) {
RtcTicks soonest_ack_timeout = 0;
dls_list_for_each_session(find_soonest_ack_timeout_cb, &soonest_ack_timeout);
if (soonest_ack_timeout == 0) {
// No one is waiting for ack, just stop the timer
new_timer_stop(s_endpoint_data.ack_timer);
return;
}
RtcTicks current_ticks = rtc_get_ticks();
if (soonest_ack_timeout < current_ticks) {
// Handle the timeout immediately. This will result the in the timer being rescheduled if we're still
// waiting for an ack.
check_ack_timeout();
return;
}
// Convert from ticks to ms for the timer
RtcTicks ticks_until_timeout = soonest_ack_timeout - current_ticks;
uint32_t ms_until_timeout = ((uint64_t) ticks_until_timeout * 1000) / RTC_TICKS_HZ;
bool success = new_timer_start(s_endpoint_data.ack_timer, ms_until_timeout, ack_timer_cb, NULL, 0 /*flags*/);
PBL_ASSERTN(success);
}
static void dls_endpoint_print_message(uint8_t *message, int num_bytes) {
PBL_ASSERTN(message != NULL);
switch (message[0]) {
case DataLoggingEndpointCmdClose:
{
DataLoggingCloseSessionMessage *msg = (DataLoggingCloseSessionMessage *)message;
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Closing session %d", msg->session_id);
break;
}
case DataLoggingEndpointCmdOpen:
{
DataLoggingOpenSessionMessage *msg = (DataLoggingOpenSessionMessage *)message;
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Opening session %u with tag %"PRIu32", type %u, size %hu",
msg->session_id, msg->logging_session_tag, msg->data_item_type, msg->data_item_size);
break;
}
case DataLoggingEndpointCmdData:
{
DataLoggingSendDataMessage *msg = (DataLoggingSendDataMessage *)message;
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Sending data with session_id %"PRIu8", items remaining %"PRIu32", crc 0x%"PRIx32", num_bytes %d",
msg->session_id, msg->items_left_hereafter, msg->crc32, num_bytes);
break;
}
default:
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Message type 0x%x not recognized", message[0]);
}
}
bool dls_endpoint_open_session(DataLoggingSession *session) {
CommSession *comm_session = comm_session_get_system_session();
if (!session) {
return false;
}
DataLoggingOpenSessionMessage msg = {
.command = DataLoggingEndpointCmdOpen,
.session_id = session->comm.session_id,
.app_uuid = session->app_uuid,
.timestamp = session->session_created_timestamp,
.logging_session_tag = session->tag,
.data_item_type = session->item_type,
.data_item_size = session->item_size,
};
dls_endpoint_print_message((uint8_t *)&msg, 0);
update_session_state(session, DataLoggingSessionCommStateOpening, true /*reschedule*/);
return (comm_session_send_data(comm_session, ENDPOINT_ID_DATA_LOGGING,
(uint8_t *)&msg, sizeof(DataLoggingOpenSessionMessage),
COMM_SESSION_DEFAULT_TIMEOUT));
}
void dls_endpoint_close_session(uint8_t session_id) {
CommSession *session = comm_session_get_system_session();
if (!session) {
return;
}
DataLoggingCloseSessionMessage msg = {
.command = DataLoggingEndpointCmdClose,
.session_id = session_id,
};
dls_endpoint_print_message((uint8_t *)&msg, 0);
comm_session_send_data(session, ENDPOINT_ID_DATA_LOGGING,
(uint8_t *)&msg, sizeof(DataLoggingCloseSessionMessage),
COMM_SESSION_DEFAULT_TIMEOUT);
}
bool dls_endpoint_send_data(DataLoggingSession *logging_session, const uint8_t *data,
unsigned int num_bytes) {
if (num_bytes < 1) {
// not sending anything
return true;
}
CommSession *session = comm_session_get_system_session();
if (!session) {
return false;
}
mutex_lock(s_endpoint_data.mutex);
if (logging_session->comm.state != DataLoggingSessionCommStateIdle) {
mutex_unlock(s_endpoint_data.mutex);
// logging_session is waiting for an ack, we'll send next time around
// don't return a failure, this is pretty innocuous.
return true;
}
const uint32_t total_length = sizeof(DataLoggingSendDataMessage) + num_bytes;
const uint32_t timeout_ms = 500;
SendBuffer *sb = comm_session_send_buffer_begin_write(session, ENDPOINT_ID_DATA_LOGGING,
total_length, timeout_ms);
if (!sb) {
mutex_unlock(s_endpoint_data.mutex);
return false;
}
analytics_inc(ANALYTICS_DEVICE_METRIC_DATA_LOGGING_ENDPOINT_SENDS,
AnalyticsClient_System);
const DataLoggingSendDataMessage header = (const DataLoggingSendDataMessage) {
.command = DataLoggingEndpointCmdData,
.session_id = logging_session->comm.session_id,
.items_left_hereafter = 0xffff, // FIXME: logging_session->storage.num_bytes - num_bytes,
.crc32 = legacy_defective_checksum_memory(data, num_bytes),
};
comm_session_send_buffer_write(sb, (const uint8_t *) &header, sizeof(header));
comm_session_send_buffer_write(sb, data, num_bytes);
comm_session_send_buffer_end_write(sb);
dls_endpoint_print_message((uint8_t *) &header, num_bytes);
DLS_HEXDUMP(data, MIN(num_bytes, 64));
logging_session->comm.num_bytes_pending = num_bytes;
update_session_state(logging_session, DataLoggingSessionCommStateSending, true /*reschedule*/);
mutex_unlock(s_endpoint_data.mutex);
unsigned int data_buffer_length = sizeof(DataLoggingSendDataMessage) + num_bytes;
if (!uuid_is_system(&logging_session->app_uuid)) {
analytics_inc_for_uuid(ANALYTICS_APP_METRIC_LOG_OUT_COUNT, &logging_session->app_uuid);
analytics_add_for_uuid(ANALYTICS_APP_METRIC_LOG_BYTE_OUT_COUNT, data_buffer_length, &logging_session->app_uuid);
}
return true;
}
static void prv_dls_endpoint_handle_ack(uint8_t session_id) {
DataLoggingSession *session = dls_list_find_by_session_id(session_id);
if (session == NULL) {
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_WARNING, "Received ack for non-existent session id: %"PRIu8, session_id);
return;
}
mutex_lock(s_endpoint_data.mutex);
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Received ACK for id: %"PRIu8" state: %u", session->comm.session_id, session->comm.state);
switch (session->comm.state) {
case DataLoggingSessionCommStateIdle:
PBL_LOG(LOG_LEVEL_ERROR, "Unexpected ACK");
break;
case DataLoggingSessionCommStateOpening:
update_session_state(session, DataLoggingSessionCommStateIdle, true /*reschedule*/);
break;
case DataLoggingSessionCommStateSending:
session->comm.nack_count = 0;
update_session_state(session, DataLoggingSessionCommStateIdle, true /*reschedule*/);
mutex_unlock(s_endpoint_data.mutex);
// unlock for time consuming activities
dls_storage_consume(session, session->comm.num_bytes_pending);
session->comm.num_bytes_pending = 0;
// the bt session is likely already active so continue to flush data
dls_private_send_session(session, true);
return;
}
mutex_unlock(s_endpoint_data.mutex);
}
static void prv_dls_endpoint_handle_nack(uint8_t session_id) {
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Received NACK for id: %"PRIu8, session_id);
DataLoggingSession *logging_session = dls_list_find_by_session_id(session_id);
if (!logging_session) {
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_WARNING, "Received nack for non-existent session id: %"PRIu8, session_id);
return;
}
mutex_lock(s_endpoint_data.mutex);
switch (logging_session->comm.state) {
case DataLoggingSessionCommStateIdle:
case DataLoggingSessionCommStateOpening:
//Currently, these messages never get NACK'd
PBL_LOG(LOG_LEVEL_ERROR, "Unexpected NACK");
break;
case DataLoggingSessionCommStateSending:
//Maybe queue a resend
logging_session->comm.num_bytes_pending = 0;
if (++logging_session->comm.nack_count > MAX_NACK_COUNT) {
PBL_LOG(LOG_LEVEL_ERROR, "Too many nacks. Flushing...");
dls_storage_consume(logging_session, logging_session->storage.num_bytes);
analytics_inc(ANALYTICS_DEVICE_METRIC_DATA_LOGGING_FLUSH_COUNT, AnalyticsClient_System);
logging_session->comm.nack_count = 0;
}
break;
}
update_session_state(logging_session, DataLoggingSessionCommStateIdle, true /*reschedule*/);
mutex_unlock(s_endpoint_data.mutex);
// reopen the session that was NACK'ed
dls_endpoint_open_session(logging_session);
}
//! System task callback executed which reopens the next session in the list built up by report_cmd_system_task_cb
static void prv_reopen_next_session_system_task_cb(void* data) {
DataLoggingReopenEntry *entry = (DataLoggingReopenEntry *)data;
if (!entry) {
s_endpoint_data.report_in_progress = false;
return;
}
DataLoggingReopenEntry *new_head = (DataLoggingReopenEntry *)list_pop_head((ListNode *)entry);
// Try and reopen this session
bool success = false;
if (dls_list_is_session_valid(entry->session) &&
uuid_equal(&entry->app_uuid, &entry->session->app_uuid) &&
entry->timestamp == entry->session->session_created_timestamp &&
entry->tag == entry->session->tag) {
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Reopening session %d",
entry->session->comm.session_id);
success = (dls_endpoint_open_session(entry->session)
&& dls_private_send_session(entry->session, false));
} else {
// Session has disappeared between the time that the reopen list was
// created and now. This ideally shouldn't happen, but there's a lot
// that's broken about datalogging. See PBL-37078.
success = true;
}
kernel_free(entry);
if (success) {
// Schedule next one
if (new_head) {
bool result = system_task_add_callback(prv_reopen_next_session_system_task_cb, new_head);
PBL_ASSERTN(result);
} else {
s_endpoint_data.report_in_progress = false;
}
} else {
s_endpoint_data.report_in_progress = false;
// If we failed, give up on the remaining ones
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Aborting all remaining open requests");
while (new_head) {
DataLoggingReopenEntry *entry = new_head;
new_head = (DataLoggingReopenEntry *)list_pop_head((ListNode *)new_head);
kernel_free(entry);
}
}
}
//! For use with dls_list_for_each_session. Appends this session to our list of sesions we need to open.
//! On entry, 'data' points to the variable holding the head of the list.
static bool dls_endpoint_add_reopen_sessions_cb(DataLoggingSession *session, void *data) {
DataLoggingReopenEntry **head_ptr = (DataLoggingReopenEntry **)data;
DataLoggingReopenEntry *entry = kernel_malloc_check(sizeof(DataLoggingReopenEntry));
*entry = (DataLoggingReopenEntry) {
.session = session,
.app_uuid = session->app_uuid,
.timestamp = session->session_created_timestamp,
.tag = session->tag,
};
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "adding session %d to reopen list", session->comm.session_id);
*head_ptr = (DataLoggingReopenEntry *)list_insert_before((ListNode *)(*head_ptr), &entry->list_node);
return true;
}
static void prv_handle_report_cmd(const uint8_t *session_ids, size_t num_sessions) {
for (size_t i = 0; i < num_sessions; ++i) {
const uint8_t session_id = session_ids[i];
DataLoggingSession *logging_session = dls_list_find_by_session_id(session_id);
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Phone reported session %u opened", session_id);
// If the phone thinks we're open and we're not, send a close message.
if (logging_session == NULL) {
dls_endpoint_close_session(session_id);
}
}
// If the bluetooth connection is flaky, a session reopen could take a few seconds, so we will chain them
// and only do 1 re-open per system callback so that we don't trigger a watchdog timeout.
DataLoggingReopenEntry *head = NULL;
dls_list_for_each_session(dls_endpoint_add_reopen_sessions_cb, (void *)&head);
// Re-open the first one and reschedule the next one
prv_reopen_next_session_system_task_cb((void *)head);
}
//! Empty a session by session id
static void prv_empty_session(uint8_t session_id) {
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Phone requested empty of session %u",
session_id);
DataLoggingSession *logging_session = dls_list_find_by_session_id(session_id);
if (logging_session) {
dls_private_send_session(logging_session, true /*empty_all_data*/);
}
}
//! data_logging_protocol_msg_callback runs on Bluetooth task. Keep it quick.
void data_logging_protocol_msg_callback(CommSession *session, const uint8_t *data, size_t length) {
// consume the first byte to read the command
uint8_t command = data[0];
--length; // the length now reflects the sizeof the payload
// All commands from the phone have their high bit set.
if ((command & ~DLS_ENDPOINT_CMD_MASK) == 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Invalid data logging endpoint command 0x%x", command);
// TODO: send some error code back?
return;
}
switch (command & DLS_ENDPOINT_CMD_MASK) {
case (DataLoggingEndpointCmdAck):
prv_dls_endpoint_handle_ack(data[1]);
break;
case (DataLoggingEndpointCmdNack):
prv_dls_endpoint_handle_nack(data[1]);
break;
case (DataLoggingEndpointCmdReport):
if (s_endpoint_data.report_in_progress) {
PBL_LOG(LOG_LEVEL_INFO, "Report already in progress");
} else {
s_endpoint_data.report_in_progress = true;
prv_handle_report_cmd(&data[1], length);
}
break;
case (DataLoggingEndpointCmdEmptySession):
prv_empty_session(data[1]);
break;
case (DataLoggingEndpointCmdGetSendEnableReq):
{
bool enabled = dls_get_send_enable();
struct PACKED {
uint8_t command;
uint8_t enabled;
} msg = {
.command = DataLoggingEndpointCmdGetSendEnableRsp,
.enabled = enabled,
};
comm_session_send_data(session, ENDPOINT_ID_DATA_LOGGING, (uint8_t *)&msg,
sizeof(msg), COMM_SESSION_DEFAULT_TIMEOUT);
}
break;
case (DataLoggingEndpointCmdSetSendEnable):
dls_set_send_enable_pp(data[1]);
break;
}
}
void dls_endpoint_init(void) {
s_endpoint_data.mutex = mutex_create();
s_endpoint_data.ack_timer = new_timer_create();
}
static bool prv_handle_disconnect_cb(DataLoggingSession *session, void *data) {
session->comm.state = DataLoggingSessionCommStateIdle;
return true;
}
void dls_private_handle_disconnect(void *data) {
mutex_lock(s_endpoint_data.mutex);
dls_list_for_each_session(prv_handle_disconnect_cb, 0);
mutex_unlock(s_endpoint_data.mutex);
}

View file

@ -0,0 +1,30 @@
/*
* 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.
*/
#pragma once
#include "dls_private.h"
#include <stdint.h>
void dls_endpoint_init(void);
void dls_endpoint_close_session(uint8_t session_id);
bool dls_endpoint_send_data(DataLoggingSession *logging_session, const uint8_t *data, unsigned int num_bytes);
bool dls_endpoint_open_session(DataLoggingSession *logging_session);

View file

@ -0,0 +1,369 @@
/*
* 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 "dls_list.h"
#include "dls_storage.h"
#include "drivers/flash.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "process_management/process_manager.h"
#include "services/common/system_task.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/uuid.h"
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
static DataLoggingSession *s_logging_sessions;
static PebbleRecursiveMutex * s_list_mutex;
// ---------------------------------------------------------------------------------------
// Assert that the current task owns the list mutex
void dls_assert_own_list_mutex(void) {
PBL_ASSERTN(mutex_is_owned_recursive(s_list_mutex));
}
// ---------------------------------------------------------------------------------------
// Lock a session (if active). If session was active, locks it and returns true.
// If session is not active, does no locking and returns false.
//
// Note regarding the list_mutex and the session->data-mutex:
// * session->status can only be read/modified while holding the list mutex
// * session->data->open_count can only be read/modified while holding the list mutex
// and is only available if session->status == DataLoggingStatusActive
// * In order to avoid deadlocks,
// - s_list_mutex MUST be released befored trying to grab session->data->mutex.
// - session->data->open_count must incremented to be > 0 under s_list_mutex before you can
// grab session->data-mutex
// - if you already own session->data-mutex, it is OK to grab s_list_mutex
bool dls_lock_session(DataLoggingSession *session) {
mutex_lock_recursive(s_list_mutex);
if (session->status != DataLoggingStatusActive) {
mutex_unlock_recursive(s_list_mutex);
return false;
}
PBL_ASSERTN(session->data);
// Incrementing open_count insures that no one else can do a dls_unlock_session(inactivate=true)
// on it and cause it to be freed before we grab the session->data->mutex below.
session->data->open_count++;
mutex_unlock_recursive(s_list_mutex);
mutex_lock(session->data->mutex);
return true;
}
// ---------------------------------------------------------------------------------------
// Callback used to free a storage buffer from unprivileged mode.
static void prv_free_storage_buffer_cb(void *p) {
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Freeing buffer storage ptr: %p", p);
task_free(p);
}
// ---------------------------------------------------------------------------------------
static void prv_free_storage_buffer(DataLoggingSession *session) {
if (!session->data->buffer_storage) {
// Skip if no buffer to free.
return;
}
if (session->data->buffer_in_kernel_heap) {
kernel_free(session->data->buffer_storage);
} else {
// The subscriber's buffer was allocated on its unprivileged process heap (app or worker). It
// is unsafe to free it from here in privileged mode because a corrupted heap could crash the
// watch. We will post a callback event to the process's event handler which is executed
// in unprivileged mode.
PebbleEvent e = {
.type = PEBBLE_CALLBACK_EVENT,
.callback = {
.callback = prv_free_storage_buffer_cb,
.data = session->data->buffer_storage
}
};
PebbleTask task = pebble_task_get_current();
process_manager_send_event_to_process(task, &e);
}
}
// ---------------------------------------------------------------------------------------
// Unlock a session previous locked by dls_lock_session(). If inactive is true, this also marks
// the session inactive and frees the memory used for maintaining the active state. See the
// comments above in dls_lock_session() for a description of the locking strategy.
void dls_unlock_session(DataLoggingSession *session, bool inactivate) {
mutex_lock_recursive(s_list_mutex);
PBL_ASSERTN(session->data->open_count > 0);
if (inactivate) {
session->data->inactivate_pending = true;
}
session->data->open_count--;
if (session->data->inactivate_pending && session->data->open_count == 0) {
session->status = DataLoggingStatusInactive;
mutex_unlock_recursive(s_list_mutex);
prv_free_storage_buffer(session);
mutex_unlock(session->data->mutex);
mutex_destroy(session->data->mutex);
kernel_free(session->data);
session->data = NULL;
} else {
mutex_unlock_recursive(s_list_mutex);
mutex_unlock(session->data->mutex);
}
}
// ---------------------------------------------------------------------------------------
// Return session status
DataLoggingStatus dls_get_session_status(DataLoggingSession *session) {
mutex_lock_recursive(s_list_mutex);
DataLoggingStatus status = session->status;
mutex_unlock_recursive(s_list_mutex);
return status;
}
DataLoggingSession *dls_list_find_by_session_id(uint8_t session_id) {
mutex_lock_recursive(s_list_mutex);
DataLoggingSession *iter = s_logging_sessions;
while (iter != NULL) {
if (iter->comm.session_id > session_id) {
break;
}
if (iter->comm.session_id == session_id) {
mutex_unlock_recursive(s_list_mutex);
return (iter);
}
iter = iter->next;
}
mutex_unlock_recursive(s_list_mutex);
return (NULL);
}
DataLoggingSession *dls_list_find_active_session(uint32_t tag, const Uuid *app_uuid) {
mutex_lock_recursive(s_list_mutex);
DataLoggingSession *iter = s_logging_sessions;
while (iter != NULL) {
if (iter->tag == tag && uuid_equal(&(iter->app_uuid), app_uuid)
&& iter->status == DataLoggingStatusActive) {
mutex_unlock_recursive(s_list_mutex);
return (iter);
}
iter = iter->next;
}
mutex_unlock_recursive(s_list_mutex);
return (NULL);
}
void dls_list_remove_session(DataLoggingSession *logging_session) {
if (uuid_is_system(&logging_session->app_uuid)) {
PBL_LOG(LOG_LEVEL_WARNING, "Deleting the system data logging session with tag %"PRIu32,
logging_session->tag);
}
mutex_lock_recursive(s_list_mutex);
DataLoggingSession **iter = &s_logging_sessions;
while (*iter != NULL) {
if (*iter == logging_session) {
*iter = (*iter)->next;
mutex_unlock_recursive(s_list_mutex);
if (logging_session->data) {
mutex_destroy(logging_session->data->mutex);
kernel_free(logging_session->data);
}
kernel_free(logging_session);
return;
}
iter = &((*iter)->next);
}
mutex_unlock_recursive(s_list_mutex);
}
void dls_list_remove_all(void) {
mutex_lock_recursive(s_list_mutex);
DataLoggingSession *cur = s_logging_sessions;
DataLoggingSession *next;
while (cur != NULL) {
next = cur->next;
if (cur->data) {
mutex_destroy(cur->data->mutex);
kernel_free(cur->data);
}
kernel_free(cur);
cur = next;
}
s_logging_sessions = NULL;
mutex_unlock_recursive(s_list_mutex);
}
//! Insert logging session with known id
void dls_list_insert_session(DataLoggingSession *logging_session) {
mutex_lock_recursive(s_list_mutex);
DataLoggingSession **iter = &s_logging_sessions;
for (int i = 0; i < logging_session->comm.session_id; i++) {
if (*iter == NULL) {
break;
}
PBL_ASSERTN(logging_session->comm.session_id != (*iter)->comm.session_id);
if ((*iter)->comm.session_id > logging_session->comm.session_id) {
break;
}
iter = &((*iter)->next);
}
logging_session->next = *iter;
*iter = logging_session;
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Created session: %p id %"PRIu8
" tag %"PRIu32, logging_session, logging_session->comm.session_id, logging_session->tag);
mutex_unlock_recursive(s_list_mutex);
}
// The newlib headers do not expose this because of the __STRICT_ANSI__ define
extern int rand_r(unsigned *seed);
uint8_t dls_list_add_new_session(DataLoggingSession *logging_session) {
// generate random ID
int loops = 0;
uint8_t session_id;
unsigned seed = rtc_get_time() ^ (uintptr_t)&session_id;
do {
// use a custom seed to avoid apps maliciously setting the task-global seed
session_id = rand_r(&seed) % 255;
// FIXME better way to avoid infinite loop? or tune this
PBL_ASSERTN(++loops < 100);
} while (dls_list_find_by_session_id(session_id));
logging_session->comm.session_id = session_id;
// insert in the spool list
dls_list_insert_session(logging_session);
return (session_id);
}
static bool count_session_cb(DataLoggingSession *session, void *data) {
uint32_t *counter = (uint32_t *) data;
++(*counter);
return true;
}
static uint32_t prv_get_num_sessions(void) {
uint32_t counter = 0;
dls_list_for_each_session(count_session_cb, &counter);
return counter;
}
DataLoggingSession *dls_list_create_session(uint32_t tag, DataLoggingItemType type, uint16_t size,
const Uuid *app_uuid, time_t timestamp, DataLoggingStatus status) {
uint32_t num_sessions = prv_get_num_sessions();
if (num_sessions >= DLS_MAX_NUM_SESSIONS) {
PBL_LOG(LOG_LEVEL_WARNING, "Could not allocate additional DataLoggingSession objects");
return NULL;
}
DataLoggingSession *logging_session = kernel_malloc_check(sizeof(DataLoggingSession));
*logging_session = (DataLoggingSession){
.status = status,
.app_uuid = *app_uuid,
.tag = tag,
.task = pebble_task_get_current(),
.item_type = type,
.item_size = size,
.session_created_timestamp = timestamp,
.storage.fd = DLS_INVALID_FILE
};
if (status == DataLoggingStatusActive) {
DataLoggingActiveState *active_state = kernel_malloc_check(sizeof(DataLoggingActiveState));
*active_state = (DataLoggingActiveState){};
active_state->mutex = mutex_create();
logging_session->data = active_state;
}
return (logging_session);
}
DataLoggingSession *dls_list_get_next(DataLoggingSession *cur) {
mutex_lock_recursive(s_list_mutex);
if (cur == NULL) {
// Return the head
mutex_unlock_recursive(s_list_mutex);
return s_logging_sessions;
}
DataLoggingSession *logging_session = ((DataLoggingSession *)cur)->next;
mutex_unlock_recursive(s_list_mutex);
return logging_session;
}
bool dls_list_for_each_session(bool (callback(DataLoggingSession*, void*)), void *data) {
mutex_lock_recursive(s_list_mutex);
DataLoggingSession *logging_session = s_logging_sessions;
while (logging_session != NULL) {
// Read the next pointer first, just in case the callback ends up removing the session.
DataLoggingSession *next_logging_session = logging_session->next;
if (!callback(logging_session, data)) {
mutex_unlock_recursive(s_list_mutex);
return false;
}
logging_session = next_logging_session;
}
mutex_unlock_recursive(s_list_mutex);
return true;
}
void dls_list_init(void) {
s_list_mutex = mutex_create_recursive();
s_logging_sessions = NULL;
}
bool dls_list_is_session_valid(DataLoggingSession *logging_session) {
mutex_lock_recursive(s_list_mutex);
DataLoggingSession *iter = s_logging_sessions;
while (iter != NULL) {
if (iter == logging_session) {
mutex_unlock_recursive(s_list_mutex);
return true;
}
iter = iter->next;
}
mutex_unlock_recursive(s_list_mutex);
return false;
}

View file

@ -0,0 +1,79 @@
/*
* 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.
*/
#pragma once
#include "dls_private.h"
#include "applib/data_logging.h"
#include <stdint.h>
#include <time.h>
DataLoggingSession *dls_list_find_by_session_id(uint8_t session_id);
DataLoggingSession *dls_list_find_active_session(uint32_t tag, const Uuid *app_uuid);
void dls_list_remove_session(DataLoggingSession *logging_session);
//! Deletes all session state in memory without changing the flash state.
void dls_list_remove_all(void);
//! Add logging_session and assign ID
uint8_t dls_list_add_new_session(DataLoggingSession *logging_session);
//! Add logging session with an already assigned ID. Used at startup when restoring previous
//! sessions from flash.
void dls_list_insert_session(DataLoggingSession *logging_session);
//! Creates a new DataLoggingSession object that is only initialized with the parameters given. The
//! session will only be initialized with the given parameters. The .storage and .comm members must
//! be seperately initialized. Also, the resulting object will need to be added to the list of
//! sessions using one of dls_list_add_new_session and dls_list_insert_session. May return NULL if
//! we've created too many sessions.
DataLoggingSession *dls_list_create_session(uint32_t tag, DataLoggingItemType type, uint16_t size,
const Uuid *app_uuid, time_t timestamp,
DataLoggingStatus status);
DataLoggingSession *dls_list_get_next(DataLoggingSession *cur);
void dls_list_rebuild_from_storage(void);
//! Call callback for each session we have. Pass the data param through to the callback each time.
//! If the callback returns false, stop iterating immediately and return false. Returns true
//! otherwise.
typedef bool (*DlsListCallback)(DataLoggingSession*, void*);
bool dls_list_for_each_session(DlsListCallback cb, void *data);
void dls_list_init(void);
//! Checks to see if this is an actual valid data session
//! Note that we pass in the logging_session parameter without making sure it's same. Make sure
//! this function handles passing in random pointers that don't actually point to valid sessions or
//! even valid memory.
bool dls_list_is_session_valid(DataLoggingSession *logging_session);
//! Lock a session (if active). If session was active, locks it and returns true.
//! If session is not active, returns false
bool dls_lock_session(DataLoggingSession *session);
//! Unlock a session previous locked by dls_lock_session()
void dls_unlock_session(DataLoggingSession *session, bool inactivate);
//! Return session status
DataLoggingStatus dls_get_session_status(DataLoggingSession *session);
//! Assert that the current task owns the list mutex
void dls_assert_own_list_mutex(void);

View file

@ -0,0 +1,536 @@
/*
* 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 "applib/data_logging.h"
#include "util/uuid.h"
#include "data_logging_service.h"
#include "dls_endpoint.h"
#include "dls_list.h"
#include "dls_storage.h"
#include "comm/bt_lock.h"
#include "drivers/flash.h"
#include "drivers/rtc.h"
#include "drivers/watchdog.h"
#include "flash_region/flash_region.h"
#include "kernel/pbl_malloc.h"
#include "process_management/pebble_process_md.h"
#include "process_management/process_manager.h"
#include "services/common/analytics/analytics.h"
#include "services/common/comm_session/session.h"
#include "services/common/regular_timer.h"
#include "services/common/system_task.h"
#include "syscall/syscall.h"
#include "system/logging.h"
#include "os/mutex.h"
#include "system/passert.h"
#include "kernel/util/sleep.h"
#include "util/string.h"
#include <string.h>
#include <stdlib.h>
static bool s_initialized = false;
static bool s_sends_enabled_pp = true;
static bool s_sends_enabled_run_level = true;
#define DATALOGGING_DO_FLUSH_CHECK_INTERVAL_MINUTES 5
static bool prv_sends_enabled(void) {
return (s_sends_enabled_run_level && s_sends_enabled_pp);
}
// ----------------------------------------------------------------------------------------
//! Wrapper for data_logging_send_session that makes it usable in dls_list_for_each_session
//! @param empty_all_data A bool that indicates if the session should be force emptied
static bool prv_send_session(DataLoggingSession *logging_session, void *empty_all_data) {
dls_private_send_session(logging_session, (bool) empty_all_data);
return true;
}
//! @param empty_all_data A bool that indicates if the session should be force emptied
static void prv_send_all_sessions_system_task_cb(void *empty_all_data) {
dls_list_for_each_session(prv_send_session, (void*) empty_all_data);
}
// ----------------------------------------------------------------------------------------
//! @param data unused
static void prv_check_all_sessions_timer_cb(void *data) {
// If sends are not enabled, do nothing
if (!prv_sends_enabled()) {
PBL_LOG(LOG_LEVEL_INFO, "Not sending sessions beause sending is disabled");
return;
}
// We regularly check all our sessions to see if we have any data to send. Normally we want to
// avoid sending the data unless there's a lot of data spooled up. This allows us to reduce the
// number of times we have to send data for each session by batching it up into larger, fewer
// messages. However, occasionally we do want to flush everything out.
static int check_counter = 0;
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "send all sessions: empty %s connected %s counter %u",
bool_to_str(check_counter == 0),
bool_to_str(comm_session_get_system_session() != NULL),
check_counter);
system_task_add_callback(prv_send_all_sessions_system_task_cb,
(void*)(uintptr_t)(check_counter == 0));
// force a flush every 15 minutes
static const int EMPTY_ALL_SESSIONS_INTERVAL_MINUTES =
15 / DATALOGGING_DO_FLUSH_CHECK_INTERVAL_MINUTES;
check_counter = (check_counter + 1) % EMPTY_ALL_SESSIONS_INTERVAL_MINUTES;
}
// ----------------------------------------------------------------------------------------
static RegularTimerInfo prv_check_all_sessions_timer_info = {
.cb = prv_check_all_sessions_timer_cb
};
// ----------------------------------------------------------------------------------------
static void prv_remove_logging_session(DataLoggingSession *data) {
DataLoggingSession *logging_session = (DataLoggingSession *)data;
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Removing session %d.",
logging_session->comm.session_id);
dls_endpoint_close_session(logging_session->comm.session_id);
dls_storage_delete_logging_storage(logging_session);
dls_list_remove_session(logging_session);
}
// ----------------------------------------------------------------------------------------
// Grab the next chunk of bytes out of the session's storage and send it to the mobile
// Returns false on unexpected errors, else true
bool dls_private_send_session(DataLoggingSession *logging_session, bool empty) {
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
// If sends are not enabled, ignore
if (!prv_sends_enabled()) {
PBL_LOG(LOG_LEVEL_INFO, "Not sending session beause sending is disabled");
return true;
}
// Only attempt to send data out if we can communicate with the phone
if (comm_session_get_system_session() == NULL) {
return true;
}
int32_t total_bytes = logging_session->storage.num_bytes;
bool inactive = (dls_get_session_status(logging_session) == DataLoggingStatusInactive);
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG,
"de-logging session %"PRIu8", tag %"PRIu32
" (inactive %s tot_bytes %"PRIu32" empty %s)",
logging_session->comm.session_id, logging_session->tag,
bool_to_str(inactive), total_bytes, bool_to_str(empty));
if (inactive && (total_bytes == 0)) {
prv_remove_logging_session(logging_session);
return true;
} else if (!empty && !inactive && (total_bytes < 8000)) {
return true; // nothing to flush yet
}
bool success = false;
uint8_t *buffer = kernel_malloc_check(DLS_ENDPOINT_MAX_PAYLOAD);
unsigned int num_bytes = DLS_ENDPOINT_MAX_PAYLOAD;
PBL_ASSERTN(logging_session->item_size <= num_bytes);
num_bytes -= (num_bytes % logging_session->item_size);
uint32_t new_read_offset;
int32_t read_bytes = dls_storage_read(logging_session, buffer, num_bytes, &new_read_offset);
if (read_bytes < 0) {
goto exit;
}
PBL_ASSERTN((uint32_t)read_bytes <= DLS_ENDPOINT_MAX_PAYLOAD);
unsigned int leftover_bytes = read_bytes % logging_session->item_size;
if (leftover_bytes) {
PBL_LOG(LOG_LEVEL_ERROR, "leftover bytes in the session. Flushing...");
// remove the number of leftover bytes so we fall back on our feet
read_bytes -= leftover_bytes;
dls_storage_consume(logging_session, leftover_bytes);
analytics_inc(ANALYTICS_DEVICE_METRIC_DATA_LOGGING_FLUSH_COUNT, AnalyticsClient_System);
}
success = dls_endpoint_send_data(logging_session, buffer, read_bytes);
exit:
kernel_free(buffer);
return (success);
}
// ----------------------------------------------------------------------------------------
void dls_pause(void) {
regular_timer_remove_callback(&prv_check_all_sessions_timer_info);
}
// ----------------------------------------------------------------------------------------
void dls_resume(void) {
regular_timer_add_multiminute_callback(&prv_check_all_sessions_timer_info,
DATALOGGING_DO_FLUSH_CHECK_INTERVAL_MINUTES);
}
// ----------------------------------------------------------------------------------------
void dls_init(void) {
dls_endpoint_init();
dls_list_init();
// rebuild data logging_sessions
dls_storage_rebuild();
// add callbacks to empty and check logging_sessions
dls_resume();
s_initialized = true;
}
// ----------------------------------------------------------------------------------------
bool dls_initialized(void) {
return s_initialized;
}
// ----------------------------------------------------------------------------------------
void dls_clear(void) {
dls_list_remove_all();
dls_storage_invalidate_all();
}
// ----------------------------------------------------------------------------------------
// Get the send_enable setting
bool dls_get_send_enable(void) {
return prv_sends_enabled();
}
// ----------------------------------------------------------------------------------------
// Set the send_enable setting
void dls_set_send_enable_pp(bool setting) {
s_sends_enabled_pp = setting;
}
// ----------------------------------------------------------------------------------------
// Set the send_enable setting
void dls_set_send_enable_run_level(bool setting) {
s_sends_enabled_run_level = setting;
}
// ----------------------------------------------------------------------------------------
// Callback used by dls_inactivate_sessions.
static bool prv_inactivate_sessions_each_cb(DataLoggingSession *session, void *data) {
// Note that s_list_mutex is already owned because this is called from
// dls_list_for_each_session(), so we CANNOT (and don't need to) call dls_lock_session() from
// here because that could result in a deadlock (see comments in dls_lock_session).
dls_assert_own_list_mutex();
if (session->status != DataLoggingStatusActive) {
// Already inactive
return true;
}
PebbleTask task = (PebbleTask)(uintptr_t)data;
// System data logging sessions are responsible for killing themselves
Uuid system_uuid = UUID_SYSTEM;
if (!uuid_equal(&session->app_uuid, &system_uuid)) {
if (task == session->task) {
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Inactivating session: %"PRIu8,
session->comm.session_id);
// Free the buffer if it's in kernel heap. If not in kernel heap we are intentionally not
// freeing the data->buffer_storage because it was allocated on the client's heap, and the
// client is being destroyed.
if (session->data->buffer_in_kernel_heap) {
kernel_free(session->data->buffer_storage);
}
// All the lock/unlock session calls are made from privileged mode, so it is impossible
// for the task to exit with the session locked (open_count > 0)
PBL_ASSERTN(session->data->open_count == 0);
session->status = DataLoggingStatusInactive;
// Free up the data and mutex for this session
mutex_destroy(session->data->mutex);
kernel_free(session->data);
session->data = NULL;
}
}
return true;
}
// ----------------------------------------------------------------------------------------
void dls_send_all_sessions(void) {
// If sends are not enabled, do nothing
if (!prv_sends_enabled()) {
PBL_LOG(LOG_LEVEL_INFO, "Not sending sessions beause sending is disabled");
return;
}
system_task_add_callback(prv_send_all_sessions_system_task_cb, (void*) true);
}
// ----------------------------------------------------------------------------------------
// Mark all sessions belonging to 'task' as inactive so that no more data can be added to them.
// They will only be deleted after the endpoint finishes sending the data to the mobile.
void dls_inactivate_sessions(PebbleTask task) {
dls_list_for_each_session(prv_inactivate_sessions_each_cb, (void *)(uintptr_t)task);
}
// ----------------------------------------------------------------------------------------
static DataLoggingSession *prv_dls_create(uint32_t tag, DataLoggingItemType item_type,
uint16_t item_size, bool buffered, void *buffer,
bool resume, const Uuid* uuid) {
// validate size parameter
if (item_size == 0 || (buffered && item_size > DLS_SESSION_MAX_BUFFERED_ITEM_SIZE)
|| (!buffered && item_size > DLS_ENDPOINT_MAX_PAYLOAD)) {
PBL_LOG(LOG_LEVEL_ERROR, "invalid logging_session item size, %d", item_size);
return (NULL);
} else if (item_type == DATA_LOGGING_UINT || item_type == DATA_LOGGING_INT) {
if (item_size > 4 || item_size == 3) {
PBL_LOG(LOG_LEVEL_ERROR, "Invalid data width: integer types can be 1, 2, or 4 bytes");
return (NULL);
}
}
DataLoggingSession *logging_session = dls_list_find_active_session(tag, uuid);
if (!resume && logging_session != NULL) {
dls_finish(logging_session);
logging_session = NULL;
}
if (logging_session == NULL) {
logging_session = dls_list_create_session(tag, item_type, item_size, uuid, rtc_get_time(),
DataLoggingStatusActive);
if (logging_session == NULL) {
// No need to log again here, dls_list_create_session will log on our behalf
return NULL;
}
// Add to the linked list of logging_sessions. This assigns a new unique session_id to this
// session.
dls_list_add_new_session(logging_session);
if (buffered) {
uint32_t buf_size = DLS_SESSION_MIN_BUFFER_SIZE;
// Allocate the buffer if the caller didn't
if (!buffer) {
// Workers are allowed to allocate the buffer storage in the system heap because
// they have such limited memory
PebbleTask task = pebble_task_get_current();
PBL_ASSERTN(task == PebbleTask_Worker || task == PebbleTask_KernelMain ||
task == PebbleTask_KernelBackground);
buffer = kernel_malloc_check(buf_size);
logging_session->data->buffer_in_kernel_heap = true;
}
logging_session->data->buffer_storage = buffer;
shared_circular_buffer_init(&logging_session->data->buffer,
logging_session->data->buffer_storage, buf_size);
shared_circular_buffer_add_client(&logging_session->data->buffer,
&logging_session->data->buffer_client);
} else {
// non buffered sessions can only be created/used from KernelBG
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
}
}
// send an open message
dls_endpoint_open_session(logging_session);
return (logging_session);
}
// ----------------------------------------------------------------------------------------
DataLoggingSession* dls_create(uint32_t tag, DataLoggingItemType item_type, uint16_t item_size,
bool buffered, bool resume, const Uuid* uuid) {
return prv_dls_create(tag, item_type, item_size, buffered, NULL /*buffer*/, resume, uuid);
}
// ----------------------------------------------------------------------------------------
DataLoggingSession* dls_create_current_process(uint32_t tag, DataLoggingItemType item_type,
uint16_t item_size, void* buffer, bool resume) {
const PebbleProcessMd *md = sys_process_manager_get_current_process_md();
return prv_dls_create(tag, item_type, item_size, true, buffer, resume, &md->uuid);
}
// ----------------------------------------------------------------------------------------
void dls_finish(DataLoggingSession *logging_session) {
PBL_ASSERTN(logging_session != NULL);
if (uuid_is_system(&logging_session->app_uuid)) {
PBL_LOG(LOG_LEVEL_WARNING, "Finishing the system data logging session at %p", logging_session);
}
bool is_active = dls_lock_session(logging_session);
if (!is_active) {
PBL_LOG(LOG_LEVEL_WARNING, "Tried to close a non-active data logging session");
return;
}
// Wait for write buffer to empty
int timeout = 1000; // 1 second
while (logging_session->data->buffer_storage != NULL && timeout) {
int bytes_pending = shared_circular_buffer_get_read_space_remaining(
&logging_session->data->buffer, &logging_session->data->buffer_client);
if (bytes_pending == 0) {
break;
}
// There's still bytes in the circular buffer that haven't been persisted to flash yet. Just
// unlock and wait a little bit, since the system task should be busy writing these to flash.
dls_unlock_session(logging_session, false /*inactivate*/);
timeout -= 10;
psleep(10);
if (!dls_lock_session(logging_session)) {
// Someone snuck in and marked it inactive on us
goto exit;
}
}
if (timeout <= 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Timed out waiting for logging_session to write");
}
dls_unlock_session(logging_session, true /*inactivate*/);
exit:
dls_send_all_sessions();
}
// ----------------------------------------------------------------------------------------
static bool prv_write_session_to_flash(DataLoggingSession* session, void *data) {
dls_storage_write_session(session);
return true;
}
static void prv_write_all_sessions_to_flash(void *data) {
dls_list_for_each_session(prv_write_session_to_flash, NULL);
}
// ----------------------------------------------------------------------------------------
DataLoggingResult dls_log(DataLoggingSession *session, const void* data, uint32_t num_items) {
#if !RELEASE
// TODO: We should be able to remove this requirement once PBL-23925 is fixed
//
// Some datalogging code holds the dls_list.c:s_list_mutex while taking the
// bt_lock. Since we are locking the list and then trying to get the bt_lock,
// any other thread which holds the bt_lock and then trys to call a log could
// result in a deadlock (since dls_lock_session() uses the list mutex). For non-release
// builds assert when this happens so we can catch the cases and fix them.
bt_lock_assert_held(false);
#endif
PBL_ASSERTN(session != NULL && data != NULL);
DataLoggingResult result = DATA_LOGGING_SUCCESS;
if (num_items == 0 || data == NULL) {
return (DATA_LOGGING_INVALID_PARAMS);
}
uint32_t num_bytes = num_items * session->item_size;
if (session->data->buffer_storage && num_bytes > DLS_SESSION_MAX_BUFFERED_ITEM_SIZE) {
return (DATA_LOGGING_INVALID_PARAMS);
}
bool active = dls_lock_session(session);
if (!active) {
return (DATA_LOGGING_CLOSED);
}
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "logging %d items of size %d to session %d",
(int)num_items, (int)session->item_size, session->comm.session_id);
if (!session->data->buffer_storage) {
// Unbuffered, we can write to storage immediately
if (!dls_storage_write_data(session, data, num_bytes)) {
// We always overwrite old data, so the only possibility for failure here is an internal
// PFS error.
result = DATA_LOGGING_INTERNAL_ERR;
}
goto unlock_and_exit;
}
if (shared_circular_buffer_get_write_space_remaining(&session->data->buffer) < num_bytes) {
result = DATA_LOGGING_BUSY;
goto unlock_and_exit;
}
shared_circular_buffer_write(&session->data->buffer, data, num_bytes,
false /*advance_slackers*/);
// Only enqueue work on the system_task if we're not already waiting on the system task to handle
// previously enqueued work for this session.
if (!session->data->write_request_pending) {
session->data->write_request_pending = true;
system_task_add_callback(prv_write_all_sessions_to_flash, NULL);
}
unlock_and_exit:
dls_unlock_session(session, false /*inactive*/);
#ifdef DLS_DEBUG_SEND_IMMEDIATELY
dls_send_all_sessions();
#endif
return (result);
}
// ----------------------------------------------------------------------------------------
bool dls_is_session_valid(DataLoggingSession *logging_session) {
return dls_list_is_session_valid(logging_session);
}
// ----------------------------------------------------------------------------------------
// These methods provided for unit tests
int dls_test_read(DataLoggingSession *logging_session, uint8_t *buffer, int num_bytes) {
uint32_t new_read_offset;
return (dls_storage_read(logging_session, buffer, num_bytes, &new_read_offset));
}
int dls_test_consume(DataLoggingSession *logging_session, int num_bytes) {
dls_storage_consume(logging_session, num_bytes);
return (num_bytes);
}
int dls_test_get_num_bytes(DataLoggingSession *logging_session) {
return (logging_session->storage.num_bytes);
}
int dls_test_get_tag(DataLoggingSession *logging_session) {
return (logging_session->tag);
}
uint8_t dls_test_get_session_id(DataLoggingSession *logging_session) {
return (logging_session->comm.session_id);
}

View file

@ -0,0 +1,235 @@
/*
* 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.
*/
#pragma once
#include "applib/data_logging.h"
#include "drivers/rtc.h"
#include "flash_region/flash_region.h"
#include "kernel/pebble_tasks.h"
#include "os/mutex.h"
#include "services/common/comm_session/protocol.h"
#include "system/hexdump.h"
#include "util/attributes.h"
#include "util/shared_circular_buffer.h"
#include "util/units.h"
#include "util/uuid.h"
#include <stdint.h>
#include <stdlib.h>
#include <time.h>
#define DLS_HEXDUMP(data, length) \
PBL_HEXDUMP_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, data, length)
// File name is formatted as: ("%s%d", DLS_FILE_NAME_PREFIX, session_id)
#define DLS_FILE_NAME_PREFIX "dls_storage_"
static const uint32_t DLS_FILE_NAME_MAX_LEN = 20;
static const uint32_t DLS_FILE_INIT_SIZE_BYTES = KiBYTES(4);
// Limits on how much free space we try to reserver for a session file
static const uint32_t DLS_MIN_FILE_FREE_BYTES = KiBYTES(8);
static const uint32_t DLS_MAX_FILE_FREE_BYTES = KiBYTES(100);
// Min amount of available space at the end of a file before we decide to grow it
static const uint32_t DLS_MIN_FREE_BYTES = KiBYTES(1);
// Max # of sessions we allow
static const uint32_t DLS_MAX_NUM_SESSIONS = 20;
// Maximum total amount of storage we are allowed to use on the file system.
static const uint32_t DLS_TOTAL_STORAGE_BYTES = KiBYTES(640);
// Maximum amount of space allowed for data over and above the minimum allotment per session
#define DLS_MAX_DATA_BYTES (DLS_TOTAL_STORAGE_BYTES \
- (DLS_MAX_NUM_SESSIONS * DLS_FILE_INIT_SIZE_BYTES))
typedef enum {
//! A session is active when it's first created and it's still being logged to.
DataLoggingStatusActive = 0x01,
//! A session is inactive when we have data to spool to the phone but the app that created the
//! session has since closed or the app has closed it by calling dls_finish.
DataLoggingStatusInactive = 0x02,
} DataLoggingStatus;
// Endpoint commands
typedef enum {
DataLoggingEndpointCmdOpen = 0x01,
DataLoggingEndpointCmdData = 0x02,
DataLoggingEndpointCmdClose = 0x03,
DataLoggingEndpointCmdReport = 0x04,
DataLoggingEndpointCmdAck = 0x05,
DataLoggingEndpointCmdNack = 0x06,
DataLoggingEndpointCmdTimeout = 0x07,
DataLoggingEndpointCmdEmptySession = 0x08,
DataLoggingEndpointCmdGetSendEnableReq = 0x09,
DataLoggingEndpointCmdGetSendEnableRsp = 0x0A,
DataLoggingEndpointCmdSetSendEnable = 0x0B,
} DataLoggingEndpointCmd;
//! Every command starts off with a 8-bit command byte. Commands from the phone will have their
//! top bit set, where commands from watch will have the top bit cleared. See
//! DataLoggingEndpointCmd for the values of the other 7 bits.
static const uint8_t DLS_ENDPOINT_CMD_MASK = 0x7f;
#define DLS_INVALID_FILE (-1)
typedef struct DataLoggingSessionStorage {
//! Handle to the pfs file we are using. Set to DLS_INVALID_FILE if no storage yet
int fd;
//! Which byte offset in the file we are writing to
uint32_t write_offset;
//! Which byte offset in the file we are reading from
uint32_t read_offset;
//! Number of unread bytes in storage
uint32_t num_bytes;
} DataLoggingSessionStorage;
// Our little comm state machine...
//
// +----------+ Rx Ack +----------+ Tx Data +----------+
// | Opening |----------->| Idle |+------------>| Sending |
// +----------+ +----------+ +----------+
// ^ |
// | Rx Ack |
// +-------------------------+
typedef enum {
//! The session is opening and waiting for the phone to acknowledge our open command.
DataLoggingSessionCommStateOpening,
//! The session is idle, ready to send data
DataLoggingSessionCommStateIdle,
//! The session has sent data to the phone and is waiting for an ack
DataLoggingSessionCommStateSending,
} DataLoggingSessionCommState;
typedef struct {
//! A session ID that is chosen by the watch and is unique to all the session IDs that the
//! watch knows about.
uint8_t session_id;
DataLoggingSessionCommState state:8;
//! The number of times this session got nacked
uint8_t nack_count;
//! How many bytes we've sent to the phone that haven't been acked yet.
int num_bytes_pending;
//! The time in RtcTicks at which the current state will timeout while waiting for an ack. Set
//! to zero if we're not waiting for one.
RtcTicks ack_timeout;
} DataLoggingSessionComm;
//! Information needed while a session is active (watch app still adding more data).
typedef struct {
PebbleMutex *mutex;
SharedCircularBuffer buffer; //! A data buffer
SharedCircularBufferClient buffer_client;
uint8_t *buffer_storage; //! Storage for the buffer
//! true if buffer_storage is in kernel heap, else it's in dls_create() caller's heap
bool buffer_in_kernel_heap:1;
//! bool used to rate control how often we ask the system task to write us out to flash.
bool write_request_pending:1;
//! bool used to record the fact that a session should be inactivated once it is unlocked
//! (by dls_unlock_session())
bool inactivate_pending:1;
//! Incremented/decremented under global list mutex. This structure can only be freed up when
//! this reaches 0.
uint8_t open_count;
} DataLoggingActiveState;
//! Data logging session metadata, struct in memory
typedef struct DataLoggingSession {
// FIXME use a ListNode instead of this custom list
struct DataLoggingSession *next; //!< The next logging_session in the linked list
Uuid app_uuid;
uint32_t tag;
PebbleTask task;
DataLoggingItemType item_type:4;
DataLoggingStatus status:4;
uint16_t item_size;
// A timestamp of when this session was first created.
time_t session_created_timestamp;
DataLoggingSessionComm comm;
DataLoggingSessionStorage storage;
//! This pointer only allocated for active sessions
DataLoggingActiveState *data;
} DataLoggingSession;
bool dls_private_send_session(DataLoggingSession *logging_session, bool empty);
//! Must be called on the system task
//! @param data unused
void dls_private_handle_disconnect(void *data);
//! Get/Set the current send_enable setting
bool dls_private_get_send_enable(void);
void dls_private_set_send_enable(bool setting);
typedef struct PACKED {
uint8_t command;
uint8_t session_id;
uint32_t items_left_hereafter;
uint32_t crc32;
uint8_t bytes[];
} DataLoggingSendDataMessage;
//! Size of the buffer we create for buffered sessions. This is the largest item size allowed
//! for buffered sessions.
static const uint32_t DLS_SESSION_MAX_BUFFERED_ITEM_SIZE = 300;
//! Size of the buffer we create for buffered sessions. This must be 1 bigger than
//! DLS_SESSION_MAX_BUFFERED_ITEM_SIZE because we build a circular buffer out of it
#define DLS_SESSION_MIN_BUFFER_SIZE (DLS_SESSION_MAX_BUFFERED_ITEM_SIZE + 1)
//! Max payload we can send when we send logging data to the phone. This is the largest item
//! size allowed for non-buffered sessions.
static const uint32_t DLS_ENDPOINT_MAX_PAYLOAD = (COMM_MAX_OUTBOUND_PAYLOAD_SIZE
- sizeof(DataLoggingSendDataMessage));
//! Unit tests only
int dls_test_read(DataLoggingSession *logging_session, uint8_t *buffer, int num_bytes);
//! Unit tests only
int dls_test_consume(DataLoggingSession *logging_session, int num_bytes);
//! Unit tests only
int dls_test_get_num_bytes(DataLoggingSession *logging_session);
//! Unit tests only
int dls_test_get_tag(DataLoggingSession *logging_session);
//! Unit tests only
uint8_t dls_test_get_session_id(DataLoggingSession *logging_session);

View file

@ -0,0 +1,999 @@
/*
* 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 "data_logging_service.h"
#include "dls_storage.h"
#include "dls_list.h"
#include "drivers/flash.h"
#include "kernel/pbl_malloc.h"
#include "kernel/pebble_tasks.h"
#include "kernel/util/sleep.h"
#include "services/common/analytics/analytics.h"
#include "services/normal/filesystem/pfs.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/attributes.h"
#include "util/math.h"
#include "util/string.h"
#include <inttypes.h>
#include <stddef.h>
#include <stdio.h>
typedef enum {
DLS_VERSION_0 = 0x20,
} DLSFileHeaderVersion;
static const DLSFileHeaderVersion DLS_CURRENT_VERSION = DLS_VERSION_0;
// Set while executing dls_storage_rebuild() which is called from dls_init() during boot time
// When set, we allow storage accesses from KernelMain whereas normally, only KernelBG is allowed.
static bool s_initializing_storage = false;
// Each session stores data in a separate pfs file with this data in the front. The file name
// is constructed as ("%s%d", DLS_FILE_NAME_PREFIX, comm_session_id)
typedef struct PACKED {
DLSFileHeaderVersion version:8;
uint8_t comm_session_id;
uint32_t timestamp;
uint32_t tag;
Uuid app_uuid;
DataLoggingItemType item_type:8;
uint16_t item_size;
} DLSFileHeader;
// We organize data in the file into chunks with this header at the front of each chunk.
// This allows us to mark chunks as already read by setting the valid bit to 0 after we
// successfully read it out. This is necessary to keep track of read chunks in the file system
// so that we can recover our read position after a reboot.
#define DLS_CHUNK_HDR_NUM_BYTES_UNINITIALIZED 0x7f
typedef struct PACKED {
//! The number of data bytes after this header, not including this header. If this value
//! is DLS_CHUNK_HDR_NUM_BYTES_UNINITIALIZED (all bits set), it means no data follows.
uint8_t num_bytes:7;
bool valid:1; // Set to false after chunk is consumed.
} DLSChunkHeader;
// The most we try to fit into a data chunk. This value must be small enough to fit within the 7
// bytes reserved for it within the DLSChunkHeader.
#define DLS_MAX_CHUNK_SIZE_BYTES 100
_Static_assert(DLS_MAX_CHUNK_SIZE_BYTES < DLS_CHUNK_HDR_NUM_BYTES_UNINITIALIZED,
"DLS_MAX_CHUNK_SIZE_BYTES must be less than DLS_CHUNK_HDR_NUM_BYTES_UNINITIALIZED");
// Forward declarations
static bool prv_realloc_storage(DataLoggingSession *session, uint32_t new_size);
static bool prv_get_session_file(DataLoggingSession *session, uint32_t space_needed);
static void prv_release_session_file(DataLoggingSession *session);
// ----------------------------------------------------------------------------------------
static void prv_assert_valid_task(void) {
if (!s_initializing_storage) {
// The s_initializing_storage is only set during boot, when dls_storage_rebuild() is called.
// That is the only time we allow a task (KernelMain) other than KernelBG access to the storage
// functions.
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
}
}
// -----------------------------------------------------------------------------------------
static void prv_get_filename(char *name, DataLoggingSession *session) {
concat_str_int(DLS_FILE_NAME_PREFIX, session->comm.session_id, name, DLS_FILE_NAME_MAX_LEN);
}
// ----------------------------------------------------------------------------------------
// Logs if an error occurs, returns true on success
static bool prv_pfs_read(int fd, void *buf, size_t size) {
int bytes_read;
bytes_read = pfs_read(fd, buf, size);
if (bytes_read < S_SUCCESS) {
PBL_LOG(LOG_LEVEL_ERROR, "Err %d while reading", (int)bytes_read);
return false;
} else if (bytes_read < (int)size) {
PBL_LOG(LOG_LEVEL_ERROR, "Read only %d bytes, expected %d", (int)bytes_read, (int)size);
return false;
}
return true;
}
// ----------------------------------------------------------------------------------------
// Logs if an error occurs, returns true on success
static bool prv_pfs_write(int fd, void *buf, size_t size) {
int bytes_wrote = pfs_write(fd, buf, size);
if (bytes_wrote < S_SUCCESS) {
PBL_LOG(LOG_LEVEL_ERROR, "Err %d while writing", (int)bytes_wrote);
return false;
} else if (bytes_wrote < (int)size) {
PBL_LOG(LOG_LEVEL_ERROR, "Wrote only %d bytes, expected %d", (int)bytes_wrote, (int)size);
return false;
}
return true;
}
// ----------------------------------------------------------------------------------------
// Logs if an error occurs, returns true on success
static bool prv_pfs_seek(int fd, int offset, FSeekType seek_type) {
int result;
result = pfs_seek(fd, offset, seek_type);
if (result < S_SUCCESS) {
PBL_LOG(LOG_LEVEL_ERROR, "Err %d while seeking", result);
return false;
}
return true;
}
// ----------------------------------------------------------------------------------------
// Logs if an error occurs, returns true on success
static size_t prv_pfs_get_file_size(int fd) {
size_t result = pfs_get_file_size(fd);
if (result == 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Err getting size");
}
return result;
}
// -----------------------------------------------------------------------------------------
// Callback passed to pfs_iterate_files. Used to find data logging files by name
static bool prv_filename_filter_cb(const char *name) {
const int prefix_len = strlen(DLS_FILE_NAME_PREFIX);
return (strncmp(name, DLS_FILE_NAME_PREFIX, prefix_len) == 0);
}
// -----------------------------------------------------------------------------------------
// Given a session pointer, return how much larger we want to grow the file for it if/when
// we decide to reallocate it
static uint32_t prv_get_desired_free_bytes(DataLoggingSession *session) {
// By default, we will make the file 50% larger than the currently used number of bytes
uint32_t free_bytes = session->storage.num_bytes / 2;
// Set some lower and upper bounds
free_bytes = CLIP(free_bytes, DLS_MIN_FILE_FREE_BYTES, DLS_MAX_FILE_FREE_BYTES);
return free_bytes;
}
// ----------------------------------------------------------------------------------------
static bool prv_accumulate_size_cb(DataLoggingSession* session, void *data) {
uint32_t *size_p = (uint32_t *)data;
if (session->storage.write_offset != 0) {
if (prv_get_session_file(session, 0)) {
*size_p += prv_pfs_get_file_size(session->storage.fd);
prv_release_session_file(session);
}
}
return true;
}
// ----------------------------------------------------------------------------------------
// Get total amount of space we have allocated from the file system. This is the sum of the
// file sizes of all the DLS files.
static uint32_t prv_get_total_file_system_bytes(void) {
uint32_t size = 0;
dls_list_for_each_session(prv_accumulate_size_cb, &size);
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Total used space: %d", (int)size);
return size;
}
// ----------------------------------------------------------------------------------------
// Compact all storage files. Used to free up space for new data.
static bool prv_compact_session_cb(DataLoggingSession* session, void *data) {
if (session->storage.write_offset == 0) {
// The write offset is 0 if we've never created storage for this session.
return true;
}
if (!prv_get_session_file(session, 0)) {
// We couldn't open up this storage file. Since we are just compacting where we can,
// just return true so that we go on to the next session in the list.
return true;
}
size_t cur_size = prv_pfs_get_file_size(session->storage.fd);
uint32_t target_free_bytes = prv_get_desired_free_bytes(session);
if ((cur_size > 0) && (session->storage.num_bytes + target_free_bytes < cur_size)) {
// We have more than the desired number of free bytes in this file, so it is a candidate
// for compaction.
uint32_t new_size = session->storage.num_bytes + target_free_bytes;
new_size = MAX(new_size, DLS_FILE_INIT_SIZE_BYTES);
prv_release_session_file(session);
if (new_size != cur_size || session->storage.read_offset > sizeof(DLSFileHeader)) {
prv_realloc_storage(session, new_size);
}
} else {
prv_release_session_file(session);
}
return true;
}
// -----------------------------------------------------------------------------------------
// Make sure there is at least 'needed' bytes available in our file system space
// allowed for data logging
static bool prv_make_file_system_space(uint32_t needed) {
// Make sure we have at least 'needed' bytes free in the file system
uint32_t used_space = prv_get_total_file_system_bytes();
if (used_space + needed >= DLS_MAX_DATA_BYTES) {
dls_list_for_each_session(prv_compact_session_cb, NULL);
used_space = prv_get_total_file_system_bytes();
if (used_space + needed >= DLS_MAX_DATA_BYTES) {
return false;
}
}
return true;
}
// -----------------------------------------------------------------------------------------
// Open an existing or create a new storage file. If the write_offset in the storage structure
// is 0, then write a new file header based on the info from the given session.
static bool prv_open_file(DataLoggingSessionStorage *storage, uint8_t op_flags,
int32_t size, DataLoggingSession *session) {
// Open/Create the file
char name[DLS_FILE_NAME_MAX_LEN];
prv_get_filename(name, session);
int fd = pfs_open(name, op_flags, FILE_TYPE_STATIC, size);
if (fd < S_SUCCESS) {
PBL_LOG(LOG_LEVEL_ERROR, "Could not open/create DLS file %s", name);
return false;
}
if (storage->write_offset != 0) {
storage->fd = fd;
return true;
}
DLSFileHeader hdr = (DLSFileHeader) {
.version = DLS_CURRENT_VERSION,
.comm_session_id = session->comm.session_id,
.timestamp = session->session_created_timestamp,
.tag = session->tag,
.app_uuid = session->app_uuid,
.item_type = session->item_type,
.item_size = session->item_size
};
// Write the header
if (!prv_pfs_write(fd, &hdr, sizeof(hdr))) {
pfs_close_and_remove(fd);
return false;
}
// Init the storage struct
*storage = (DataLoggingSessionStorage) {
.fd = fd,
.write_offset = sizeof(hdr),
.read_offset = sizeof(hdr)
};
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Created session-storage: "
"id %"PRIu8", filename: %s, fd: %d, size: %d", session->comm.session_id, name, fd,
(int)size);
return true;
}
// -----------------------------------------------------------------------------------------
// Close the session file
static void prv_release_session_file(DataLoggingSession *session) {
PBL_ASSERTN(session->storage.fd != DLS_INVALID_FILE);
status_t status = pfs_close(session->storage.fd);
if (status != S_SUCCESS) {
PBL_LOG(LOG_LEVEL_ERROR, "Error %d closing file for session %d", (int)status,
session->comm.session_id);
}
session->storage.fd = DLS_INVALID_FILE;
}
// -----------------------------------------------------------------------------------------
// Open the session file, creating it if necessary. If space_needed is > 0, then make sure there is
// enough space in the file to add 'space_needed' more bytes, compacting, growing, or lopping off
// older data in the file if necessary. Returns true if there is enough space and the file was
// successfully opened.
// If space_needed is 0 (generally used when reading only), we just attempt to open the file and
// don't check the available space for writing.
// If this method returns true, the caller must eventually close the file using
// prv_release_session_file().
static bool prv_get_session_file(DataLoggingSession *session, uint32_t space_needed) {
bool success = false;
PBL_ASSERTN(session->storage.fd == DLS_INVALID_FILE);
// Open/create the file
// We always reserve enough space to create a file of DLS_FILE_INIT_SIZE_BYTES, so no need to
// check the quota (see calculation of DLS_MAX_DATA_BYTES).
if (!prv_open_file(&session->storage, OP_FLAG_WRITE | OP_FLAG_READ, DLS_FILE_INIT_SIZE_BYTES,
session)) {
return false;
}
if (space_needed == 0) {
// If no extra space needed, we are done because we successfully opened the file.
return true;
}
// Get the file size
size_t file_size = prv_pfs_get_file_size(session->storage.fd);
if (file_size == 0) {
prv_release_session_file(session);
return false;
}
// Add a minium buffer to needed. This gives us a little insurance and also allows for the
// extra space needed for the chunk header byte that occurs at least once evvery
// DLS_MAX_CHUNK_SIZE_BYTES bytes.
space_needed += DLS_MIN_FREE_BYTES;
uint32_t space_avail = file_size - session->storage.write_offset;
if (space_needed <= space_avail) {
// We have enough space
return true;
}
uint32_t min_delta_size = space_needed - space_avail;
// The remaining strategies rely on reallocating the file, so we need to close it first
prv_release_session_file(session);
// If we can free up space by reallocating this file, try that next. Since we are
// reallocating anyways, take this chance to optimize the amount of free space in the file.
uint32_t target_file_size = session->storage.num_bytes + space_needed
+ prv_get_desired_free_bytes(session);
target_file_size = MAX(target_file_size, DLS_FILE_INIT_SIZE_BYTES);
uint32_t optimum_delta_size = target_file_size - file_size;
bool have_space_to_grow = prv_make_file_system_space(optimum_delta_size);
if (!have_space_to_grow) {
// If we don't have enough space to grow to our optimum size, see if growing to fill whatever
// free space is left in the file system is sufficient.
uint32_t total_allocated_bytes = prv_get_total_file_system_bytes();
if (total_allocated_bytes + min_delta_size <= DLS_MAX_DATA_BYTES) {
target_file_size = DLS_MAX_DATA_BYTES - total_allocated_bytes + file_size;
have_space_to_grow = true;
}
}
if (have_space_to_grow) {
if (!prv_realloc_storage(session, target_file_size)) {
goto exit;
}
success = prv_open_file(&session->storage, OP_FLAG_WRITE | OP_FLAG_READ,
DLS_FILE_INIT_SIZE_BYTES, session);
if (success) {
goto exit;
}
}
// Lop off old data at the beginning of the file if there is enough there.
success = false;
// If we are going to consume, we have to be prepared to consume at least 1 data chunk
min_delta_size = MAX(min_delta_size, DLS_MAX_CHUNK_SIZE_BYTES);
if (session->storage.num_bytes <= min_delta_size) {
// Lopping off the used bytes won't satisfy space_needed
goto exit;
}
uint32_t consume_bytes = MAX(session->storage.num_bytes/2, min_delta_size);
if (dls_storage_consume(session, consume_bytes) < 0) {
// We failed to lop off the used bytes
goto exit;
}
// Reallocate which removes the consumed bytes from the beginning of the file.
if (!prv_realloc_storage(session, file_size)) {
goto exit;
}
// Re-open it now
success = prv_open_file(&session->storage, OP_FLAG_WRITE | OP_FLAG_READ, DLS_FILE_INIT_SIZE_BYTES,
session);
exit:
// If success, double-check that we have enough space.
if (success && space_needed > 0) {
file_size = prv_pfs_get_file_size(session->storage.fd);
PBL_ASSERTN(session->storage.write_offset + space_needed <= file_size);
}
return success;
}
// -----------------------------------------------------------------------------------------
static bool prv_write_data(DataLoggingSessionStorage *storage, const void *data,
uint32_t remaining_bytes) {
const uint8_t *data_ptr = data;
// Write out in chunks
while (remaining_bytes > 0) {
uint8_t data_chunk_length = MIN(DLS_MAX_CHUNK_SIZE_BYTES, remaining_bytes);
// Write the data first, so if an error occurs, the header is left in the uninitialized state
if (!prv_pfs_seek(storage->fd, storage->write_offset + sizeof(DLSChunkHeader), FSeekSet)) {
return false;
}
if (!prv_pfs_write(storage->fd, (void *)data_ptr, data_chunk_length)) {
return false;
}
// Write data chunk header now
if (!prv_pfs_seek(storage->fd, storage->write_offset, FSeekSet)) {
return false;
}
DLSChunkHeader data_hdr = { .num_bytes = data_chunk_length, .valid = true };
if (!prv_pfs_write(storage->fd, &data_hdr, sizeof(DLSChunkHeader))) {
return false;
}
// Bump pointer and count
storage->write_offset += data_chunk_length + sizeof(DLSChunkHeader);
storage->num_bytes += data_chunk_length;
remaining_bytes -= data_chunk_length;
data_ptr += data_chunk_length;
}
return true;
}
// -----------------------------------------------------------------------------------------
// Migrate a session's data to a new file, removing already consumed bytes from the front
static bool prv_realloc_storage(DataLoggingSession *session, uint32_t new_size) {
bool success;
uint8_t *tmp_buf = NULL;
// Record in metrics
analytics_inc(ANALYTICS_DEVICE_METRIC_DATA_LOGGING_REALLOC_COUNT, AnalyticsClient_System);
// Must be called with the file closed
PBL_ASSERTN(session->storage.fd == DLS_INVALID_FILE);
PBL_LOG(LOG_LEVEL_INFO, "Compacting storage for session %d", session->comm.session_id);
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG,
"Before compaction: num_bytes: %"PRIu32", write_offset:%"PRIu32,
session->storage.num_bytes, session->storage.write_offset);
// Init a storage struct and create a new file for the compacted data
DataLoggingSessionStorage new_storage = {
.fd = DLS_INVALID_FILE,
};
success = prv_open_file(&new_storage, OP_FLAG_OVERWRITE | OP_FLAG_READ, new_size,
session);
if (!success) {
PBL_LOG(LOG_LEVEL_ERROR, "Could not create temporary file to migrate storage file");
goto exit;
}
// Copy data in chunks from the old file to the new one. Things go faster with a bigger buffer.
success = false;
// We have to make sure we have at least 1 delineated item within each DLS_ENDPOINT_MAX_PAYLOAD
// bytes and clipping max_chunk_size to DLS_ENDPOINT_MAX_PAYLOAD insures that. If we didn't clip
// it and the item size was 645 for example, we might pack 2 items back to back in storage
// using DLS_MAX_CHUNK_SIZE_BYTES (100) byte chunks and dls_private_send_session() wouldn't be
// able to get a complete single item because we wrote 1290 bytes using DLS_MAX_CHUNK_SIZE_BYTES
// byte chunks and there is no chunk boundary at the 645 byte offset.
int32_t max_chunk_size = DLS_ENDPOINT_MAX_PAYLOAD;
while (true) {
tmp_buf = kernel_malloc(max_chunk_size);
if (tmp_buf) {
break;
}
if (max_chunk_size < 256) {
PBL_LOG(LOG_LEVEL_ERROR, "Not enough memory for reallocation");
goto exit;
}
max_chunk_size /= 2;
}
int32_t bytes_to_copy = session->storage.num_bytes;
while (bytes_to_copy) {
uint32_t new_read_offset;
int32_t bytes_read = dls_storage_read(session, tmp_buf, MIN(max_chunk_size, bytes_to_copy),
&new_read_offset);
if (bytes_read <= 0) {
goto exit;
}
// Write to new file
if (!prv_write_data(&new_storage, tmp_buf, bytes_read)) {
goto exit;
}
// Consume out of old one now.
if (dls_storage_consume(session, bytes_read) < 0) {
goto exit;
}
bytes_to_copy -= bytes_read;
}
// We successfully transferred the unread data to the new storage, place it into the session
// info
pfs_close(session->storage.fd);
// Close the new file now. That will finish up the swap for us
pfs_close(new_storage.fd);
new_storage.fd = DLS_INVALID_FILE;
// Plug in the new storage info into the session
session->storage = new_storage;
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG,
"After compaction: size: %d, num_bytes: %d, write_offset:%d",
(int)new_size, (int)session->storage.num_bytes, (int)session->storage.write_offset);
success = true;
exit:
kernel_free(tmp_buf);
if (new_storage.fd != DLS_INVALID_FILE) {
pfs_close_and_remove(new_storage.fd);
}
if (!success) {
PBL_LOG(LOG_LEVEL_ERROR, "Migration failed of session file %d", session->comm.session_id);
dls_storage_delete_logging_storage(session);
}
return success;
}
// -----------------------------------------------------------------------------------------
void dls_storage_invalidate_all(void) {
// Iterate through all files in the file system, looking for all DLS storage files and
// deleting them.
pfs_remove_files(prv_filename_filter_cb);
}
// -----------------------------------------------------------------------------------------
void dls_storage_delete_logging_storage(DataLoggingSession *session) {
prv_assert_valid_task();
PBL_ASSERTN(session->storage.fd == DLS_INVALID_FILE);
char name[DLS_FILE_NAME_MAX_LEN];
prv_get_filename(name, session);
status_t status = pfs_remove(name);
if (status != S_SUCCESS) {
PBL_LOG(LOG_LEVEL_ERROR, "Error %d removing file", (int) status);
}
// Clear out storage info
session->storage = (DataLoggingSessionStorage) {
.fd = DLS_INVALID_FILE,
};
}
// -----------------------------------------------------------------------------------------
// Write data directly to flash. Called from dls_log() when the session is
// unbuffered. Assumes the caller has already locked the session using dls_lock_session().
bool dls_storage_write_data(DataLoggingSession *session, const void *data, uint32_t num_bytes) {
prv_assert_valid_task();
bool success = false;
bool got_session_file = prv_get_session_file(session, num_bytes);
if (!got_session_file) {
goto exit;
}
success = prv_write_data(&session->storage, data, num_bytes);
exit:
if (got_session_file) {
prv_release_session_file(session);
}
if (!success) {
PBL_LOG(LOG_LEVEL_ERROR, "Nuking storage for session %d", session->comm.session_id);
dls_storage_delete_logging_storage(session);
}
return success;
}
// -----------------------------------------------------------------------------------------
// Copy data out of a session's circular buffer and write it to flash. Called from a KernelBG
// system task callback triggered by dls_log() after data is added to a buffered session.
bool dls_storage_write_session(DataLoggingSession *session) {
prv_assert_valid_task();
bool success = true;
bool got_session_file = false;
// Note that s_list_mutex is already owned because this is called from
// dls_list_for_each_session(), so we CANNOT (and don't need to) call dls_lock_session() from
// here because that could result in a deadlock (see comments in dls_lock_session).
dls_assert_own_list_mutex();
if (session->status != DataLoggingStatusActive) {
// Not active
return true;
}
// If this session is not buffered, there is no circular buffer to move data out of, it would
// have been written directly to flash during dls_log. But, we can end up here because of a
// call to prv_write_all_sessions_to_flash() which iterates through ALL active sessions.
if (!session->data->buffer_storage) {
goto exit;
}
session->data->write_request_pending = false;
int bytes_remaining = shared_circular_buffer_get_read_space_remaining(
&session->data->buffer, &session->data->buffer_client);
if (bytes_remaining == 0) {
goto exit;
}
got_session_file = prv_get_session_file(session, bytes_remaining);
if (!got_session_file) {
success = false;
goto exit;
}
while (bytes_remaining > 0) {
const uint8_t* read_ptr;
uint16_t bytes_read;
success = shared_circular_buffer_read(&session->data->buffer,
&session->data->buffer_client,
bytes_remaining, &read_ptr, &bytes_read);
PBL_ASSERTN(success);
success = prv_write_data(&session->storage, read_ptr, bytes_read);
if (!success) {
goto exit;
}
shared_circular_buffer_consume(&session->data->buffer, &session->data->buffer_client,
bytes_read);
bytes_remaining -= bytes_read;
}
exit:
if (got_session_file) {
prv_release_session_file(session);
}
if (!success) {
PBL_LOG(LOG_LEVEL_ERROR, "Nuking storage for session %d", session->comm.session_id);
dls_storage_delete_logging_storage(session);
}
return success;
}
// -----------------------------------------------------------------------------------------
// Special case: if buffer is NULL, just doesn't perform any reads, it just returns the # of bytes
// of data available for reading. Returns -1 on error.
// On exit, *new_read_offset contains the new read_offset
int32_t dls_storage_read(DataLoggingSession *logging_session, uint8_t *buffer, int32_t num_bytes,
uint32_t *new_read_offset) {
prv_assert_valid_task();
int32_t read_bytes = 0;
int32_t last_whole_items_read_bytes = 0;
bool got_session_file = false;
if (logging_session->storage.write_offset == 0) {
// no data available for this session
goto exit;
}
got_session_file = prv_get_session_file(logging_session, 0);
if (!got_session_file) {
last_whole_items_read_bytes = -1; // error
goto exit;
}
uint32_t read_offset = logging_session->storage.read_offset;
while (!buffer || read_bytes < num_bytes) {
DLSChunkHeader chunk_hdr;
// Reached the end of the file?
// NOTE: we don't do this check if we are scanning for the last written byte (buffer == NULL)
if (buffer) {
if (read_offset >= logging_session->storage.write_offset) {
break;
}
}
// Read the chunk header
if (!prv_pfs_seek(logging_session->storage.fd, read_offset, FSeekSet)) {
last_whole_items_read_bytes = -1;
goto exit;
}
if (!prv_pfs_read(logging_session->storage.fd, &chunk_hdr, sizeof(chunk_hdr))) {
last_whole_items_read_bytes = -1;
goto exit;
}
// Reached the end of the valid data?
if (chunk_hdr.valid && chunk_hdr.num_bytes == DLS_CHUNK_HDR_NUM_BYTES_UNINITIALIZED) {
break;
}
// Valid data?
if (chunk_hdr.valid) {
if (buffer) {
if (chunk_hdr.num_bytes + read_bytes > num_bytes) {
// Not enough room in buffer to read next chunk.
break;
}
if (!prv_pfs_read(logging_session->storage.fd, buffer, chunk_hdr.num_bytes)) {
last_whole_items_read_bytes = -1;
goto exit;
}
read_bytes += chunk_hdr.num_bytes;
buffer += chunk_hdr.num_bytes;
} else {
// Just scanning for the last written byte
read_bytes += chunk_hdr.num_bytes;
}
}
read_offset += sizeof(chunk_hdr) + chunk_hdr.num_bytes;
// Did we reach a whole item boundary? If so, update our "last_whole_item" bookkeeping now
if ((read_bytes % logging_session->item_size) == 0) {
last_whole_items_read_bytes = read_bytes;
*new_read_offset = read_offset;
}
}
exit:
if (got_session_file) {
prv_release_session_file(logging_session);
}
if (last_whole_items_read_bytes < 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Nuking storage for session %d", logging_session->comm.session_id);
dls_storage_delete_logging_storage(logging_session);
}
return last_whole_items_read_bytes;
}
// -----------------------------------------------------------------------------------------
// Consume num_bytes of data. As a special case, if num_bytes is 0, this simply advances the
// internal storage.read_offset to match the # of bytes already consumed without consuming any more.
// This special mode is only used by dls_storage_rebuild() when we are resurrecting old
// sessions from the file system.
int32_t dls_storage_consume(DataLoggingSession *logging_session, int32_t num_bytes) {
prv_assert_valid_task();
int32_t consumed_bytes = 0;
bool got_session_file = false;
if (logging_session->storage.write_offset == 0) {
// no data available for this session
goto exit;
}
got_session_file = prv_get_session_file(logging_session, 0);
if (!got_session_file) {
consumed_bytes = -1; // error
goto exit;
}
bool reset_read_offset = (num_bytes == 0);
while (reset_read_offset || consumed_bytes < num_bytes) {
DLSChunkHeader chunk_hdr;
// Reached the end of the file?
if (logging_session->storage.read_offset >= logging_session->storage.write_offset) {
break;
}
if (!prv_pfs_seek(logging_session->storage.fd, logging_session->storage.read_offset,
FSeekSet)) {
consumed_bytes = -1; // error
goto exit;
}
if (!prv_pfs_read(logging_session->storage.fd, &chunk_hdr, sizeof(chunk_hdr))) {
consumed_bytes = -1; // error
goto exit;
}
if (chunk_hdr.valid && chunk_hdr.num_bytes == DLS_CHUNK_HDR_NUM_BYTES_UNINITIALIZED) {
// End of valid data
break;
}
if (chunk_hdr.valid) {
if (reset_read_offset) {
// If we are only resetting the read offset, break out now.
break;
}
if (chunk_hdr.num_bytes > num_bytes) {
// Somehow the caller tried to consume less than they read?
PBL_LOG(LOG_LEVEL_WARNING, "Read/consume out of sync");
goto exit;
}
// Invalidate the chunk, now that we have consumed it
chunk_hdr.valid = false;
if (!prv_pfs_seek(logging_session->storage.fd, logging_session->storage.read_offset,
FSeekSet)) {
consumed_bytes = -1; // error
goto exit;
}
if (!prv_pfs_write(logging_session->storage.fd, &chunk_hdr, sizeof(chunk_hdr))) {
consumed_bytes = -1; // error
goto exit;
}
if (logging_session->storage.num_bytes < chunk_hdr.num_bytes) {
PBL_LOG(LOG_LEVEL_ERROR, "Inconsistent tracking of num_bytes");
consumed_bytes = -1; // error
goto exit;
}
logging_session->storage.num_bytes -= chunk_hdr.num_bytes;
}
logging_session->storage.read_offset += sizeof(DLSChunkHeader) + chunk_hdr.num_bytes;
consumed_bytes += chunk_hdr.num_bytes;
}
exit:
if (consumed_bytes > 0) {
PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG,
"Consumed %d bytes from session %d", (int)consumed_bytes,
logging_session->comm.session_id);
}
if (got_session_file) {
prv_release_session_file(logging_session);
}
if (consumed_bytes < 0) {
PBL_LOG(LOG_LEVEL_ERROR, "Nuking storage for session %d", logging_session->comm.session_id);
dls_storage_delete_logging_storage(logging_session);
}
return consumed_bytes;
}
// -----------------------------------------------------------------------------------------
// Called from dls_init() during boot time to scan for existing DLS storage files in the file
// system and recreate sessions from them.
void dls_storage_rebuild(void) {
char name[DLS_FILE_NAME_MAX_LEN];
// This disables the checks that verify that only KernelBG is accessing the storage files.
// dls_storage_rebuild() is called from KernelMain during boot.
s_initializing_storage = true;
// Iterate through all files in the file system, looking for DLS storage files by name
PFSFileListEntry *dir_list = pfs_create_file_list(prv_filename_filter_cb);
// Create a session for each entry
PFSFileListEntry *head = dir_list;
int num_sessions_restored = 0;
while (head) {
DataLoggingSession *session = NULL;
int fd = pfs_open(head->name, OP_FLAG_READ | OP_FLAG_WRITE, FILE_TYPE_STATIC,
DLS_FILE_INIT_SIZE_BYTES);
if (fd < S_SUCCESS) {
PBL_LOG(LOG_LEVEL_ERROR, "Error %d opening file %s", fd, head->name);
pfs_remove(head->name);
goto bad_session;
}
// Get the session info
DLSFileHeader hdr;
if (!prv_pfs_read(fd, &hdr, sizeof(hdr))) {
pfs_close(fd);
goto bad_session;
}
pfs_close(fd);
// Create a new session based on the file info
session = dls_list_create_session(hdr.tag, hdr.item_type, hdr.item_size, &hdr.app_uuid,
hdr.timestamp, DataLoggingStatusInactive);
if (!session) {
goto bad_session;
}
session->comm.session_id = hdr.comm_session_id;
session->storage = (DataLoggingSessionStorage) {
.fd = DLS_INVALID_FILE,
.write_offset = sizeof(hdr),
.read_offset = sizeof(hdr)
};
// Make sure the filename is what we expect
prv_get_filename(name, session);
if (strncmp(name, head->name, DLS_FILE_NAME_MAX_LEN)) {
PBL_LOG(LOG_LEVEL_ERROR, "Expected name of %s, got %s", head->name, name);
pfs_remove(head->name);
goto bad_session;
}
// We need to figure out how many bytes of data are unread and the offset of the
// last byte of data (which becomes the write offset). We pass NULL into the buffer argument
// of dls_storage_read() to tell it to compute these for us.
uint32_t write_offset;
int32_t num_bytes = dls_storage_read(session, NULL, 0 /*numbytes*/, &write_offset);
if (num_bytes < 0) {
goto bad_session;
}
session->storage.num_bytes = num_bytes;
session->storage.write_offset = write_offset;
// To update the read offset, we pass 0 as num_bytes into dls_storage_consume()
if (dls_storage_consume(session, 0) < 0) {
goto bad_session;
}
PBL_LOG(LOG_LEVEL_INFO,
"Restored session %"PRIu8
" num_bytes:%"PRIu32", read_offset:%"PRIu32", write_offset:%"PRIu32,
session->comm.session_id, session->storage.num_bytes,
session->storage.read_offset, session->storage.write_offset);
// Insert this session into our list
dls_list_insert_session(session);
head = (PFSFileListEntry *)head->list_node.next;
if (head) {
// This operation can take awhile and tends to starve out other threads while it's on going.
// It typically takes 100-200ms to restore a session, so if you have a lot of sessions you
// can take 2-4 seconds to do. The KernelMain task_watchdog isn't a problem at this time
// because we haven't started monitoring it yet, but if we starve KernelBG we'll hit false
// watchdog reboots. Sleep a bit here so the background task has a chance to run. See
// PBL-24560 for a long term fix.
psleep(10);
}
num_sessions_restored++;
continue;
bad_session:
pfs_remove(head->name);
kernel_free(session);
head = (PFSFileListEntry *)head->list_node.next;
}
PBL_LOG(LOG_LEVEL_INFO, "Restored %d sessions. Total %"PRIu32" bytes allocated",
num_sessions_restored, prv_get_total_file_system_bytes());
// Free the directory list
pfs_delete_file_list(dir_list);
// No longer in initialization. From now on, only KernelBG can use the session storage calls.
s_initializing_storage = false;
}
// -------------------------------------------------------------------------------------------------
// Analytics
static bool prv_max_numbytes_cb(DataLoggingSession* session, void *data) {
uint32_t *max_bytes = (uint32_t *)data;
*max_bytes = MAX(*max_bytes, session->storage.num_bytes);
return true;
}
void analytics_external_collect_dls_stats(void) {
uint32_t max_bytes = 0;
dls_list_for_each_session(prv_max_numbytes_cb, &max_bytes);
analytics_set(ANALYTICS_DEVICE_METRIC_DATA_LOGGING_MAX_SPOOLED_BYTES, max_bytes,
AnalyticsClient_System);
}

View file

@ -0,0 +1,63 @@
/*
* 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.
*/
#pragma once
#include "services/normal/data_logging/dls_private.h"
//! @file dls_storage.h
//!
//! All these functions are only safe to call from the system task.
//! Invalidate all data logging storage space.
void dls_storage_invalidate_all(void);
//! Erase the storage for the given session
//! @param[in] session pointer to session
void dls_storage_delete_logging_storage(DataLoggingSession *session);
//! Read data from the given session. If buffer is NULL, this routine simply returns
//! the number bytes available for reading (and num_bytes is ignored). This method returns
//! the actual number of bytes read, which may be less than the number of bytes requested
//! if the last read would end in the middle of a data chunk.
//! @param[in] logging_session session to read from
//! @param[in] buffer buffer to read bytes into
//! @param[in] num_bytes number of bytes to read
//! @param[out] *new_read_offset file offset of the next byte to read
//! @return number of bytes read, or -1 if error
int32_t dls_storage_read(DataLoggingSession *logging_session, uint8_t *buffer, int32_t num_bytes,
uint32_t *new_read_offset);
//! Consume data from the session without reading it into a buffer.
//! @param[in] logging_session session to read from
//! @param[in] num_bytes number of bytes to consume.
//! @return number of bytes consumed, or -1 if error
int32_t dls_storage_consume(DataLoggingSession *logging_session, int32_t num_bytes);
//! Move the data from the circular buffer in memory to flash.
//! @param[in] logging_session session to write to
//! @return true on success, false on failure
bool dls_storage_write_session(DataLoggingSession *session);
//! Write data directly to logging session storage from a passed in buffer
//! @param[in] logging_session session to write to
//! @param[in] data buffer to write from
//! @param[in] num_bytes number of bytes to write
//! @return true on success, false on failure
bool dls_storage_write_data(DataLoggingSession *session, const void *data, uint32_t num_bytes);
//! Call this at startup to read the session state that's been stored in the file system.
void dls_storage_rebuild(void);

View file

@ -0,0 +1,67 @@
/*
* 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 "data_logging_service.h"
#include "dls_private.h"
#include "kernel/memory_layout.h"
#include "syscall/syscall_internal.h"
#include "system/logging.h"
#include <inttypes.h>
DEFINE_SYSCALL(DataLoggingSessionRef, sys_data_logging_create, uint32_t tag,
DataLoggingItemType item_type, uint16_t item_size,
void *buffer, bool resume) {
return dls_create_current_process(tag, item_type, item_size, buffer, resume);
}
DEFINE_SYSCALL(void, sys_data_logging_finish, DataLoggingSessionRef session_ref) {
// TODO: It would be nice to verify the session itself, because they could be
// passing us any memory address (not necesarilly a valid DataLoggingSession).
// An evil developer could potentially use this to confuse the data_logging
// logic, and do evil things with kernel rights. However, it's pretty unlikely
// (especially since our executable code lives in microflash, and hence can't
// just be overwritten by a buffer overrun), so it's probably fine.
DataLoggingSession* session = (DataLoggingSession*)session_ref;
if (!dls_is_session_valid(session)) {
PBL_LOG(LOG_LEVEL_WARNING, "finish: Invalid session %p", session);
return; // TODO: Return error code?
}
dls_finish(session);
}
DEFINE_SYSCALL(DataLoggingResult, sys_data_logging_log,
DataLoggingSessionRef session_ref, void* data, uint32_t num_items) {
DataLoggingSession* session = (DataLoggingSession*)session_ref;
if (!dls_is_session_valid(session)) {
PBL_LOG(LOG_LEVEL_WARNING, "log: Invalid session %p", session);
return DATA_LOGGING_INVALID_PARAMS;
}
if (data == NULL) {
PBL_LOG(LOG_LEVEL_WARNING, "log: NULL data pointer");
return DATA_LOGGING_INVALID_PARAMS;
}
if (PRIVILEGE_WAS_ELEVATED) {
syscall_assert_userspace_buffer(data, num_items * session->item_size);
}
return dls_log(session, data, num_items);
}