mirror of
https://github.com/google/pebble.git
synced 2025-11-25 00:32:24 -05:00
443 lines
14 KiB
C
443 lines
14 KiB
C
/*
|
|
* Copyright 2024 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "app_fetch_endpoint.h"
|
|
|
|
#include <string.h>
|
|
#include <stdbool.h>
|
|
|
|
#include "applib/rockyjs/rocky_res.h"
|
|
#include "kernel/pbl_malloc.h"
|
|
#include "process_management/pebble_process_info.h"
|
|
#include "services/common/comm_session/session.h"
|
|
#include "services/common/put_bytes/put_bytes.h"
|
|
#include "services/common/system_task.h"
|
|
#include "services/normal/app_cache.h"
|
|
#include "services/normal/blob_db/app_db.h"
|
|
#include "services/normal/process_management/app_storage.h"
|
|
#include "system/logging.h"
|
|
#include "system/passert.h"
|
|
#include "util/attributes.h"
|
|
#include "util/math.h"
|
|
#include "util/uuid.h"
|
|
|
|
//! Used for keeping track of binaries that are loaded through put_bytes
|
|
typedef struct {
|
|
AppInstallId app_id;
|
|
uint32_t total_size;
|
|
AppFetchResult prev_error;
|
|
bool cancelling;
|
|
bool in_progress;
|
|
bool app;
|
|
bool worker;
|
|
bool resources;
|
|
} AppFetchState;
|
|
|
|
//! Command type
|
|
enum {
|
|
APP_FETCH_INSTALL_COMMAND = 0x01,
|
|
} AppFetchCommand;
|
|
|
|
//! Possible results that come back from the INSTALL_COMMAND
|
|
enum {
|
|
APP_FETCH_INSTALL_RESPONSE = 0x01,
|
|
} AppFetchResponse;
|
|
|
|
//! Possible results that come back from the INSTALL_COMMAND
|
|
enum {
|
|
APP_FETCH_RESPONSE_STARTING = 0x01,
|
|
APP_FETCH_RESPONSE_BUSY = 0x02,
|
|
APP_FETCH_RESPONSE_UUID_INVALID = 0x03,
|
|
APP_FETCH_RESPONSE_NO_DATA = 0x04,
|
|
} AppFetchInstallResult;
|
|
|
|
//! Data sent to mobile phone for an INSTALL_COMMAND
|
|
typedef struct PACKED {
|
|
uint8_t command;
|
|
Uuid uuid;
|
|
AppInstallId app_id;
|
|
} AppFetchInstallRequest;
|
|
|
|
//! Timeout used to determine how long we should wait before the phone starts sending the app
|
|
//! we requested (by issuing a put_bytes request).
|
|
#define FETCH_TIMEOUT_MS 15000
|
|
|
|
//! State for the app fetch flow
|
|
static AppFetchState s_fetch_state;
|
|
|
|
//! Endpoint ID
|
|
static const uint16_t APP_FETCH_ENDPOINT_ID = 6001;
|
|
|
|
////////////////////////////
|
|
// Internal Helper Functions
|
|
////////////////////////////
|
|
|
|
//! Puts an error event with the given error code
|
|
static void prv_put_event_error(uint8_t error_code) {
|
|
s_fetch_state.prev_error = error_code;
|
|
PebbleEvent event = {
|
|
.type = PEBBLE_APP_FETCH_EVENT,
|
|
.app_fetch = {
|
|
.type = AppFetchEventTypeError,
|
|
.id = s_fetch_state.app_id,
|
|
.error_code = error_code,
|
|
}
|
|
};
|
|
event_put(&event);
|
|
}
|
|
|
|
//! Puts an event with the given progress
|
|
static void prv_put_event_progress(uint8_t percent) {
|
|
PebbleEvent event = {
|
|
.type = PEBBLE_APP_FETCH_EVENT,
|
|
.app_fetch = {
|
|
.type = AppFetchEventTypeProgress,
|
|
.id = s_fetch_state.app_id,
|
|
.progress_percent = percent,
|
|
}
|
|
};
|
|
event_put(&event);
|
|
}
|
|
|
|
//! Simply posts the type of event given.
|
|
static void prv_put_event_simple(AppFetchEventType type) {
|
|
PebbleEvent event = {
|
|
.type = PEBBLE_APP_FETCH_EVENT,
|
|
.app_fetch = {
|
|
.type = type,
|
|
.id = s_fetch_state.app_id,
|
|
}
|
|
};
|
|
event_put(&event);
|
|
}
|
|
|
|
|
|
//! Recomputes and saves the progress percent for the current application fetch session
|
|
static uint8_t prv_compute_progress_percent(PutBytesObjectType type, unsigned int type_percent) {
|
|
// Add 33(34) percent for each piece that has finished (or is unneeded)
|
|
uint8_t percent = 0;
|
|
if (s_fetch_state.app) {
|
|
percent += 30;
|
|
}
|
|
if (s_fetch_state.worker) {
|
|
percent += 10;
|
|
}
|
|
if (s_fetch_state.resources) {
|
|
percent += 60;
|
|
}
|
|
|
|
// add in the progress for the currently transferring piece.
|
|
percent += (type_percent / 3);
|
|
|
|
// store value
|
|
return MIN(100, percent);
|
|
}
|
|
|
|
//! Cleans up the state of the app fetch endpoint. Always called from the system task
|
|
static void prv_cleanup(AppFetchResult result) {
|
|
if (result != AppFetchResultSuccess) {
|
|
put_bytes_cancel();
|
|
app_cache_remove_entry(s_fetch_state.app_id);
|
|
prv_put_event_error(result);
|
|
}
|
|
|
|
s_fetch_state.in_progress = false;
|
|
|
|
PBL_LOG(LOG_LEVEL_INFO, "App fetch cleanup with result %d", result);
|
|
}
|
|
|
|
//! System task callback triggered by app_fetch_put_bytes_event_handler() when we are receiving
|
|
//! put_bytes messages in reponse to a fetch request to the phone.
|
|
void prv_put_bytes_event_system_task_cb(void *data) {
|
|
PebblePutBytesEvent *pb_event = (PebblePutBytesEvent *)data;
|
|
|
|
if (!s_fetch_state.in_progress) {
|
|
return;
|
|
}
|
|
|
|
// If put_bytes has failed, let's just say fail and stop everything.
|
|
if (pb_event->failed == true) {
|
|
AppFetchResult error;
|
|
if (s_fetch_state.cancelling) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Put bytes cancelled by user");
|
|
error = AppFetchResultUserCancelled;
|
|
} else {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Put bytes failure");
|
|
error = AppFetchResultPutBytesFailure;
|
|
}
|
|
|
|
prv_cleanup(error);
|
|
goto finally;
|
|
}
|
|
|
|
if (pb_event->type == PebblePutBytesEventTypeInitTimeout) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Timed out waiting for putbytes request from phone");
|
|
prv_cleanup(AppFetchResultTimeoutError);
|
|
}
|
|
|
|
// If this is an object that doesn't have a cookie, then we won't care about it.
|
|
if (pb_event->has_cookie == false) {
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "Ignoring non cookie put_bytes event");
|
|
goto finally;
|
|
}
|
|
|
|
// Check for the different types of PutBytes events.
|
|
if (pb_event->type == PebblePutBytesEventTypeProgress) {
|
|
// compute and save the new progress, then show it on the progress bar
|
|
uint8_t percent =
|
|
prv_compute_progress_percent(pb_event->object_type, pb_event->progress_percent);
|
|
prv_put_event_progress(percent);
|
|
} else if (pb_event->type == PebblePutBytesEventTypeCleanup) {
|
|
// Mark off each finishing put_bytes transaction in our progress struct
|
|
|
|
switch (pb_event->object_type) {
|
|
case ObjectWatchApp:
|
|
s_fetch_state.app = true;
|
|
break;
|
|
case ObjectWatchWorker:
|
|
s_fetch_state.worker = true;
|
|
break;
|
|
case ObjectAppResources:
|
|
s_fetch_state.resources = true;
|
|
break;
|
|
default:
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Got a PutBytes Object that we shouldn't have gotten");
|
|
prv_cleanup(AppFetchResultGeneralFailure);
|
|
goto finally;
|
|
}
|
|
|
|
// add the size of the finished PutBytes transaction to the total size.
|
|
s_fetch_state.total_size += pb_event->total_size;
|
|
}
|
|
|
|
if (s_fetch_state.app && s_fetch_state.worker && s_fetch_state.resources) {
|
|
// if everything has finished being transferred
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "All pieces (%"PRIu32" bytes) have been sent over put_bytes",
|
|
s_fetch_state.total_size);
|
|
|
|
// signify in the app cache that the app binaries are now loaded
|
|
status_t added = app_cache_add_entry(s_fetch_state.app_id, s_fetch_state.total_size);
|
|
if (added == S_SUCCESS) {
|
|
const PebbleProcessMd *md = app_install_get_md(s_fetch_state.app_id, false);
|
|
if (rocky_app_validate_resources(md) == RockyResourceValidation_Invalid) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Received app contains invalid JS bytecode");
|
|
prv_put_event_error(AppFetchResultIncompatibleJSFailure);
|
|
} else {
|
|
// Set prev_error as a Success.
|
|
s_fetch_state.prev_error = AppFetchResultSuccess;
|
|
prv_put_event_simple(AppFetchEventTypeFinish);
|
|
}
|
|
app_install_release_md(md);
|
|
} else {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Failed to insert into app cache: %"PRId32, added);
|
|
prv_put_event_error(AppFetchResultGeneralFailure);
|
|
}
|
|
prv_cleanup(AppFetchResultSuccess);
|
|
|
|
} else if (pb_event->type == PebblePutBytesEventTypeCleanup) {
|
|
// Start the timeout watchdog again so we can tell if things get hung up
|
|
// before the phone starts sending the next putbytes object.
|
|
// This will only trigger if we've completed a piece and are still
|
|
// waiting for another one.
|
|
put_bytes_expect_init(FETCH_TIMEOUT_MS);
|
|
}
|
|
|
|
finally:
|
|
kernel_free(pb_event);
|
|
}
|
|
|
|
//! Put Bytes handler. Used for keeping track of progress and cleanup events. This is called
|
|
//! from KernelMain's event handler when it receives a PEBBLE_PUT_BYTES_EVENT event. put_bytes
|
|
//! posts these events to inform clients of progress.
|
|
void app_fetch_put_bytes_event_handler(PebblePutBytesEvent *pb_event) {
|
|
// If an app fetch isn't in progress, ignore it.
|
|
if (!s_fetch_state.in_progress) {
|
|
return;
|
|
}
|
|
|
|
PebblePutBytesEvent *pb_event_copy = kernel_malloc_check(sizeof(PebblePutBytesEvent));
|
|
memcpy(pb_event_copy, pb_event, sizeof(PebblePutBytesEvent));
|
|
system_task_add_callback(prv_put_bytes_event_system_task_cb, pb_event_copy);
|
|
}
|
|
|
|
//! Callback for the system task to fire off the fetch request. Triggered by a call to
|
|
//! app_fetch_binaries().
|
|
static void prv_app_fetch_binaries_system_task_cb(void *data) {
|
|
AppFetchInstallRequest *request = (AppFetchInstallRequest *)data;
|
|
|
|
// check if Bluetooth is active. If so, this will send.
|
|
bool successful = comm_session_send_data(comm_session_get_system_session(), APP_FETCH_ENDPOINT_ID,
|
|
(uint8_t*)request, sizeof(AppFetchInstallRequest), COMM_SESSION_DEFAULT_TIMEOUT);
|
|
|
|
// log it
|
|
char uuid_buffer[UUID_STRING_BUFFER_LENGTH];
|
|
uuid_to_string(&request->uuid, uuid_buffer);
|
|
PBL_LOG(LOG_LEVEL_INFO, "%s request for app with uuid: %s and app_id: %"PRIu32"",
|
|
successful ? "Sent" : "Failed to send", uuid_buffer, request->app_id);
|
|
|
|
// free before error checking
|
|
kernel_free(request);
|
|
|
|
// If Bluetooth wasn't active, then post the error and cleanup.
|
|
if (!successful) {
|
|
prv_cleanup(AppFetchResultNoBluetooth);
|
|
return;
|
|
}
|
|
|
|
// We next expect app_fetch_put_bytes_event_handler() to be called when the phone
|
|
// gets our fetch request and issues a putbytes request.
|
|
// Start the timeout watchdog to catch us in case the phone never issues the putbytes request.
|
|
put_bytes_expect_init(FETCH_TIMEOUT_MS);
|
|
}
|
|
|
|
//! Called from the system task. Translates an Endpoint error to an event error and sends
|
|
//! off the appropriate event.
|
|
void prv_handle_app_fetch_install_response(uint8_t result_code) {
|
|
switch (result_code) {
|
|
case APP_FETCH_RESPONSE_STARTING:
|
|
PBL_LOG(LOG_LEVEL_INFO, "Phone confirmed it will start sending data");
|
|
prv_put_event_simple(AppFetchEventTypeStart);
|
|
put_bytes_expect_init(FETCH_TIMEOUT_MS);
|
|
break;
|
|
case APP_FETCH_RESPONSE_BUSY:
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Error: Phone is currently busy");
|
|
prv_cleanup(AppFetchResultPhoneBusy);
|
|
break;
|
|
case APP_FETCH_RESPONSE_UUID_INVALID:
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Error: UUID Invalid");
|
|
prv_cleanup(AppFetchResultUUIDInvalid);
|
|
break;
|
|
case APP_FETCH_RESPONSE_NO_DATA:
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Error: No data on phone");
|
|
prv_cleanup(AppFetchResultNoData);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/////////////////////////
|
|
// Exported App Fetch API
|
|
/////////////////////////
|
|
|
|
//! Called by the system that triggers an app fetch install request
|
|
void app_fetch_binaries(const Uuid *uuid, AppInstallId app_id, bool has_worker) {
|
|
if (s_fetch_state.in_progress) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Already an app fetch in progress. Ignoring request");
|
|
return;
|
|
}
|
|
|
|
AppFetchInstallRequest *request = kernel_malloc_check(sizeof(AppFetchInstallRequest));
|
|
|
|
// reset all state
|
|
s_fetch_state = (AppFetchState){};
|
|
|
|
// Mark whether the worker needs to be sent over.
|
|
s_fetch_state.worker = !has_worker;
|
|
s_fetch_state.app_id = app_id;
|
|
s_fetch_state.in_progress = true;
|
|
|
|
// populate fields
|
|
request->command = APP_FETCH_INSTALL_COMMAND;
|
|
request->uuid = *uuid;
|
|
request->app_id = app_id;
|
|
|
|
// Start "warming up" the connection, this will cause the low-latency period to start ~1s sooner.
|
|
// Put bytes will extend the low-latency period after this:
|
|
comm_session_set_responsiveness(comm_session_get_system_session(), BtConsumerPpAppFetch,
|
|
ResponseTimeMin, MIN_LATENCY_MODE_TIMEOUT_APP_FETCH_SECS);
|
|
|
|
system_task_add_callback(prv_app_fetch_binaries_system_task_cb, request);
|
|
}
|
|
|
|
AppFetchError app_fetch_get_previous_error(void) {
|
|
AppFetchError error = {
|
|
.error = s_fetch_state.prev_error,
|
|
.id = s_fetch_state.app_id,
|
|
};
|
|
|
|
return error;
|
|
}
|
|
|
|
static void prv_cancel_fetch_from_system_task(void *data) {
|
|
AppInstallId app_id = (AppInstallId)data;
|
|
|
|
if ((!s_fetch_state.in_progress) ||
|
|
((s_fetch_state.app_id != app_id) && (app_id != INSTALL_ID_INVALID))) {
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "Attempted to cancel an app that is currently not being"
|
|
" fetched: %"PRId32, app_id);
|
|
return;
|
|
}
|
|
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "Cancelling app fetch from system task");
|
|
s_fetch_state.cancelling = true;
|
|
put_bytes_cancel();
|
|
}
|
|
|
|
void app_fetch_cancel_from_system_task(AppInstallId app_id) {
|
|
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
|
|
|
prv_cancel_fetch_from_system_task((void *)(uintptr_t)app_id);
|
|
}
|
|
|
|
void app_fetch_cancel(AppInstallId app_id) {
|
|
// Everything within app fetch happens on the background task
|
|
system_task_add_callback(prv_cancel_fetch_from_system_task, (void *)(uintptr_t)app_id);
|
|
}
|
|
|
|
bool app_fetch_in_progress(void) {
|
|
return s_fetch_state.in_progress;
|
|
}
|
|
|
|
////////////////////////////
|
|
// Exported Callbacks
|
|
////////////////////////////
|
|
|
|
typedef struct __attribute__((__packed__)) {
|
|
uint8_t command;
|
|
uint8_t result_code;
|
|
} AppFetchResponseData;
|
|
|
|
//! System task callback triggered by app_fetch_protocol_msg_callback().
|
|
static void prv_app_fetch_protocol_handle_msg(AppFetchResponseData *response_data) {
|
|
|
|
switch (response_data->command) {
|
|
case APP_FETCH_INSTALL_RESPONSE:
|
|
prv_handle_app_fetch_install_response(response_data->result_code);
|
|
break;
|
|
default:
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Invalid message received, command: %u result: %u",
|
|
response_data->command, response_data->result_code);
|
|
prv_cleanup(AppFetchResultGeneralFailure);
|
|
break;
|
|
}
|
|
}
|
|
|
|
//! Callback that is placed in the endpoints table. As of now, only responses will come through this
|
|
//! callback as all commands are originally sent to the phone.
|
|
void app_fetch_protocol_msg_callback(CommSession *session, const uint8_t *data, size_t length) {
|
|
if (length < sizeof(AppFetchResponseData)) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Invalid message length %"PRIu32"", (uint32_t)length);
|
|
prv_cleanup(AppFetchResultGeneralFailure);
|
|
return;
|
|
}
|
|
|
|
if (!s_fetch_state.in_progress) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Got a message but app fetch not in progress. Ignoring");
|
|
return;
|
|
}
|
|
|
|
prv_app_fetch_protocol_handle_msg((AppFetchResponseData *)data);
|
|
}
|