diff --git a/components/kaos/appmgr/src/appmgr_ao.c b/components/kaos/appmgr/src/appmgr_ao.c index 63bc606..bdaf84e 100644 --- a/components/kaos/appmgr/src/appmgr_ao.c +++ b/components/kaos/appmgr/src/appmgr_ao.c @@ -16,9 +16,13 @@ #include "kdrv_apps.h" #include "kdrv_kfw.h" #include "kapp_data.h" +#if CONFIG_KODEOS_BLE_CONSOLE +#include "kdrv_ble.h" /* the console's second port: its backlog, before a launch */ +#endif #include "sdkconfig.h" #include "kode_log.h" #include "khal_crc32.h" +#include "khal_nvs.h" #include "khal_system.h" #include "kaos_claim.h" #include "kverbs.h" @@ -30,12 +34,16 @@ * costs ~1.6s over a 5MB image - cheap for a UI that stays alive. */ #define APPMGR_CHUNK_GAP_TICKS KQP_TICKS_PER_MS(10U) /* No chunk for this long during an install and the host is presumed gone - - * KODE_RECV_IDLE_MS (kqp_config.h) in ticks, this AO's own watchdog on the + * KODE_RECV_IDLE_MS (kqp_config.h) in ticks - KODE_RECV_IDLE_BLE_MS when the + * payload rides BLE - this AO's own watchdog on the * command (README.md "The console": AO_Console disarms its 5 s one for the * length of this command and trusts this one instead - a 5 MB image over a * slow link runs for far longer than 5 s, and only the AO watching the * stream can tell "slow" from "dead"). */ #define APPMGR_RECV_IDLE_TICKS KQP_TICKS_PER_MS(KODE_RECV_IDLE_MS) +/* The same watchdog for a transfer that rides BLE: a link that stalls for a + * few seconds is slow, and the phone app waits 10 s for an ack. */ +#define APPMGR_RECV_IDLE_BLE_TICKS KQP_TICKS_PER_MS(KODE_RECV_IDLE_BLE_MS) /* Insurance on the arming handshake itself, armed at the moment this AO * enters AppMgr_receiving: if the console's yes never arrives (its answer * event could not be allocated - the one path with nobody left to speak), @@ -56,6 +64,9 @@ /* Between "the image is installed" and the reset that boots it: long enough * for AO_Log's lower-priority task to put `>ok` on the wire first. */ #define APPMGR_ANSWER_DRAIN_TICKS KQP_TICKS_PER_MS(50U) +/* Over BLE the answer may still be queued for the link after that wait: the + * launch looks again every APPMGR_ANSWER_DRAIN_TICKS, up to this long. */ +#define APPMGR_ANSWER_BLE_TICKS KQP_TICKS_PER_MS(KODE_RECV_DRAIN_BLE_MS) /* Region bytes read per step while an evicted app is copied out of the * region: the same 32 KB the writes use, so a step stays short (rule 5). */ #define APPMGR_KEEP_CHUNK (32U * 1024U) @@ -63,6 +74,11 @@ * takes the LED's orange pulse back after three seconds without a report, and * a multi-MB write has none of its own. */ #define APPMGR_KEEP_BEAT_TICKS KQP_TICKS_PER_MS(1000U) +/* Re-publish the percent this often while the AO waits on the network: the + * open of a download, a file that stops arriving, a data folder being fetched. + * AO_Power takes the LED's orange pulse back after three seconds without a + * report, and the glass's write card has nothing to follow between two. */ +#define APPMGR_WAIT_BEAT_TICKS KQP_TICKS_PER_MS(1000U) /* How long the card may take over one copy before it is given up on: a base * that covers mounting and a slow counterfeit card, plus a second for every * 128 KB, so a 13 MB extent gets nearly three minutes and a small one a @@ -108,6 +124,18 @@ _Static_assert(((2U * (KODE_STORAGE_LIST_MAX + 1U)) + (KODE_APP_EVICT_MAX + 1U)) _Static_assert((KAPPS_MAX_ENTRIES + 1U) <= UINT8_MAX, "AppEvictEndEvt.victims must hold every victim of a plan"); +/* The recovery of an install a reset cut runs one step at a time, each step + * started by a one-shot timer rather than posted at once, so everything else + * on the board runs between two. A step folds at most this many bytes of an + * image into its CRC: the read through the flash is the cost, and the time it + * takes is kept to a few tens of milliseconds, whatever the image's size. */ +#define APPMGR_UNDO_PACE_MS 50U /**< Between two steps of the recovery. */ +#define APPMGR_UNDO_PACE_TICKS KQP_TICKS_PER_MS(APPMGR_UNDO_PACE_MS) /**< The same in ticks. */ +#define APPMGR_UNDO_STEP_BYTES (512U * 1024U) /**< Bytes of an image one step folds into its CRC. */ + +/** Listings a start-up recovery holds for after it ends: the launcher's and the console's, and room. */ +#define APPMGR_REC_LIST_MAX 4U + /* How long AO_Storage gets to answer one folder request of the tidy step * (APPMGR_TIDY_MS, kaos.h) before this AO gives up on it and goes on. The * install has already succeeded when the wait starts, so the budget only @@ -199,6 +227,39 @@ typedef enum { KEEP_FAILED /**< No card, no memory, no name, or the write failed. */ } keep_res_t; +/** @brief Which way an app an install evicted went (undo_table_t.fate). */ +enum { + UNDO_STAYS = 0, /**< Never left the directory, or could not be put back. */ + UNDO_CHECK, /**< A reset cut the install: its image is still to be verified. */ + UNDO_GONE, /**< Overwritten, and gone. */ + UNDO_BACK /**< Put back. */ +}; + +/** + * @brief AO_AppMgr's table of what an install evicted. + * + * Its head is the blob kept in NVS, byte for byte: the header and the first + * `count` records. What became of each victim follows it and never leaves + * RAM: it lives and dies with the table, in the same PSRAM block. + */ +typedef struct { + appmgr_undo_hdr_t hdr; /**< Version and count. */ + appmgr_undo_rec_t rec[KAPPS_MAX_ENTRIES]; /**< One per victim, the first `count` of them. */ + uint8_t fate[KAPPS_MAX_ENTRIES]; /**< UNDO_*: what became of each; not in the blob. */ +} undo_table_t; + +/* A record is a directory entry and a name with nothing between, and a table + * has a slot for every entry of the directory: the layout in NVS is this one. */ +_Static_assert(sizeof(appmgr_undo_hdr_t) == APPMGR_UNDO_HDR_LEN, + "the table's header is a frozen NVS layout"); +_Static_assert(sizeof(appmgr_undo_rec_t) == APPMGR_UNDO_REC_LEN, + "the table's record is a frozen NVS layout"); +_Static_assert(sizeof(appmgr_undo_rec_t) + == (sizeof(kapps_entry_t) + KDRV_SLOT_PROJECT_MAX + sizeof(uint32_t)), + "a record is a directory entry, a name and a CRC"); +_Static_assert(APPMGR_UNDO_MAX == KAPPS_MAX_ENTRIES, + "a table holds every slot of the directory"); + /** @brief The apps-region AO, plus the state of one in-flight copy. */ typedef struct { QActive super; @@ -257,9 +318,16 @@ typedef struct { /* ---- An image arriving on the console (APP_RECV_REQ_SIG) ------------ */ QTimeEvt idle; /**< No chunk for this long: the host is gone. */ + QTimeEvt beat; /**< Says the percent again once a second while the + * network is waited on (APP_BEAT_SIG). */ QTimeEvt answer; /**< Lets the `>ok` reach the wire before the * board resets into the new app; see * AppMgr_launching. */ + uint8_t recv_src; /**< KODE_CONSOLE_SRC_*: the port the payload + * rides, which sets the idle bound and + * whether the launch waits for the link. */ + uint32_t answer_polls; /**< Times the launch has looked at the BLE + * console's backlog and found it not empty. */ uint32_t recv_crc; /**< Running CRC over what has arrived. */ char recv_name[KODE_APP_PROJECT_MAX]; /**< The project the host says * the image is, or "": the app it replaces. */ @@ -407,6 +475,38 @@ typedef struct { uint32_t victim_count; /**< Names recorded in victims[]. */ uint32_t victim_next; /**< The next one to ask AO_Storage about. */ + /* ---- What an install evicted, to give back if it fails --------------- + * The plan takes its victims out of the directory before the first byte + * arrives, but the region erases 64 KB as the data gets there: an install + * that stops early has erased only what it reached. Each victim is noted + * as the plan takes it, and a failed install puts back every one whose + * extent was not reached (undo_restore()); a landed one discards them. + * PSRAM, taken with the first victim (rule 8). */ + undo_table_t *undo; /**< The evicted entries; NULL when none are held. */ + uint32_t undo_count; /**< Records noted in undo->rec[]. */ + bool undo_stored; /**< The table is in NVS, so a reset in the middle + * of the install gives the victims back. */ + + /* ---- Recovering what a reset cut out of an install (AppMgr_recovering) + * At start-up a table in NVS names apps a reset took out of the directory + * and never gave back. Each is checked by the CRC of its extent, a step at a + * time, and nothing is + * answered until the last is settled; a listing asked meanwhile waits. */ + QTimeEvt undo_tmo; /**< Starts each step of the recovery (APP_UNDO_STEP_SIG). */ + uint32_t rec_next; /**< The next record to look at. */ + uint32_t rec_done; /**< Bytes of that record's extent already folded into rec_crc. */ + uint32_t rec_crc; /**< The running CRC of that extent. */ + bool rec_adopt; /**< The mailbox holds an image: adopt it once the recovery ends. */ + uint8_t rec_list_n; /**< Listings asked during the recovery. */ + QActive *rec_list_who[APPMGR_REC_LIST_MAX]; /**< Who asked. */ + uint16_t rec_list_skip[APPMGR_REC_LIST_MAX]; /**< From which entry. */ + + /* ---- A verdict still owed (FlashReqEvt.who) ---------------------------- + * An install off a medium is taken once the medium has opened its file, so + * the asker is remembered from the request to that answer. NULL between + * installs, and for a request that asked for none. */ + QActive *req_who; + /* ---- The erase in hand (APP_ERASE_REQ_SIG) ---------------------------- * What its end says (AppEraseDoneEvt), kept from the idle step that took * the image to the answer of AO_Storage's wipe; the name is tidy_name. */ @@ -451,6 +551,7 @@ static QState AppMgr_asking(AppMgr * const me, QEvt const * const e); static QState AppMgr_finding(AppMgr * const me, QEvt const * const e); static QState AppMgr_scrubbing(AppMgr * const me, QEvt const * const e); static QState AppMgr_tidying(AppMgr * const me, QEvt const * const e); +static QState AppMgr_recovering(AppMgr * const me, QEvt const * const e); #if CONFIG_KODEOS_NET static QState AppMgr_fetching(AppMgr * const me, QEvt const * const e); #endif @@ -513,16 +614,24 @@ void appmgr_cancel_request(void) QACTIVE_POST(AO_AppMgr, &cancel, NULL); } -void appmgr_run_request(const char *name) +bool appmgr_run_ask(const char *name, QActive *who) { AppFlashEvt *e = Q_NEW_X(AppFlashEvt, 0U, APP_RUN_REQ_SIG); if ( NULL == e ) { KLOG_E(TAG, "out of events; run request dropped"); - return; + return false; } (void)snprintf(e->file, sizeof(e->file), "%s", name); + e->launch = false; + e->who = who; QACTIVE_POST(AO_AppMgr, &e->super, NULL); + return true; +} + +void appmgr_run_request(const char *name) +{ + (void)appmgr_run_ask(name, NULL); } void appmgr_erase_request(const char *name) @@ -534,6 +643,8 @@ void appmgr_erase_request(const char *name) return; } (void)snprintf(e->file, sizeof(e->file), "%s", name); + e->launch = false; + e->who = NULL; QACTIVE_POST(AO_AppMgr, &e->super, NULL); } @@ -638,43 +749,73 @@ static void app_cat_deny(AppMgr * const me) } /** - * @brief One transfer: where the bytes come from, where they go. + * @brief Build the request for one transfer: where the bytes come from, where + * they go. * - * The only way to ask AO_AppMgr to write an image. appmgr_flash_request() is - * the shorthand for "this file, as an app, and boot it". + * @param src_kind kfw_src_t as a plain integer. + * @param src File name for fs/sd, URL for http; NULL for none. + * @param dst_kind kfw_target_t as a plain integer. + * @param launch Boot the image once it is written and verified. + * @param who Where AO_AppMgr's verdict is posted, or NULL for none. + * @return The event, or NULL when the pool is dry. */ -QEvt const *appmgr_image_evt(uint8_t src_kind, const char *src, - uint8_t dst_kind, bool launch) +static FlashReqEvt *flash_req_new(uint8_t src_kind, const char *src, + uint8_t dst_kind, bool launch, QActive *who) { - FlashReqEvt *e = Q_NEW_X(FlashReqEvt, 0U, FLASH_REQ_SIG); - QEvt const *evt = NULL; + FlashReqEvt *e = Q_NEW_X(FlashReqEvt, 0U, FLASH_REQ_SIG); if ( NULL != e ) { e->src_kind = src_kind; e->dst_kind = dst_kind; e->launch = launch; + e->who = who; e->src[0] = '\0'; if ( NULL != src ) { (void)snprintf(e->src, sizeof(e->src), "%s", src); } - evt = &e->super; + } + return e; +} + +/** + * @brief One transfer: where the bytes come from, where they go. + * + * The only way to ask AO_AppMgr to write an image. appmgr_flash_request() is + * the shorthand for "this file, as an app, and boot it". + */ +QEvt const *appmgr_image_evt(uint8_t src_kind, const char *src, + uint8_t dst_kind, bool launch) +{ + FlashReqEvt * const e = flash_req_new(src_kind, src, dst_kind, launch, NULL); + QEvt const *evt = NULL; + + if ( NULL != e ) + { + evt = &e->super; } return evt; } -void appmgr_image_request(uint8_t src_kind, const char *src, - uint8_t dst_kind, bool launch) +bool appmgr_image_ask(uint8_t src_kind, const char *src, uint8_t dst_kind, + bool launch, QActive *who) { - QEvt const *e = appmgr_image_evt(src_kind, src, dst_kind, launch); + FlashReqEvt * const e = flash_req_new(src_kind, src, dst_kind, launch, who); if ( NULL == e ) { KLOG_E(TAG, "out of events; flash request dropped"); - return; + return false; } - QACTIVE_POST(AO_AppMgr, e, NULL); + QACTIVE_POST(AO_AppMgr, &e->super, NULL); + return true; +} + +void appmgr_image_request(uint8_t src_kind, const char *src, + uint8_t dst_kind, bool launch) +{ + (void)appmgr_image_ask(src_kind, src, dst_kind, launch, NULL); } /* A reader that keeps KODE_APP_LIST_MAX rows of the installed list holds @@ -773,6 +914,29 @@ static void victims_clear(AppMgr * const me) me->victim_next = 0U; } +/** + * @brief Forget the apps the install evicted: it landed, or they were given back. + * + * The table in NVS goes with it, after whatever made it unnecessary. + */ +static void undo_clear(AppMgr * const me) +{ + if ( me->undo_stored ) + { + me->undo_stored = false; + if ( khal_nvs_erase(APPMGR_UNDO_NS, APPMGR_UNDO_KEY) != KODE_OK ) + { + KLOG_W(TAG, "cannot erase the record of the apps an install evicted"); + } + } + if ( NULL != me->undo ) + { + khal_free(me->undo); + me->undo = NULL; + } + me->undo_count = 0U; +} + /** @brief Forget the round: the install it belonged to has ended. */ static void keep_clear(AppMgr * const me) { @@ -782,6 +946,7 @@ static void keep_clear(AppMgr * const me) me->keep_buf = NULL; } victims_clear(me); /* an install that ends owes no folder to anyone */ + undo_clear(me); /* nor is anything left to give back */ me->keep_round = false; me->keep_pending = false; me->keep_forced = false; @@ -1080,9 +1245,539 @@ static void plan_evict(AppMgr * const me, kapps_dir_t *d, { evict_one(me, d, idx); } - d->e[idx].flags = 0U; /* dead entry */ + d->e[idx].flags = 0U; /* dead entry */ + } + appdir_compact(d); +} + +/** + * @brief Note an app the plan is about to take out of the directory. + * + * Its entry and its name, read before the write that follows, which may erase + * the header the name is in. A dry PSRAM costs the way back, never the + * install: the app is lost with the failure, as it was before apps were + * noted. + * + * @param me The AO. + * @param d The working directory copy the plan was made on, not yet edited. + * @param idx The victim's entry in @p d. + */ +static void undo_note(AppMgr * const me, const kapps_dir_t *d, uint8_t idx) +{ + kdrv_slot_desc_t vd; + + if ( NULL == me->undo ) + { + /* cppcheck-suppress misra-c2012-11.5 ; a table from a void * allocator */ + me->undo = (undo_table_t *)khal_realloc_psram(NULL, sizeof(undo_table_t)); + me->undo_count = 0U; + } + if ( (NULL == me->undo) || (KAPPS_MAX_ENTRIES <= me->undo_count) ) + { + KLOG_W(TAG, "no room to remember an app this install evicts; a failure " + "of the install costs it"); + return; + } + appmgr_undo_rec_t * const r = &me->undo->rec[me->undo_count]; + + (void)memset(r, 0, sizeof(*r)); + r->offset = d->e[idx].offset; + r->size = d->e[idx].size; + r->seq = d->e[idx].seq; + r->flags = d->e[idx].flags; + if ( kdrv_apps_desc(&d->e[idx], &vd, NULL) == KODE_OK ) + { + (void)snprintf(r->project, sizeof(r->project), "%s", vd.project); + } + else + { + unnamed_label(r->project, sizeof(r->project), d->e[idx].offset); + } + /* The digest the next boot checks the image against: of the bytes as they + * lie now, before the install's first erase can reach them. Read in one + * step, for every victim of the plan: a few tens of ms per MB of images. */ + uint32_t crc = KHAL_CRC32_INIT; + + if ( kdrv_apps_extent_crc(d->e[idx].offset, d->e[idx].size, &crc) != KODE_OK ) + { + KLOG_W(TAG, "cannot read '%s' to record it; a reset in the middle of " + "the install costs it", r->project); + crc = 0U; + } + r->crc = crc; + me->undo->fate[me->undo_count] = (uint8_t)UNDO_STAYS; + ++me->undo_count; +} + +/** + * @brief The directory entry a record holds. + * + * @param r The record. + * @return The entry as it stood. + */ +static kapps_entry_t undo_entry(const appmgr_undo_rec_t *r) +{ + kapps_entry_t const e = { + .offset = r->offset, .size = r->size, .seq = r->seq, .flags = r->flags, + }; + + return e; +} + +/** + * @brief Write the table of the victims noted so far into NVS. + * + * Before the eviction is committed, so a reset between the two loses nothing: + * the table is the only place the victims' entries are kept. One blob, so a + * power cut leaves the old table or the new. A write that fails costs the way + * back from a reset, never the install. + * + * @param me The AO, with its victims noted. + */ +static void undo_store(AppMgr * const me) +{ + if ( (NULL == me->undo) || (0U == me->undo_count) ) + { + return; + } + me->undo->hdr.version = (uint8_t)APPMGR_UNDO_VERSION; + me->undo->hdr.count = (uint8_t)me->undo_count; + me->undo->hdr.reserved[0] = 0U; + me->undo->hdr.reserved[1] = 0U; + size_t const len = sizeof(me->undo->hdr) + + ((size_t)me->undo_count * sizeof(appmgr_undo_rec_t)); + if ( khal_nvs_set_blob(APPMGR_UNDO_NS, APPMGR_UNDO_KEY, me->undo, len) == KODE_OK ) + { + me->undo_stored = true; + } + else + { + KLOG_W(TAG, "cannot record the apps this install evicts; a reset in the " + "middle of it costs them"); + } +} + +/** + * @brief Take a plan's victims out of the working copy and commit it. + * + * Each victim is noted first (undo_note()), so a failed install can give back + * the ones its write never reaches. Nothing is committed when the plan names + * none. + * + * @param me The AO. + * @param d The working directory copy the plan was made on. + * @param plan The plan. + * @return true when the directory is as the plan wants it; false when the + * commit failed, with nothing evicted. + */ +static bool plan_commit(AppMgr * const me, kapps_dir_t *d, + const kapps_plan_t *plan) +{ + for ( uint32_t i = 0U; i < plan->evict_count; ++i ) + { + undo_note(me, d, plan->evict[i]); + } + if ( 0U < plan->evict_count ) + { + undo_store(me); + } + plan_evict(me, d, plan); + return (0U == plan->evict_count) || (kdrv_apps_dir_commit(d) == KODE_OK); +} + +/** + * @brief Whether the directory holds an entry with install number @p seq. + * + * @param d The directory. + * @param seq The install number: unique to an entry. + * @return true when one does. + */ +static bool appdir_has_seq(const kapps_dir_t *d, uint32_t seq) +{ + for ( uint32_t i = 0U; i < d->count; ++i ) + { + if ( d->e[i].seq == seq ) + { + return true; + } + } + return false; +} + +/** + * @brief Put an entry into the directory where its install number belongs. + * + * The directory is in install order; the caller has made sure it has room. + * + * @param d The working directory copy. + * @param ent The entry to add. + */ +static void appdir_insert(kapps_dir_t *d, const kapps_entry_t *ent) +{ + uint32_t at = d->count; + + for ( uint32_t i = 0U; i < d->count; ++i ) + { + if ( d->e[i].seq > ent->seq ) + { + at = i; + break; + } + } + for ( uint32_t j = d->count; j > at; --j ) + { + d->e[j] = d->e[j - 1U]; + } + d->e[at] = *ent; + ++d->count; +} + +/** + * @brief An install failed: give back the apps its write did not reach. + * + * An app whose extent the write erased any part of is gone, and the log says + * so; every other one has its entry put back in one directory commit, because + * its flash is as it was. A victim whose eviction never reached the flash (the + * commit failed) is still in the directory and is left alone. The data + * folders of the apps given back are not this step's: nothing deletes a + * folder for a failed install. + * + * @param me The AO. + */ +static void undo_restore(AppMgr * const me) +{ + if ( 0U == me->undo_count ) + { + undo_clear(me); + return; + } + + kapps_dir_t dir = *kdrv_apps_dir(); + bool changed = false; + + for ( uint32_t i = 0U; i < me->undo_count; ++i ) + { + kapps_entry_t const ent = undo_entry(&me->undo->rec[i]); + + me->undo->fate[i] = (uint8_t)UNDO_STAYS; + if ( kdrv_apps_write_reached(ent.offset, ent.size) ) + { + me->undo->fate[i] = (uint8_t)UNDO_GONE; + } + else if ( !appdir_has_seq(&dir, ent.seq) && (KAPPS_MAX_ENTRIES > dir.count) ) + { + appdir_insert(&dir, &ent); + me->undo->fate[i] = (uint8_t)UNDO_BACK; + changed = true; + } + else + { + /* Its eviction never reached the flash: it never left. */ + } + } + if ( changed && (kdrv_apps_dir_commit(&dir) != KODE_OK) ) + { + KLOG_E(TAG, "cannot update the directory; the apps the failed install " + "evicted stay out"); + } + for ( uint32_t i = 0U; i < me->undo_count; ++i ) + { + appmgr_undo_rec_t const * const r = &me->undo->rec[i]; + + if ( (uint8_t)UNDO_GONE == me->undo->fate[i] ) + { + KLOG_N(TAG, "'%s' was overwritten and is gone", r->project); + } + else if ( ((uint8_t)UNDO_BACK == me->undo->fate[i]) + && appdir_has_seq(kdrv_apps_dir(), r->seq) ) + { + KLOG_N(TAG, "kept '%s' after the failed install", r->project); + } + else + { + /* Never left the directory, or could not be put back (said above). */ + } + } + undo_clear(me); +} + +/** + * @brief Whether two extents share a 64 KB block of the region. + * + * An extent holds the blocks its length reaches into, so an image of 100 KB + * holds two. + * + * @param a_off First extent's region-relative offset. + * @param a_size Its length in bytes. + * @param b_off The second's offset. + * @param b_size Its length. + * @return true when they share a block. + */ +static bool extents_overlap(uint32_t a_off, uint32_t a_size, uint32_t b_off, + uint32_t b_size) +{ + uint64_t const mask = (uint64_t)KAPPS_ALIGN - 1U; + uint64_t const a_end = (uint64_t)a_off + (((uint64_t)a_size + mask) & ~mask); + uint64_t const b_end = (uint64_t)b_off + (((uint64_t)b_size + mask) & ~mask); + + return ((uint64_t)a_off < b_end) && ((uint64_t)b_off < a_end); +} + +/** + * @brief Whether the directory holds an entry that lies over @p r's blocks. + * + * @param d The directory. + * @param r The record of an app the directory no longer holds. + * @return true when a live entry shares a block with it. + */ +static bool appdir_overlaps(const kapps_dir_t *d, const appmgr_undo_rec_t *r) +{ + for ( uint32_t i = 0U; i < d->count; ++i ) + { + if ( ((d->e[i].flags & KAPPS_ENTRY_VALID) != 0U) + && extents_overlap(d->e[i].offset, d->e[i].size, r->offset, r->size) ) + { + return true; + } + } + return false; +} + +/** + * @brief Settle the table a start-up found: put back what verified, say what + * became of every victim, and take the table out of NVS. + * + * One directory commit for every app that comes back, in install order. A + * commit that fails leaves the table where it is, for the next boot to try + * again. + * + * @param me The AO, with its fates decided. + */ +static void undo_recover_end(AppMgr * const me) +{ + kapps_dir_t dir = *kdrv_apps_dir(); + bool changed = false; + + for ( uint32_t i = 0U; i < me->undo_count; ++i ) + { + kapps_entry_t const ent = undo_entry(&me->undo->rec[i]); + + if ( ((uint8_t)UNDO_BACK == me->undo->fate[i]) + && !appdir_has_seq(&dir, ent.seq) && (KAPPS_MAX_ENTRIES > dir.count) ) + { + appdir_insert(&dir, &ent); + changed = true; + } + else if ( (uint8_t)UNDO_BACK == me->undo->fate[i] ) + { + me->undo->fate[i] = (uint8_t)UNDO_STAYS; + } + else + { + /* Held already, overwritten or refused: nothing to add. */ + } + } + /* No path here brings the table back: it left NVS before the first check, + * and a recovery that fails, or that dies on the next boot, must not be + * asked for again by a table that is still there - the board would take + * the same road at every boot. An app the commit fails to put back is + * lost, and the board is not. */ + if ( changed && (kdrv_apps_dir_commit(&dir) != KODE_OK) ) + { + KLOG_E(TAG, "cannot update the directory; the apps a reset cut out of " + "an install stay out"); + for ( uint32_t i = 0U; i < me->undo_count; ++i ) + { + if ( (uint8_t)UNDO_BACK == me->undo->fate[i] ) + { + me->undo->fate[i] = (uint8_t)UNDO_STAYS; + } + } + } + for ( uint32_t i = 0U; i < me->undo_count; ++i ) + { + appmgr_undo_rec_t const * const r = &me->undo->rec[i]; + + if ( (uint8_t)UNDO_GONE == me->undo->fate[i] ) + { + KLOG_N(TAG, "'%s' was overwritten by an install a reset cut and is gone", + r->project); + } + else if ( (uint8_t)UNDO_BACK == me->undo->fate[i] ) + { + KLOG_N(TAG, "kept '%s' after an install a reset cut", r->project); + } + else + { + /* Held, or not put back (said above). */ + } + } + undo_clear(me); +} + +/** + * @brief Look for the table of an install a reset cut, take it out of NVS, and + * decide what is to be checked. + * + * The table is read into RAM and erased before anything else is done with it. + * One that is not this build's is ignored. Each victim the directory no longer + * holds, and that no entry the directory holds lies over, is a candidate; the + * others are settled here. This is all the initial transition pays for. + * + * @param me The AO, at start-up. + * @return true when there are candidates: AppMgr_recovering verifies them one + * a step. false when there was no table, or nothing to verify. + */ +static bool undo_recover_begin(AppMgr * const me) +{ + size_t len = offsetof(undo_table_t, fate); /* the blob's room: the fates are not in it */ + /* cppcheck-suppress misra-c2012-11.5 ; khal_alloc_psram() hands back an untyped block aligned for any type */ + undo_table_t * const t = (undo_table_t *)khal_alloc_psram(sizeof(uint32_t), + sizeof(undo_table_t)); + + if ( NULL == t ) + { + KLOG_W(TAG, "no PSRAM to look for the apps a reset cut out of an install"); + return false; + } + kode_status_t const st = khal_nvs_get_blob(APPMGR_UNDO_NS, APPMGR_UNDO_KEY, t, &len); + + if ( KODE_DENIED == st ) + { + khal_free(t); /* no table: nothing was cut */ + return false; + } + /* Out of NVS before anything is checked: whatever the checking does, the + * next boot finds no table. See undo_recover_end(). A table that cannot be + * taken out is not worked on, for the same reason. */ + if ( khal_nvs_erase(APPMGR_UNDO_NS, APPMGR_UNDO_KEY) != KODE_OK ) + { + KLOG_W(TAG, "cannot erase the record of the apps an install evicted; " + "they are not looked for"); + khal_free(t); + return false; + } + if ( (KODE_OK != st) || (sizeof(t->hdr) > len) + || ((uint8_t)APPMGR_UNDO_VERSION != t->hdr.version) + || (0U == t->hdr.count) || (KAPPS_MAX_ENTRIES < t->hdr.count) + || (len != (sizeof(t->hdr) + ((size_t)t->hdr.count * sizeof(appmgr_undo_rec_t)))) ) + { + KLOG_W(TAG, "the record of the apps an install evicted is not this " + "build's; it is ignored"); + khal_free(t); + return false; + } + me->undo = t; + me->undo_count = t->hdr.count; + me->rec_next = 0U; + me->rec_done = 0U; + me->rec_crc = KHAL_CRC32_INIT; + + kapps_dir_t const * const d = kdrv_apps_dir(); + bool work = false; + + for ( uint32_t i = 0U; i < me->undo_count; ++i ) + { + appmgr_undo_rec_t const * const r = &t->rec[i]; + + me->undo->fate[i] = (uint8_t)UNDO_STAYS; + if ( appdir_has_seq(d, r->seq) ) + { + /* Still in the directory: the eviction never reached it. */ + } + else if ( appdir_overlaps(d, r) ) + { + me->undo->fate[i] = (uint8_t)UNDO_GONE; + } + else + { + me->undo->fate[i] = (uint8_t)UNDO_CHECK; + work = true; + } + } + if ( !work ) + { + undo_recover_end(me); + } + return work; +} + +/** + * @brief One step of the recovery: fold the next part of a candidate's extent into + * its CRC, or end. + * + * The image is not parsed: what a reset left in an extent may be another + * image's header, whose lengths no parser can be trusted to stop at. The CRC + * of the extent as it lies now is compared with the one taken when the app was + * evicted. At most APPMGR_UNDO_STEP_BYTES a step, and the next step waits for + * the timer, so the AO answers what arrives between two and the other tasks + * run. + * + * @param me The AO, recovering. + * @return The state's answer: handled while there is more, idle at the end. + */ +static QState undo_step(AppMgr * const me) +{ + while ( (me->rec_next < me->undo_count) + && ((uint8_t)UNDO_CHECK != me->undo->fate[me->rec_next]) ) + { + ++me->rec_next; + } + if ( me->rec_next < me->undo_count ) + { + appmgr_undo_rec_t const * const r = &me->undo->rec[me->rec_next]; + uint32_t n = r->size - me->rec_done; + + if ( APPMGR_UNDO_STEP_BYTES < n ) + { + n = APPMGR_UNDO_STEP_BYTES; + } + KLOG_D(TAG, "checking '%s': %u bytes from %u of %u", r->project, + (unsigned)n, (unsigned)me->rec_done, (unsigned)r->size); + if ( kdrv_apps_extent_crc(r->offset + me->rec_done, n, &me->rec_crc) != KODE_OK ) + { + me->undo->fate[me->rec_next] = (uint8_t)UNDO_GONE; /* not readable where it lay */ + ++me->rec_next; + me->rec_done = 0U; + me->rec_crc = KHAL_CRC32_INIT; + } + else + { + me->rec_done += n; + if ( me->rec_done >= r->size ) + { + me->undo->fate[me->rec_next] = (uint8_t)UNDO_GONE; + if ( me->rec_crc == r->crc ) + { + me->undo->fate[me->rec_next] = (uint8_t)UNDO_BACK; + } + ++me->rec_next; + me->rec_done = 0U; + me->rec_crc = KHAL_CRC32_INIT; + } + } + (void)QTimeEvt_rearm(&me->undo_tmo, APPMGR_UNDO_PACE_TICKS); + return Q_HANDLED(); } - appdir_compact(d); + undo_recover_end(me); + for ( uint8_t k = 0U; k < me->rec_list_n; ++k ) + { + AppListReqEvt req; + + (void)memset(&req, 0, sizeof(req)); + req.skip = me->rec_list_skip[k]; + req.who = me->rec_list_who[k]; + app_list_page(&req); + } + me->rec_list_n = 0U; + if ( me->rec_adopt ) + { + static const QEvt adopt = QEVT_INITIALIZER(APP_ADOPT_SIG); + + me->rec_adopt = false; + QACTIVE_POST(&me->super, &adopt, &me->super); + } + return Q_TRAN(&AppMgr_idle); } /** @@ -1287,6 +1982,50 @@ static void xfer_reset(AppMgr * const me) me->launch = false; } +/** + * @brief Post the verdict on a request to the asker that named itself. + * + * A request that named nobody asked for none. An answer that cannot be posted + * is lost, and the asker's own watchdog says so. + * + * @param who The asker; NULL for none. + * @param verdict What AO_AppMgr made of the request. + */ +static void req_verdict(QActive * const who, app_req_verdict_t verdict) +{ + if ( NULL == who ) + { + return; + } + AppReqRspEvt * const ev = Q_NEW_X(AppReqRspEvt, 0U, APP_REQ_RSP_SIG); + + if ( NULL == ev ) + { + KLOG_W(TAG, "out of events; the verdict on a request is lost"); + return; + } + ev->verdict = (uint8_t)verdict; + if ( !QACTIVE_POST_X(who, &ev->super, 0U, AO_AppMgr) ) + { + KLOG_W(TAG, "the asker's queue is full; the verdict on a request is " + "lost"); + } +} + +/** + * @brief Give the verdict still owed for the install in hand, once. + * + * @param me The AO. + * @param verdict What AO_AppMgr made of the request. + */ +static void req_settle(AppMgr * const me, app_req_verdict_t verdict) +{ + QActive * const who = me->req_who; + + me->req_who = NULL; + req_verdict(who, verdict); +} + /** Release everything an in-flight copy owns. Safe to call twice. */ static void flash_cleanup(AppMgr * const me) { @@ -1322,6 +2061,8 @@ static void flash_fail(AppMgr * const me, const char *why, kfail_t reason) }; KLOG_E(TAG, "install failed: %s", why); flash_cleanup(me); + req_settle(me, APP_REQ_REFUSED); /* an install that never began: its asker is owed an answer */ + undo_restore(me); /* the apps its write never reached come back */ keep_clear(me); /* the install ends here; so does its round */ if ( KFAIL_KEEP == reason ) { @@ -1428,6 +2169,7 @@ static const char *region_install(AppMgr * const me, uint32_t len, { return "cannot update the directory"; } + undo_clear(me); /* the install landed: its victims are gone for good */ name[0] = '\0'; if ( KODE_OK == dst ) @@ -1508,17 +2250,39 @@ static void erase_publish(const char *name, kode_status_t st, bool image, } /** - * @brief Answer an erase that arrives while the AO is busy: KODE_DENIED, - * published, so nobody waits on an erase that will not run. + * @brief Answer a request that arrives while the AO is busy, so nobody waits + * on one that will not run. + * + * An erase is answered KODE_DENIED, published; an install or a run that asked + * for a verdict is told busy; a terminal install is failed on the bus, which + * is what its console waits for. * * @param e The request refused; any other signal is left alone. */ -static void erase_deny(QEvt const * const e) +static void busy_deny(QEvt const * const e) { - if ( (QSignal)APP_ERASE_REQ_SIG == e->sig ) + switch ( e->sig ) { - erase_publish(Q_EVT_CAST(AppFlashEvt)->file, KODE_DENIED, false, - NULL, NULL); + case APP_ERASE_REQ_SIG: { + erase_publish(Q_EVT_CAST(AppFlashEvt)->file, KODE_DENIED, false, + NULL, NULL); + break; + } + case FLASH_REQ_SIG: { + req_verdict(Q_EVT_CAST(FlashReqEvt)->who, APP_REQ_BUSY); + break; + } + case APP_RUN_REQ_SIG: { + req_verdict(Q_EVT_CAST(AppFlashEvt)->who, APP_REQ_BUSY); + break; + } + case APP_RECV_REQ_SIG: { + QACTIVE_PUBLISH(&s_recv_fail_evt, AO_AppMgr); + break; + } + default: { + break; + } } } @@ -2024,8 +2788,7 @@ static kode_status_t appmgr_place(void *ctx, uint32_t img_len, const char *name) } /* Evictions, loudly and before any flash is touched; the copy is * persisted BEFORE anything the victims occupied is erased. */ - plan_evict(me, &dir, &plan); - if ( (0U < plan.evict_count) && (kdrv_apps_dir_commit(&dir) != KODE_OK) ) + if ( !plan_commit(me, &dir, &plan) ) { KLOG_E(TAG, "cannot update the directory"); return KODE_ERROR; @@ -2296,8 +3059,13 @@ static void progress_publish(AppMgr * const me, unsigned pct) (void)snprintf(ev->project, sizeof(ev->project), "%s", me->project); } QACTIVE_PUBLISH(&ev->super, &me->super); + me->reported = pct; + } + else + { + /* Not counted as said: the next chunk or beat says it again. */ + KLOG_D(TAG, "out of events; %u%% goes unreported", pct); } - me->reported = pct; } /** @@ -2368,6 +3136,20 @@ static void progress_report(AppMgr * const me) } } +/** + * @brief A second of waiting on the network has gone by: say the percent again. + * + * The write card and the LED follow the reports alone; a download that is + * still opening, or has stopped sending, makes none of its own. + * + * @param me The AO. + */ +static void beat_say(AppMgr * const me) +{ + progress_publish(me, progress_pct(me)); + (void)QTimeEvt_rearm(&me->beat, APPMGR_WAIT_BEAT_TICKS); +} + /** * @brief Write one delivered chunk into the region. * @return true while more chunks are wanted. @@ -2526,6 +3308,7 @@ static void adopt_finish(AppMgr * const me) block_discard(); return; } + undo_clear(me); /* adopted: the apps it evicted are gone for good */ (void)kdrv_apps_pending_consume(); /* only after the commit */ KLOG_N(TAG, "dev app '%s' v%s adopted from the mailbox", desc.project, desc.version); @@ -2548,6 +3331,8 @@ void AppMgr_ctor(void) QActive_ctor(&me->super, Q_STATE_CAST(&AppMgr_initial)); QTimeEvt_ctorX(&me->pacer, &me->super, APP_FLASH_CHUNK_SIG, 0U); QTimeEvt_ctorX(&me->idle, &me->super, APP_RECV_IDLE_SIG, 0U); + QTimeEvt_ctorX(&me->beat, &me->super, APP_BEAT_SIG, 0U); + QTimeEvt_ctorX(&me->undo_tmo, &me->super, APP_UNDO_STEP_SIG, 0U); QTimeEvt_ctorX(&me->answer, &me->super, APP_RECV_LAUNCH_SIG, 0U); QTimeEvt_ctorX(&me->tidy, &me->super, APP_TIDY_TIMEOUT_SIG, 0U); me->writing = false; @@ -2579,6 +3364,15 @@ void AppMgr_ctor(void) me->tidy_ad.icon_len = 0U; me->tidy_swallow = false; me->victims = NULL; /* a used AO's table is the heap's now */ + me->undo = NULL; /* and so is the one an install gives back from */ + me->undo_count = 0U; + me->undo_stored = false; + me->rec_next = 0U; + me->rec_done = 0U; + me->rec_crc = KHAL_CRC32_INIT; + me->rec_adopt = false; + me->rec_list_n = 0U; + me->req_who = NULL; keep_clear(me); /* no round: a constructor over a used AO frees its buffer */ me->origin = (uint8_t)APPMGR_ORIGIN_FILE; #if CONFIG_KODE_LOWPOWER @@ -2640,12 +3434,29 @@ static QState AppMgr_initial(AppMgr * const me, void const * const par) * Posted to self rather than done here - a multi-MB flash copy has no * business inside an initial transition. */ uint32_t img_len = 0U; + /* A table in NVS is an install a reset cut: its apps are looked at before + * anything is served, the adoption included. */ + (void)khal_nvs_init(); + bool const recovering = undo_recover_begin(me); if ( kdrv_apps_pending_present(&img_len, NULL) ) { static const QEvt adopt_evt = QEVT_INITIALIZER(APP_ADOPT_SIG); - QACTIVE_POST(&me->super, &adopt_evt, me); + + if ( recovering ) + { + me->rec_adopt = true; + } + else + { + QACTIVE_POST(&me->super, &adopt_evt, me); + } } + if ( recovering ) + { + KLOG_N(TAG, "verifying the apps a reset cut out of an install"); + return Q_TRAN(&AppMgr_recovering); + } KLOG_D(TAG, "FSM start: initial -> idle"); return Q_TRAN(&AppMgr_idle); } @@ -2851,7 +3662,8 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) xfer_reset(me); /* a leaked bypass from a failed park must not * make this install skip the decoder */ - me->origin = (uint8_t)APPMGR_ORIGIN_CONSOLE; + me->origin = (uint8_t)APPMGR_ORIGIN_CONSOLE; + me->recv_src = req->src; keep_clear(me); me->os_target = to_os; me->total = req->size; @@ -2920,6 +3732,7 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) keep_clear(me); me->keep_req = *req; /* what a restart would send again */ } + me->req_who = req->who; /* owed a verdict, settled below or in AppMgr_opening */ me->origin = (uint8_t)APPMGR_ORIGIN_FILE; if ( (uint8_t)KFW_SRC_HTTP == req->src_kind ) { @@ -2935,6 +3748,16 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) if ( (uint8_t)KFW_TARGET_C5 == req->dst_kind ) { KLOG_E(TAG, "dst=c5 belongs to AO_Radio; the console routes it there"); + req_settle(me, APP_REQ_REFUSED); + status = Q_HANDLED(); + break; + } + if ( !kdrv_apps_pending_present(NULL, NULL) ) + { + /* Said here, where the asker can be told: the adoption + * would find the same and could only log it. */ + KLOG_W(TAG, "the mailbox is empty: nothing to install"); + req_settle(me, APP_REQ_EMPTY); status = Q_HANDLED(); break; } @@ -2943,6 +3766,7 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) me->adopt_run = req->launch; static const QEvt adopt = QEVT_INITIALIZER(APP_ADOPT_SIG); QACTIVE_POST(&me->super, &adopt, &me->super); + req_settle(me, APP_REQ_TAKEN); status = Q_HANDLED(); break; } @@ -2953,6 +3777,7 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) && ((uint8_t)KFW_SRC_SD != req->src_kind) ) { KLOG_E(TAG, "that source is not wired up yet"); + req_settle(me, APP_REQ_REFUSED); status = Q_HANDLED(); break; } @@ -2960,6 +3785,7 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) if ( me->from_net ) { KLOG_E(TAG, "this firmware has no network services"); + req_settle(me, APP_REQ_REFUSED); status = Q_HANDLED(); break; } @@ -3003,6 +3829,7 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) /* Only reachable from kode_sys_flash() or a stale caller: the * console routes dst=c5 straight to AO_Radio. */ KLOG_E(TAG, "the coprocessor is AO_Radio's, not this AO's"); + req_settle(me, APP_REQ_REFUSED); status = Q_HANDLED(); break; } @@ -3022,6 +3849,7 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) { KLOG_E(TAG, "'%s' is longer than a file name may be (%u)", req->src, (unsigned)(sizeof(me->file) - 1U)); + req_settle(me, APP_REQ_REFUSED); status = Q_HANDLED(); break; } @@ -3039,7 +3867,20 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) #if CONFIG_KODEOS_NET if ( me->from_net ) { + if ( !me->keep_round ) + { + me->total = 0U; + me->written = 0U; + me->reported = 0U; + me->next_report = APPMGR_PROGRESS; + } + /* The card and the LED from this moment, not from the first + * byte: DNS, TLS and the headers come before it. */ + progress_publish(me, progress_pct(me)); net_stream_open_request(req->src, &me->super); + /* The download's own failures are asynchronous: the verdict + * is that it has begun, not that it will end well. */ + req_settle(me, APP_REQ_TAKEN); } else { @@ -3067,9 +3908,11 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) { KLOG_E(TAG, "'%s' is not installed - `app list` knows what is", req->file); + req_verdict(req->who, APP_REQ_MISSING); } else { + req_verdict(req->who, APP_REQ_TAKEN); /* before the board resets */ boot_app_run_request(d->e[idx].offset, d->e[idx].size, KBOOT_RUN_ONCE); } /* AO_Boot resets */ @@ -3140,8 +3983,17 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) uint32_t decoded_len = 0U; if ( !kdrv_apps_pending_present(&img_len, &decoded_len) ) { - /* Gone, or the driver just found it broken and consumed it - - * either way it already said why. */ + /* Gone, or the driver just found it broken and consumed it, + * and then it said why. An empty one says nothing by itself: + * every boot looks, so only an install that was asked for is + * told there was nothing to install. What the request set is + * dropped with it, or the next adoption would take it. */ + if ( me->dst_forced ) + { + KLOG_W(TAG, "the mailbox is empty: nothing to install"); + } + me->dst_forced = false; + me->adopt_run = false; keep_clear(me); status = Q_HANDLED(); break; @@ -3259,9 +4111,7 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) status = Q_TRAN(&AppMgr_keeping); break; } - plan_evict(me, &dir, &plan); - if ( (0U < plan.evict_count) - && (kdrv_apps_dir_commit(&dir) != KODE_OK) ) + if ( !plan_commit(me, &dir, &plan) ) { KLOG_E(TAG, "cannot update the directory; mailbox left intact"); keep_clear(me); @@ -3315,6 +4165,18 @@ static QState AppMgr_opening(AppMgr * const me, QEvt const * const e) { case Q_ENTRY_SIG: { QTimeEvt_armX(&me->idle, APPMGR_OPEN_TICKS, 0U); + if ( me->from_net ) + { + (void)QTimeEvt_rearm(&me->beat, APPMGR_WAIT_BEAT_TICKS); + } + status = Q_HANDLED(); + break; + } + case APP_BEAT_SIG: { + if ( me->from_net ) + { + beat_say(me); + } status = Q_HANDLED(); break; } @@ -3325,6 +4187,7 @@ static QState AppMgr_opening(AppMgr * const me, QEvt const * const e) } case Q_EXIT_SIG: { QTimeEvt_disarm(&me->idle); + (void)QTimeEvt_disarm(&me->beat); status = Q_HANDLED(); break; } @@ -3348,6 +4211,7 @@ static QState AppMgr_opening(AppMgr * const me, QEvt const * const e) ? "'%s' did not come back from the server" : "'%s' is not on the SD card nor in the " "internal storage", me->file); + req_settle(me, APP_REQ_MISSING); /* a URL's was settled when it opened */ flash_fail(me, me->from_net ? "the server said no" : "file not found", KFAIL_WRITE); @@ -3384,6 +4248,7 @@ static QState AppMgr_opening(AppMgr * const me, QEvt const * const e) status = Q_TRAN(&AppMgr_idle); break; } + req_settle(me, APP_REQ_TAKEN); /* the file is open and the write has begun */ me->writing = true; me->pacing = false; @@ -3422,9 +4287,10 @@ static QState AppMgr_opening(AppMgr * const me, QEvt const * const e) case FLASH_REQ_SIG: case APP_RUN_REQ_SIG: case APP_ERASE_REQ_SIG: - case APP_LIST_REQ_SIG: { + case APP_LIST_REQ_SIG: + case APP_RECV_REQ_SIG: { KLOG_W(TAG, "busy opening '%s'; request ignored", me->file); - erase_deny(e); + busy_deny(e); status = Q_HANDLED(); break; } @@ -3489,6 +4355,7 @@ static void keep_restart(AppMgr * const me) r->src_kind = me->keep_req.src_kind; r->dst_kind = me->keep_req.dst_kind; r->launch = me->keep_req.launch; + r->who = NULL; /* the verdict was given with the first open */ (void)snprintf(r->src, sizeof(r->src), "%s", me->keep_req.src); /* LIFO: a request already queued must not run between the copies and * the install they were made for. */ @@ -3596,16 +4463,54 @@ static void card_stat(AppMgr * const me, const char *path) } /** - * @brief Does a STAT's answer say a copy is on the card? + * @brief Does a STAT's answer say a file of the name is on the card? * @param r The answer. * @return true for a file that is there; false for no card, a failed STAT, * nothing at the path, or a folder where the file would be. */ -static bool card_has_copy(const StoragePathRspEvt *r) +static bool card_has_file(const StoragePathRspEvt *r) { return ((uint8_t)KODE_OK == r->rc) && r->exists && !r->is_dir; } +/** + * @brief Does a STAT's answer say the card holds a copy of an installed image? + * + * A file of the app's name is its copy only at the image's length. An older + * version of the app, kept by an earlier eviction, or another file of that + * name, is something else: the app evicted on its account would be lost. + * + * @param r The answer. + * @param image_len The installed image's length in bytes. + * @return true for a file that is there and as long as the image. + */ +static bool card_has_copy(const StoragePathRspEvt *r, uint32_t image_len) +{ + return card_has_file(r) && (r->size == image_len); +} + +/** + * @brief The length of the installed image that lies at @p offset. + * + * @param d The directory. + * @param offset The extent's region-relative offset. + * @return The entry's size in bytes, 0 when no live entry lies there. + */ +static uint32_t appdir_size_at(const kapps_dir_t *d, uint32_t offset) +{ + uint32_t size = 0U; + + for ( uint32_t i = 0U; i < d->count; ++i ) + { + if ( ((d->e[i].flags & KAPPS_ENTRY_VALID) != 0U) && (d->e[i].offset == offset) ) + { + size = d->e[i].size; + break; + } + } + return size; +} + /** * @brief Start on the next untried victim. * @return true when one was started (a STAT is out), false when none is left. @@ -3768,7 +4673,7 @@ static QState AppMgr_keeping(AppMgr * const me, QEvt const * const e) } me->keep_stat = false; (void)QTimeEvt_disarm(&me->idle); - if ( card_has_copy(r) ) + if ( card_has_copy(r, me->keep_len) ) { KLOG_I(TAG, "'%s' is already on the card", me->keep_name); keep_settle(me, KEEP_THERE); @@ -3777,7 +4682,7 @@ static QState AppMgr_keeping(AppMgr * const me, QEvt const * const e) { keep_settle(me, KEEP_FAILED); /* no card, or it will not mount */ } - else if ( r->exists ) + else if ( r->exists && r->is_dir ) { /* A folder of that name is no copy, and a file cannot be * written over it. */ @@ -3787,6 +4692,14 @@ static QState AppMgr_keeping(AppMgr * const me, QEvt const * const e) } else { + if ( r->exists ) + { + /* Another file of the name: an older version, or not the + * app at all. The copy truncates it. */ + KLOG_N(TAG, "'%s' is %u bytes and the image is %u; the " + "app is copied over it", me->keep_path, + (unsigned)r->size, (unsigned)me->keep_len); + } /* Not zeroed: every byte is read from the region before it * is written, and clearing up to 13 MB is no step (rule 5). */ /* cppcheck-suppress misra-c2012-11.5 ; a byte buffer from a void * allocator */ @@ -3907,7 +4820,7 @@ static QState AppMgr_keeping(AppMgr * const me, QEvt const * const e) case APP_LIST_REQ_SIG: case APP_RECV_REQ_SIG: { KLOG_W(TAG, "busy keeping apps on the card; request ignored"); - erase_deny(e); + busy_deny(e); status = Q_HANDLED(); break; } @@ -4130,7 +5043,7 @@ static bool busy_refused(AppMgr * const me, QEvt const * const e, case APP_LIST_REQ_SIG: case APP_RECV_REQ_SIG: { KLOG_W(TAG, "busy %s; request ignored", doing); - erase_deny(e); + busy_deny(e); break; } case APP_CAT_REQ_SIG: { @@ -4454,6 +5367,7 @@ static QState get_fail(AppMgr * const me, appdata_get_why_t why) } get_say(me->get_project, why); get_reply(me->get_who, me->get_project, why, false); + QACTIVE_PUBLISH(&s_flash_fail_evt.super, &me->super); /* the card the fetch raised ends too */ return Q_TRAN(&AppMgr_idle); } @@ -4681,6 +5595,18 @@ static QState get_begin(AppMgr * const me, AppDataGetReqEvt const * const req) (void)snprintf(me->get_project, sizeof(me->get_project), "%s", req->project); (void)snprintf(me->get_url, (size_t)KODE_APPDATA_URL_MAX, "%s", req->url); KLOG_N(TAG, "fetching the data folder of '%s'", me->get_project); + /* The card and the LED from this moment: the first file's open can take + * as long as a download's. The reports name the folder's app. */ + me->origin = (uint8_t)APPMGR_ORIGIN_NET; + me->os_target = false; + me->launch = false; + me->file[0] = '\0'; + (void)snprintf(me->project, sizeof(me->project), "%s", me->get_project); + me->total = 0U; + me->written = 0U; + me->reported = 0U; + me->next_report = APPMGR_PROGRESS; + progress_publish(me, 0U); get_open_file(me, GET_JSON_NAME); return Q_TRAN(&AppMgr_fetching); #else @@ -4709,6 +5635,12 @@ static QState AppMgr_fetching(AppMgr * const me, QEvt const * const e) { case Q_ENTRY_SIG: { QTimeEvt_armX(&me->idle, APPMGR_OPEN_TICKS, 0U); + (void)QTimeEvt_rearm(&me->beat, APPMGR_WAIT_BEAT_TICKS); + status = Q_HANDLED(); + break; + } + case APP_BEAT_SIG: { + beat_say(me); status = Q_HANDLED(); break; } @@ -4762,6 +5694,16 @@ static QState AppMgr_fetching(AppMgr * const me, QEvt const * const e) get_reply(me->get_who, me->get_project, why, (APPDATA_GET_OK == why) && (0U != me->get_icon_len)); + /* The card the fetch raised ends too; the done names no + * app, as a park's does: nothing was installed. */ + if ( APPDATA_GET_OK == why ) + { + QACTIVE_PUBLISH(&s_flash_done_evt.super, &me->super); + } + else + { + QACTIVE_PUBLISH(&s_flash_fail_evt.super, &me->super); + } status = Q_TRAN(&AppMgr_idle); } } @@ -4788,6 +5730,7 @@ static QState AppMgr_fetching(AppMgr * const me, QEvt const * const e) } case Q_EXIT_SIG: { (void)QTimeEvt_disarm(&me->idle); + (void)QTimeEvt_disarm(&me->beat); get_release(me); status = Q_HANDLED(); break; @@ -4808,6 +5751,31 @@ static QState AppMgr_fetching(AppMgr * const me, QEvt const * const e) } #endif /* CONFIG_KODEOS_NET */ +/** + * @brief What a request that asks for an install or a run is called in the log. + * + * @param sig FLASH_REQ_SIG, APP_RUN_REQ_SIG or APP_RECV_REQ_SIG. + * @return "install", "run" or "terminal install". + */ +static const char *request_name(QSignal sig) +{ + const char *name = "terminal install"; + + if ( (QSignal)FLASH_REQ_SIG == sig ) + { + name = "install"; + } + else if ( (QSignal)APP_RUN_REQ_SIG == sig ) + { + name = "run"; + } + else + { + /* APP_RECV_REQ_SIG. */ + } + return name; +} + /** * @brief The parent of every state with work in hand. * @@ -4830,7 +5798,16 @@ static QState AppMgr_busy(AppMgr * const me, QEvt const * const e) /* A state under this one that names no refusal of its own. */ KLOG_W(TAG, "busy; the erase of '%s' is refused", Q_EVT_CAST(AppFlashEvt)->file); - erase_deny(e); + busy_deny(e); + status = Q_HANDLED(); + break; + } + case FLASH_REQ_SIG: + case APP_RUN_REQ_SIG: + case APP_RECV_REQ_SIG: { + /* Likewise: an install or a run is told it was not taken. */ + KLOG_W(TAG, "busy; the %s request is refused", request_name(e->sig)); + busy_deny(e); status = Q_HANDLED(); break; } @@ -4895,6 +5872,71 @@ static QState AppMgr_busy(AppMgr * const me, QEvt const * const e) return status; } +/** + * @brief Verifying the apps a reset cut out of an install, before anything is served. + * + * Entered from the initial transition when NVS held the table of an install + * that did not end. A step, started by a timer, folds a part of one image's + * extent into its CRC; every request but a listing is + * refused as busy, and a listing waits for the directory the recovery leaves, + * so the launcher is never given a list that is about to change. + */ +static QState AppMgr_recovering(AppMgr * const me, QEvt const * const e) +{ + QState status; + switch ( e->sig ) + { + case Q_ENTRY_SIG: { + /* The first step is the timer's: the initial transition, which a + * boot task runs for every owner, has done nothing but read NVS. */ + (void)QTimeEvt_rearm(&me->undo_tmo, APPMGR_UNDO_PACE_TICKS); + status = Q_HANDLED(); + break; + } + case Q_EXIT_SIG: { + (void)QTimeEvt_disarm(&me->undo_tmo); + status = Q_HANDLED(); + break; + } + case APP_UNDO_STEP_SIG: { + status = undo_step(me); + break; + } + case APP_LIST_REQ_SIG: { + AppListReqEvt const * const req = Q_EVT_CAST(AppListReqEvt); + + if ( NULL != req->who ) + { + if ( APPMGR_REC_LIST_MAX > me->rec_list_n ) + { + me->rec_list_who[me->rec_list_n] = req->who; + me->rec_list_skip[me->rec_list_n] = req->skip; + ++me->rec_list_n; + } + else + { + KLOG_W(TAG, "too many listings asked while the apps a reset " + "cut out of an install are verified; one is dropped"); + } + } + status = Q_HANDLED(); + break; + } + case APP_CAT_REQ_SIG: { + KLOG_W(TAG, "busy verifying the apps a reset cut out of an install; " + "app_cat request denied"); + app_cat_deny(me); + status = Q_HANDLED(); + break; + } + default: { + status = Q_SUPER(&AppMgr_busy); + break; + } + } + return status; +} + /** * @brief Waiting for AO_Storage to say whether the victim in hand has its copy * on the card. @@ -4918,7 +5960,9 @@ static QState AppMgr_asking(AppMgr * const me, QEvt const * const e) if ( card_answer_taken(me, e, &me->ask_stat) ) { evict_entry(me->ask_who, me->ask_name, - card_has_copy(Q_EVT_CAST(StoragePathRspEvt))); + card_has_copy(Q_EVT_CAST(StoragePathRspEvt), + appdir_size_at(kdrv_apps_dir(), + me->ask_off[me->ask_idx]))); ++me->ask_idx; if ( !evict_next(me) ) { @@ -4979,8 +6023,10 @@ static QState AppMgr_finding(AppMgr * const me, QEvt const * const e) status = Q_HANDLED(); if ( card_answer_taken(me, e, &me->find_stat) ) { + /* By name alone: the app is not installed, so there is no + * image whose length the file could be held to. */ find_reply(me->find_who, me->find_name, NULL, - card_has_copy(Q_EVT_CAST(StoragePathRspEvt)), false); + card_has_file(Q_EVT_CAST(StoragePathRspEvt)), false); me->find_who = NULL; status = Q_TRAN(&AppMgr_idle); } @@ -5177,7 +6223,7 @@ static QState AppMgr_tidying(AppMgr * const me, QEvt const * const e) case APP_RECV_REQ_SIG: { KLOG_W(TAG, "busy tidying the data folder of '%s'; request ignored", me->tidy_name); - erase_deny(e); + busy_deny(e); status = Q_HANDLED(); break; } @@ -5216,11 +6262,25 @@ static QState AppMgr_flashing(AppMgr * const me, QEvt const * const e) { case Q_ENTRY_SIG: { QTimeEvt_armX(&me->idle, APPMGR_CHUNK_TICKS, 0U); + if ( me->from_net ) + { + (void)QTimeEvt_rearm(&me->beat, APPMGR_WAIT_BEAT_TICKS); + } status = Q_HANDLED(); break; } case Q_EXIT_SIG: { (void)QTimeEvt_disarm(&me->idle); + (void)QTimeEvt_disarm(&me->beat); + status = Q_HANDLED(); + break; + } + case APP_BEAT_SIG: { + /* A download that stops sending makes no report of its own. */ + if ( me->from_net ) + { + beat_say(me); + } status = Q_HANDLED(); break; } @@ -5284,11 +6344,12 @@ static QState AppMgr_flashing(AppMgr * const me, QEvt const * const e) case FLASH_REQ_SIG: case APP_RUN_REQ_SIG: case APP_ERASE_REQ_SIG: - case APP_LIST_REQ_SIG: { + case APP_LIST_REQ_SIG: + case APP_RECV_REQ_SIG: { KLOG_W(TAG, "busy writing %s (%u%%); request ignored", me->os_target ? "the kodeOS slot" : "the region", (unsigned)((me->written * 100U) / me->total)); - erase_deny(e); + busy_deny(e); status = Q_HANDLED(); break; } @@ -5351,6 +6412,7 @@ static QState AppMgr_adopting(AppMgr * const me, QEvt const * const e) * mailbox stays, and the next boot copies it again. */ xfer_abort(me); me->os_target = false; + undo_restore(me); keep_clear(me); KLOG_E(TAG, "adoption failed mid-copy; mailbox left intact"); status = Q_TRAN(&AppMgr_idle); @@ -5367,6 +6429,7 @@ static QState AppMgr_adopting(AppMgr * const me, QEvt const * const e) adopt_finish(me); if ( !me->tidy_on ) { + undo_restore(me); /* a failed adoption gives back what its write never reached */ keep_clear(me); /* every way adoption ends ends its round; * a tidying one ends it in tidy_finish() */ } @@ -5380,7 +6443,7 @@ static QState AppMgr_adopting(AppMgr * const me, QEvt const * const e) case APP_LIST_REQ_SIG: case APP_RECV_REQ_SIG: { KLOG_W(TAG, "adopting the mailbox app; request ignored"); - erase_deny(e); + busy_deny(e); status = Q_HANDLED(); break; } @@ -5398,6 +6461,78 @@ static QState AppMgr_adopting(AppMgr * const me, QEvt const * const e) return status; } +/** + * @brief The silence a terminal transfer may keep, in QP ticks, by the port it rides. + * + * @param me The AO, with the request's port noted. + * @return APPMGR_RECV_IDLE_BLE_TICKS over BLE, APPMGR_RECV_IDLE_TICKS otherwise. + */ +static uint32_t recv_idle_ticks(const AppMgr * const me) +{ + uint32_t ticks = APPMGR_RECV_IDLE_TICKS; + + if ( (uint8_t)KODE_CONSOLE_SRC_BLE == me->recv_src ) + { + ticks = APPMGR_RECV_IDLE_BLE_TICKS; + } + return ticks; +} + +/** + * @brief The same bound in milliseconds, for the log. + * + * @param me The AO, with the request's port noted. + * @return KODE_RECV_IDLE_BLE_MS over BLE, KODE_RECV_IDLE_MS otherwise. + */ +static uint32_t recv_idle_ms(const AppMgr * const me) +{ + uint32_t ms = KODE_RECV_IDLE_MS; + + if ( (uint8_t)KODE_CONSOLE_SRC_BLE == me->recv_src ) + { + ms = KODE_RECV_IDLE_BLE_MS; + } + return ms; +} + +/** + * @brief Whether the launch must wait, because the answer to a BLE install is + * still queued for the link. + * + * The `>ok` the host waits for goes out before the board resets into the new + * app; one still queued then is lost, and the phone retries against a running + * app. The wait is bounded: a link that never drains does not hold the board. + * + * @param me The AO. + * @return true to look again after another APPMGR_ANSWER_DRAIN_TICKS. + */ +static bool launch_waits(AppMgr * const me) +{ + bool waits = false; + +#if CONFIG_KODEOS_BLE_CONSOLE + if ( ((uint8_t)KODE_CONSOLE_SRC_BLE == me->recv_src) + && (0U < kdrv_ble_console_backlog()) ) + { + if ( me->answer_polls < (APPMGR_ANSWER_BLE_TICKS / APPMGR_ANSWER_DRAIN_TICKS) ) + { + ++me->answer_polls; + waits = true; + } + else + { + KLOG_W(TAG, "the BLE console still holds %u bytes after %u ms; " + "launching anyway", + (unsigned)kdrv_ble_console_backlog(), + (unsigned)KODE_RECV_DRAIN_BLE_MS); + } + } +#else + (void)me; +#endif + return waits; +} + /** * @brief Close an install: verify or abort the write session, publish the * outcome, and say what to do next. @@ -5488,6 +6623,7 @@ static QState recv_close(AppMgr * const me, bool ok, bool stalled) me->os_target = false; me->bypass = false; + undo_restore(me); /* the apps its write never reached come back */ keep_clear(me); /* a failed install owes no folder to the apps it evicted */ QACTIVE_PUBLISH(stalled ? &s_recv_stall_evt : &s_recv_fail_evt, &me->super); if ( announced ) @@ -5546,7 +6682,7 @@ static QState AppMgr_receiving(AppMgr * const me, QEvt const * const e) break; } me->writing = true; - (void)QTimeEvt_rearm(&me->idle, APPMGR_RECV_IDLE_TICKS); + (void)QTimeEvt_rearm(&me->idle, recv_idle_ticks(me)); QACTIVE_PUBLISH(&s_recv_ready_evt, &me->super); progress_publish(me, 0U); status = Q_HANDLED(); @@ -5566,9 +6702,8 @@ static QState AppMgr_receiving(AppMgr * const me, QEvt const * const e) status = recv_close(me, false, false); break; } - plan_evict(me, &dir, &plan); /* the stream cannot wait: no copies */ - if ( (0U < plan.evict_count) - && (kdrv_apps_dir_commit(&dir) != KODE_OK) ) + /* The stream cannot wait: no copies. */ + if ( !plan_commit(me, &dir, &plan) ) { KLOG_E(TAG, "install failed: cannot update the directory"); QACTIVE_POST(AO_Log, &s_raw_end_evt, &me->super); @@ -5593,7 +6728,7 @@ static QState AppMgr_receiving(AppMgr * const me, QEvt const * const e) * from the start it would have been counting through the erases * above, and a big extent erases for longer than the whole * deadline. */ - (void)QTimeEvt_rearm(&me->idle, APPMGR_RECV_IDLE_TICKS); + (void)QTimeEvt_rearm(&me->idle, recv_idle_ticks(me)); /* The host may start streaming. This is `>|ready`, and the 0 % * that raises the Opening sheet and the LED's pulse - the write * is announced with the wire, never before it. */ @@ -5607,7 +6742,7 @@ static QState AppMgr_receiving(AppMgr * const me, QEvt const * const e) /* Still hearing from the host: push the deadline out. rearm, not * armX - armX asserts on a timer that is already ticking * (Q_REQUIRE 450 in qf_time.c), and this one always is here. */ - (void)QTimeEvt_rearm(&me->idle, APPMGR_RECV_IDLE_TICKS); + (void)QTimeEvt_rearm(&me->idle, recv_idle_ticks(me)); me->recv_crc = khal_crc32(me->recv_crc, d->data, d->len); @@ -5672,7 +6807,7 @@ static QState AppMgr_receiving(AppMgr * const me, QEvt const * const e) case APP_RECV_IDLE_SIG: { KLOG_E(TAG, "install failed: no data for %u ms at %u/%u bytes; " "the host is presumed gone", - (unsigned)KODE_RECV_IDLE_MS, + (unsigned)recv_idle_ms(me), (unsigned)me->recv_wire, (unsigned)me->recv_size); /* The reader task may still be mid-transfer; tell AO_Log to * abandon raw mode rather than leave it waiting for bytes that @@ -5682,12 +6817,17 @@ static QState AppMgr_receiving(AppMgr * const me, QEvt const * const e) status = recv_close(me, false, true); break; } - /* Unlike AppMgr_opening/flashing/adopting, this state does not - * enumerate the other fire-and-forget requests (FLASH_REQ_SIG and - * friends already fall silently to Q_SUPER here, pre-existing and - * out of scope for this fix) - but app_cat is the one request a - * console command waits synchronously on, so it still must not be - * dropped without an answer while an image is streaming in. */ + case FLASH_REQ_SIG: + case APP_RUN_REQ_SIG: + case APP_RECV_REQ_SIG: { + KLOG_W(TAG, "busy receiving an installed image; request ignored"); + busy_deny(e); + status = Q_HANDLED(); + break; + } + /* app_cat is a request a console command waits synchronously on, so + * it must not be dropped without an answer while an image is + * streaming in. */ case APP_CAT_REQ_SIG: { KLOG_W(TAG, "busy receiving an installed image; " "app_cat request denied"); @@ -5716,11 +6856,18 @@ static QState AppMgr_launching(AppMgr * const me, QEvt const * const e) switch ( e->sig ) { case Q_ENTRY_SIG: { + me->answer_polls = 0U; QTimeEvt_armX(&me->answer, APPMGR_ANSWER_DRAIN_TICKS, 0U); status = Q_HANDLED(); break; } case APP_RECV_LAUNCH_SIG: { + if ( launch_waits(me) ) + { + (void)QTimeEvt_rearm(&me->answer, APPMGR_ANSWER_DRAIN_TICKS); + status = Q_HANDLED(); + break; + } if ( me->os_target ) { /* `dst=os`: the launch is AO_Boot's switch. Back to @@ -5736,6 +6883,14 @@ static QState AppMgr_launching(AppMgr * const me, QEvt const * const e) status = Q_TRAN(&AppMgr_idle); /* only reached if it could not */ break; } + case FLASH_REQ_SIG: + case APP_RUN_REQ_SIG: + case APP_RECV_REQ_SIG: { + KLOG_W(TAG, "busy launching an installed app; request ignored"); + busy_deny(e); + status = Q_HANDLED(); + break; + } default: { status = Q_SUPER(&AppMgr_busy); break; diff --git a/components/kaos/appmgr/src/appmgr_verbs.c b/components/kaos/appmgr/src/appmgr_verbs.c index deba130..023e2b4 100644 --- a/components/kaos/appmgr/src/appmgr_verbs.c +++ b/components/kaos/appmgr/src/appmgr_verbs.c @@ -9,6 +9,7 @@ #include "kaos.h" #include "kapp_cat.h" +#include "kdrv_kfw.h" #include "kode_log.h" #include "kqp.h" @@ -21,13 +22,15 @@ #define APPMGR_V_BYTES_PER_KB 1024U /**< `size_kb=`'s unit. */ #define APPMGR_V_ERASE_MARGIN_MS 5000U /**< What `app_erase` waits past AO_AppMgr's own bound on AO_Storage. */ #define APPMGR_V_ERASE_MS (APPMGR_WIPE_MS + APPMGR_V_ERASE_MARGIN_MS) /**< `app_erase`'s deadline. */ +#define APPMGR_V_RUN_MS (KODE_APPMGR_OPEN_MS + APPMGR_V_ERASE_MARGIN_MS) /**< `app_run`'s: AO_AppMgr's bound on a medium's open, and the margin. */ /** @brief Which app verb holds the console. */ typedef enum { APPMGR_V_NONE = 0, /**< None of them. */ APPMGR_V_LIST, /**< app_list: APP_ENTRY_SIG pages until the end. */ APPMGR_V_CAT, /**< app_cat set/clear: closed by APP_CAT_RSP_SIG. */ - APPMGR_V_ERASE /**< app_erase: closed by APP_ERASE_DONE_SIG. */ + APPMGR_V_ERASE, /**< app_erase: closed by APP_ERASE_DONE_SIG. */ + APPMGR_V_RUN /**< app_run: closed by APP_REQ_RSP_SIG. */ } appmgr_verb_kind_t; static appmgr_verb_kind_t s_appmgr_kind; @@ -55,19 +58,36 @@ static void h_app_list(const kverb_t *c, const karg_val_t *v) appmgr_list_request(AO_Console); } -/** @brief app_run name=: a .bin is flashed then booted. */ +/** + * @brief app_run name=: a .bin is flashed then booted. + * + * The line stays open until AO_AppMgr says whether it took the request: `>ok` + * when the app is on its way to boot, `missing` for a name nobody installed or + * a file that is not on a medium, `busy` when AO_AppMgr has work in hand. + */ static void h_app_run(const kverb_t *c, const karg_val_t *v) { - (void)c; + bool posted; + + console_begin(c->verb); + console_extend(APPMGR_V_RUN_MS); + s_appmgr_kind = APPMGR_V_RUN; if ( console_is_bin(v[0].s) ) { - appmgr_flash_request(v[0].s, true); /* flash, then boot it */ + /* Flash, then boot it. */ + posted = appmgr_image_ask((uint8_t)KFW_SRC_FS, v[0].s, + (uint8_t)KFW_TARGET_APP, true, AO_Console); } else { - appmgr_run_request(v[0].s); + posted = appmgr_run_ask(v[0].s, AO_Console); + } + if ( !posted ) + { + /* Nothing was posted, so nobody will answer. */ + console_reply_err(KCLI_ERR_REJECT, c->verb); + console_end(); } - console_reply(">ok"); } /** @@ -363,6 +383,19 @@ static bool appmgr_erase_answer(QEvt const *e) return true; } +/** @brief app_run's answer: AO_AppMgr's verdict on the request. */ +static bool appmgr_run_answer(QEvt const *e) +{ + bool handled = false; + + if ( (QSignal)APP_REQ_RSP_SIG == e->sig ) + { + console_reply_app_verdict(Q_EVT_CAST(AppReqRspEvt)->verdict); + handled = true; + } + return handled; +} + /** @brief AO_AppMgr's answers to the verb in flight. */ static bool appmgr_answer(QEvt const *e) { @@ -380,6 +413,10 @@ static bool appmgr_answer(QEvt const *e) { handled = appmgr_erase_answer(e); } + else if ( APPMGR_V_RUN == s_appmgr_kind ) + { + handled = appmgr_run_answer(e); + } else { /* None of them. */ diff --git a/components/kaos/console/src/console_ao.c b/components/kaos/console/src/console_ao.c index 5fd926b..6b24f1f 100644 --- a/components/kaos/console/src/console_ao.c +++ b/components/kaos/console/src/console_ao.c @@ -85,8 +85,8 @@ KAOS_WEAK void storage_path_request(QActive *who, uint8_t op, const char *path); KAOS_WEAK void storage_read_request(void); KAOS_WEAK void storage_write_all_request(const char *name, const void *data, uint32_t len, QActive *who); -KAOS_WEAK void appmgr_image_request(uint8_t src_kind, const char *src, - uint8_t dst_kind, bool launch); +KAOS_WEAK bool appmgr_image_ask(uint8_t src_kind, const char *src, + uint8_t dst_kind, bool launch, QActive *who); KAOS_WEAK void appmgr_list_page(uint16_t skip, QActive *who); KAOS_WEAK void appmgr_list_request(QActive *who); KAOS_WEAK bool appmgr_recv_request(uint8_t dst, uint32_t size, uint32_t crc32, @@ -286,6 +286,8 @@ typedef enum { * APPDATA_END_SIG. */ KCLI_APPDATA_RM, /**< appdata_rm: closed by APPDATA_RM_RSP_SIG. */ KCLI_APPDATA_GET, /**< appdata_get: closed by APPDATA_GET_RSP_SIG. */ + KCLI_INSTALL, /**< install off a medium or the mailbox: closed by + * AO_AppMgr's verdict, APP_REQ_RSP_SIG. */ KCLI_OWNER, /**< A registered verb: its owner's answer hook sees * the events, and console_end() closes it. */ } kcli_inflight_t; @@ -1778,6 +1780,49 @@ static void install_from_wire(Console *me, const kcmd_t *c, uint8_t dst, } } +/** How long `install` waits for AO_AppMgr's verdict: the deadline AO_AppMgr gives a + * medium to open a file, then the console's own. AO_AppMgr answers every + * request it takes, so this only bounds a lost answer. */ +#define KCLI_INSTALL_MS (KODE_APPMGR_OPEN_MS + KCLI_CMD_TIMEOUT_MS) + +/** + * @brief Close the command in flight with AO_AppMgr's verdict on its request. + * + * `>ok` for a request taken; code 4 `busy` when AO_AppMgr had work in hand; + * code 5 `missing` for a file that is not on the medium or an app that is not + * installed, `empty` for a mailbox with nothing in it, `refused` for anything + * else AO_AppMgr turned down. + * + * @param verdict app_req_verdict_t. + */ +void console_reply_app_verdict(uint8_t verdict) +{ + switch ( (app_req_verdict_t)verdict ) + { + case APP_REQ_TAKEN: { + console_reply(">ok"); + break; + } + case APP_REQ_BUSY: { + console_reply_err(KCLI_ERR_BUSY, "busy"); + break; + } + case APP_REQ_MISSING: { + console_reply_err(KCLI_ERR_REJECT, "missing"); + break; + } + case APP_REQ_EMPTY: { + console_reply_err(KCLI_ERR_REJECT, "empty"); + break; + } + default: { + console_reply_err(KCLI_ERR_REJECT, "refused"); + break; + } + } + console_end(); +} + /** * @brief install src= dst= [run=on|off] * [size= crc32=]. @@ -1785,7 +1830,9 @@ static void install_from_wire(Console *me, const kcmd_t *c, uint8_t dst, * `dst=` is `os`, `c5`, `pending` (the mailbox) or `app`, which boots once * only on `run=on`; `run` is refused for any other `dst`. `src=` is * `fs:name`, `sd:name`, `http(s)://...`, `"pending"` or `"terminal"`. Only - * `"terminal"` takes `size` and `crc32`, and only there is `>ok` a result. + * `"terminal"` takes `size` and `crc32`, and only there is `>ok` a result: off + * a medium or the mailbox it says AO_AppMgr took the install, and the + * verdict decides (console_reply_app_verdict()). */ static void h_install(Console *me, const kcmd_t *c, const karg_val_t *v) { @@ -1904,8 +1951,14 @@ static void h_install(Console *me, const kcmd_t *c, const karg_val_t *v) console_reply(">ok"); return; } - appmgr_image_request(kind, spec, dst, run); - console_reply(">ok"); + cmd_begin(me, KCLI_INSTALL, c->verb); + console_extend(KCLI_INSTALL_MS); + if ( !appmgr_image_ask(kind, spec, dst, run, &me->super) ) + { + /* Nothing was posted, so nobody will answer. */ + console_reply_err(KCLI_ERR_REJECT, c->verb); + console_end(); + } } /* ---- Handlers: the app data folder --------------------------------------- @@ -7925,6 +7978,16 @@ static QState console_own(Console * const me, QEvt const * const e) status = Q_HANDLED(); break; } + case APP_REQ_RSP_SIG: { + /* Only the install in flight hears it: a verdict that arrives + * after the watchdog closed the command belongs to nobody. */ + if ( (uint8_t)KCLI_INSTALL == me->inflight ) + { + console_reply_app_verdict(Q_EVT_CAST(AppReqRspEvt)->verdict); + } + status = Q_HANDLED(); + break; + } case RECV_FAIL_SIG: { if ( (uint8_t)KCLI_RECV == me->inflight ) { diff --git a/components/kaos/console/src/console_verbs.h b/components/kaos/console/src/console_verbs.h index cd9ddc7..b8fd8bb 100644 --- a/components/kaos/console/src/console_verbs.h +++ b/components/kaos/console/src/console_verbs.h @@ -50,6 +50,13 @@ void console_reply(const char *fmt, ...) __attribute__((format(printf, 1, 2))); */ void console_reply_err(kcli_err_t code, const char *msg); +/** + * @brief Close the command in flight with AO_AppMgr's verdict on its request. + * @param verdict app_req_verdict_t: `>ok` for taken; `busy` is code 4; `missing`, + * `empty` and `refused` are code 5. + */ +void console_reply_app_verdict(uint8_t verdict); + /** * @brief `>ok` when the request reached its owner, else code 5 naming the verb. * @param verb The verb. diff --git a/components/kaos/core/include/kaos.h b/components/kaos/core/include/kaos.h index 20877b6..e914ddb 100644 --- a/components/kaos/core/include/kaos.h +++ b/components/kaos/core/include/kaos.h @@ -3528,6 +3528,39 @@ kode_status_t appdata_put_request(const char *project, uint8_t *buf, * is still making would be cut. */ #define APPMGR_TIDY_LATE_MS (5000U) +/* ---- What an install evicted, kept in NVS until the install ends -------------- + * An install takes its victims out of the directory before the first byte + * arrives. AO_AppMgr records them first, in its own namespace, so a reset in + * the middle of the install does not cost them: the next boot puts back the + * ones whose bytes are as they were, which each record says by a CRC-32 of the + * image's extent taken when the victim was evicted. One blob, a header and one + * record per victim; absent when no install is evicting. A boot takes the table + * out of NVS before it checks anything. + */ +#define APPMGR_UNDO_NS "kappmgr" /**< The namespace AO_AppMgr keeps its records in. */ +#define APPMGR_UNDO_KEY "undo" /**< The table of the victims of the install in hand. */ +#define APPMGR_UNDO_VERSION 2U /**< The layout below; any other is ignored and erased. */ +#define APPMGR_UNDO_MAX 64U /**< Records in a table: every slot of the directory. */ +#define APPMGR_UNDO_HDR_LEN 4U /**< Bytes of appmgr_undo_hdr_t. */ +#define APPMGR_UNDO_REC_LEN 52U /**< Bytes of appmgr_undo_rec_t. */ + +/** @brief The head of the blob. */ +typedef struct { + uint8_t version; /**< APPMGR_UNDO_VERSION. */ + uint8_t count; /**< Records that follow: 1..APPMGR_UNDO_MAX. */ + uint8_t reserved[2]; /**< Zero. */ +} appmgr_undo_hdr_t; + +/** @brief One victim: its directory entry as it stood, field for field, its name and the CRC of its extent. */ +typedef struct { + uint32_t offset; /**< Region-relative, a multiple of 64 KB. */ + uint32_t size; /**< The image's exact length in bytes. */ + uint32_t seq; /**< Its install number. */ + uint32_t flags; /**< The entry's flags. */ + char project[KODE_APP_PROJECT_MAX]; /**< Its name, read before the install could overwrite it. */ + uint32_t crc; /**< khal_crc32() over [offset, offset + size), taken before the eviction. */ +} appmgr_undo_rec_t; + /** * @brief The folder's path on @p m, or false when the name cannot be a folder. * @@ -3592,8 +3625,10 @@ const uint8_t *net_stream_buf(uint8_t idx); /* ---- AO_AppMgr: owner of the apps region -------------------------------- * Copies a .bin from a medium into the apps region (evicting the oldest install - * when short of room), lists, launches and removes apps by name. Which firmware - * boots is AO_Boot's; the two write different flash regions. + * when short of room), lists, launches and removes apps by name. An install that + * fails gives back every app it evicted whose flash its write never reached, and + * so does the next boot after a reset in the middle of one (the table below). + * Which firmware boots is AO_Boot's; the two write different flash regions. */ extern QActive * const AO_AppMgr; /**< The apps-region AO. */ void AppMgr_ctor(void); /**< Construct it; kaos_appmgr_start() calls it. */ @@ -3700,6 +3735,41 @@ bool appmgr_recv_request(uint8_t dst, uint32_t size, uint32_t crc32, void appmgr_image_request(uint8_t src_kind, const char *src, uint8_t dst_kind, bool launch); +/** + * @brief appmgr_image_request(), asking AO_AppMgr for its verdict. + * + * AO_AppMgr posts @p who one APP_REQ_RSP_SIG (AppReqRspEvt) once it has + * decided: taken (an `https` source as soon as it starts opening, a file on a + * medium once the medium has opened it, the mailbox at once), busy, missing (no + * such file on the medium), empty (nothing in the mailbox) or refused. What + * goes wrong after that, a download that fails, is published as + * APP_FLASH_FAIL_SIG and says nothing here. + * + * @param src_kind kfw_src_t as a plain integer. + * @param src File name for fs/sd, URL for http, ignored otherwise. + * @param dst_kind kfw_target_t as a plain integer. + * @param launch Boot the image once it is written and verified. + * @param who Where the verdict is posted. + * @return false when the pool was dry and nothing was posted: no verdict + * will come. + */ +bool appmgr_image_ask(uint8_t src_kind, const char *src, uint8_t dst_kind, + bool launch, QActive *who); + +/** + * @brief appmgr_run_request(), asking AO_AppMgr for its verdict. + * + * AO_AppMgr posts @p who one APP_REQ_RSP_SIG: taken when an installed app has + * that name (the boot request is on its way to AO_Boot), missing when none + * does, busy when it has work in hand. + * + * @param name The installed app's project name. + * @param who Where the verdict is posted. + * @return false when the pool was dry and nothing was posted: no verdict + * will come. + */ +bool appmgr_run_ask(const char *name, QActive *who); + /** * @brief The event appmgr_image_request() posts, built for a caller that posts it itself. * @param src_kind kfw_src_t as a plain integer. @@ -3828,6 +3898,10 @@ typedef struct { * to @p who, refusals included: AO_AppMgr does one thing at a time and answers * APPDATA_GET_BUSY while it writes an image. Any task or AO. * + * A fetch that began shows like a write: APP_FLASH_PROGRESS_SIG at 0 % at once + * and again every second while it waits on the network, ended by an unnamed + * APP_FLASH_DONE_SIG or by APP_FLASH_FAIL_SIG. + * * @param project The app's project name, as its image declares it. * @param url The base URL, ending where the file names begin; shorter than * KODE_APPDATA_URL_MAX. diff --git a/components/kdrv/apps/include/kdrv_apps.h b/components/kdrv/apps/include/kdrv_apps.h index 814936b..c4896b8 100644 --- a/components/kdrv/apps/include/kdrv_apps.h +++ b/components/kdrv/apps/include/kdrv_apps.h @@ -115,6 +115,23 @@ kode_status_t kdrv_apps_write_end(void); /** @brief Abandon the session. Whatever was written stays, unverified. */ void kdrv_apps_write_abort(void); +/** + * @brief Has the write erased any part of an extent? + * + * Answers for the write session in hand, or the last one that ran, and starts + * over with every committed directory: a commit says which extents hold apps, + * so an erase made before it speaks for nothing that comes after. An erase that + * failed counts as having reached its block, because flash a failed erase + * touched is not flash that can be trusted. Only blocks the write got to count: + * the region erases 64 KB at a time, as the data arrives. + * + * @param offset Region-relative start of the extent. + * @param size Its length in bytes. + * @return true when some of its bytes may no longer be what they were; false + * when every one of them is, or when @p size is 0. + */ +bool kdrv_apps_write_reached(uint32_t offset, uint32_t size); + /** * @brief Read @p len bytes of the region at region-relative @p offset. * @@ -130,6 +147,26 @@ void kdrv_apps_write_abort(void); */ kode_status_t kdrv_apps_read(uint32_t offset, void *buf, size_t len); +/** + * @brief Fold the bytes of an extent of the region into a running CRC-32. + * + * Reads exactly @p size bytes from @p offset, in pieces, and never one more: + * what lies beyond the extent is not this call's to touch. The CRC is the + * one khal_crc32() gives, chainable, so an extent may be taken in parts: pass + * KHAL_CRC32_INIT in @p crc for the first and the result for the next. It + * does not parse the data. An image a reset half overwrote is another + * image's header and segments, and what a parser makes of them is not bounded + * by @p size; a digest is. + * + * @param offset Region-relative start, at or past KAPPS_DATA_OFF. + * @param size How many bytes; 0 reads nothing and changes nothing. + * @param crc The running CRC on entry and, on KODE_OK, the updated one; + * left alone on any other answer. + * @return KODE_OK; KODE_DENIED with no region or a NULL @p crc; KODE_ERROR for + * an extent that leaves the data area or one that cannot be mapped. + */ +kode_status_t kdrv_apps_extent_crc(uint32_t offset, uint32_t size, uint32_t *crc); + /** @brief What kdrv_apps_sink() needs: where kdrv_apps_plan() said to write. */ typedef struct { uint32_t offset; /**< Region-relative, 64 KB-aligned. */ diff --git a/components/kdrv/apps/src/kdrv_apps.c b/components/kdrv/apps/src/kdrv_apps.c index d3a6156..91e0bcc 100644 --- a/components/kdrv/apps/src/kdrv_apps.c +++ b/components/kdrv/apps/src/kdrv_apps.c @@ -19,9 +19,13 @@ #include "esp_image_format.h" #include "esp_partition.h" #include "esp_rom_crc.h" +#include "khal_crc32.h" +#include "kdrv_image_walk.h" #define TAG "kdrv.apps" #define APPS_SECTOR 0x1000U /* the 4 KB unit every metadata sector is erased in */ +/** Bytes of the region one extent CRC maps at a time. */ +#define APPS_CRC_WINDOW (512U * 1024U) /* ---- State: the region partition and its RAM directory ------------------- */ @@ -37,6 +41,10 @@ static uint32_t s_wr_off; static uint32_t s_wr_size; static uint32_t s_wr_written; static uint32_t s_wr_erased; /* region offset erased up to, monotonic */ +/* Region offset up to which the flash from s_wr_off on may have been changed: + * s_wr_erased, and the block of an erase that failed. An empty run (nothing at + * or past the start) is what a record with nothing in it looks like. */ +static uint32_t s_wr_reach; /** * @brief Find the `app_pending` mailbox partition. @@ -265,6 +273,8 @@ kode_status_t kdrv_apps_dir_commit(const kapps_dir_t *d) } s_dir = next; /* only adopted once the write above succeeded */ s_dir_active = target; + s_wr_off = 0U; /* the new directory is what says which extents hold apps: */ + s_wr_reach = 0U; /* no erase made before it speaks for them */ return KODE_OK; } @@ -464,6 +474,7 @@ kode_status_t kdrv_apps_write_begin(uint32_t offset, uint32_t size) s_wr_size = size; s_wr_written = 0U; s_wr_erased = offset; + s_wr_reach = offset; s_wr_open = true; return KODE_OK; } @@ -485,6 +496,7 @@ kode_status_t kdrv_apps_write(const void *data, size_t len) uint32_t const reach = s_wr_off + s_wr_written + (uint32_t)len; while ( s_wr_erased < reach ) { + s_wr_reach = s_wr_erased + KAPPS_ALIGN; /* before the erase: a failed one may have changed the block */ esp_err_t const err = esp_partition_erase_range(s_part, s_wr_erased, KAPPS_ALIGN); if ( ESP_OK != err ) { @@ -521,6 +533,14 @@ void kdrv_apps_write_abort(void) s_wr_open = false; } +bool kdrv_apps_write_reached(uint32_t offset, uint32_t size) +{ + uint64_t const end = (uint64_t)offset + (uint64_t)size; + + return (0U < size) && (s_wr_off < s_wr_reach) + && (offset < s_wr_reach) && ((uint64_t)s_wr_off < end); +} + /* ---- Reading an image back ------------------------------------------------- */ kode_status_t kdrv_apps_read(uint32_t offset, void *buf, size_t len) @@ -541,6 +561,57 @@ kode_status_t kdrv_apps_read(uint32_t offset, void *buf, size_t len) return KODE_OK; } +kode_status_t kdrv_apps_extent_crc(uint32_t offset, uint32_t size, uint32_t *crc) +{ + if ( (NULL == s_part) || (NULL == crc) ) + { + return KODE_DENIED; + } + if ( (KAPPS_DATA_OFF > offset) + || (((uint64_t)offset + (uint64_t)size) > (uint64_t)s_part->size) ) + { + return KODE_ERROR; + } + if ( 0U == size ) + { + return KODE_OK; + } + /* Through the mapping the images are verified and run through, a window + * at a time: the bytes come at the cache's pace, and nothing holds the + * flash with the cache off the way a read through the driver does. */ + uint32_t run = *crc; + uint32_t done = 0U; + kode_status_t st = KODE_OK; + + while ( (KODE_OK == st) && (done < size) ) + { + uint32_t n = size - done; + const void *map = NULL; + esp_partition_mmap_handle_t h = 0U; + + if ( APPS_CRC_WINDOW < n ) + { + n = APPS_CRC_WINDOW; + } + if ( esp_partition_mmap(s_part, offset + done, n, ESP_PARTITION_MMAP_DATA, + &map, &h) != ESP_OK ) + { + st = KODE_ERROR; + } + else + { + run = khal_crc32(run, map, n); + done += n; + esp_partition_munmap(h); + } + } + if ( KODE_OK == st ) + { + *crc = run; + } + return st; +} + /* ---- The region as a kfw_sink_t ------------------------------------------- * This one needs a context, because where an image lands is the caller's * decision: kdrv_apps_plan() runs against a directory the caller is holding, @@ -672,6 +743,18 @@ kode_status_t kdrv_apps_verify(uint32_t offset, uint32_t size, uint32_t *img_len { return KODE_DENIED; } + /* esp_image_verify() hashes every segment by the length its header + * declares, and compares the total with `size` only afterwards: a header + * that declares more than the image holds keeps it reading flash for as + * long as the lengths say, in the caller's own step. The chain is walked + * first, and an image whose segments leave its bytes is refused unhashed. */ + if ( (((uint64_t)offset + (uint64_t)size) > (uint64_t)s_part->size) + || (0U == kdrv_image_walk_len(s_part, offset, size)) ) + { + KLOG_E(TAG, "verify @0x%x failed (the image does not hold together in its %u bytes)", + (unsigned)offset, (unsigned)size); + return KODE_ERROR; + } esp_image_metadata_t meta; esp_partition_pos_t const pos = { .offset = s_part->address + offset, @@ -855,9 +938,10 @@ void kdrv_apps_req_clear(void) * validate a hash or a signature. * * Phase 2 is the real verification: once AppMgr copies those bytes into the - * apps region, kdrv_apps_write_end() calls kdrv_apps_verify(), a plain - * esp_image_verify(). Note what the region's growth did to that call - the - * region now runs from 0xC40000 to 0x1800000 and therefore crosses 16 MB, so + * apps region, kdrv_apps_write_end() calls kdrv_apps_verify(): the same walk + * over the bytes written, then esp_image_verify(). Note what the region's + * growth did to that call - the region now runs from 0xC40000 to 0x1800000 + * and therefore crosses 16 MB, so * an image installed in its upper part is verified through the same 32-bit * mapping it is executed through. The flag is not optional decoration for * this layout; without it, phase 2 fails on exactly the images phase 1 cannot @@ -878,46 +962,7 @@ static uint32_t pending_walk_len(const esp_partition_t *p) * partition: an image allowed to run into that sector would be adopted * and then have its tail erased by consume(). This is the same ceiling * write_begin() enforces, so an esptool push and a cable upload agree. */ - uint32_t const limit = pending_intent_off(p); - esp_image_header_t header; - if ( (esp_partition_read(p, 0, &header, sizeof(header)) != ESP_OK) - || (ESP_IMAGE_HEADER_MAGIC != header.magic) - || (0U == header.segment_count) - || (ESP_IMAGE_MAX_SEGMENTS < header.segment_count) ) - { - return 0U; - } - - uint32_t off = sizeof(esp_image_header_t); - for ( uint8_t i = 0U; i < header.segment_count; ++i ) - { - esp_image_segment_header_t seg; - if ( (off > limit) || (esp_partition_read(p, off, &seg, sizeof(seg)) != ESP_OK) ) - { - return 0U; - } - /* Bound the addition, not its result. `data_len` comes straight off - * flash and is not to be trusted: a value near UINT32_MAX wraps `off` - * back to a small number, and a check made afterwards waves that - * through. Such a segment does not run off the end of the partition, - * it runs around it. `off <= limit` holds above, so both - * subtractions here are safe. */ - uint32_t const room = limit - off; - if ( ((uint32_t)sizeof(seg) > room) - || ((room - (uint32_t)sizeof(seg)) < seg.data_len) ) - { - return 0U; - } - off += (uint32_t)sizeof(seg) + seg.data_len; - } - /* The trailing 1-byte checksum pads the image out to a 16-byte boundary, - * then the optional SHA-256 "simple hash" (32 bytes) follows it. */ - off = (off | 15U) + 1U; - if ( 0U != header.hash_appended ) - { - off += 32U; - } - return (off <= limit) ? off : 0U; + return kdrv_image_walk_len(p, 0U, pending_intent_off(p)); } kode_status_t kdrv_apps_pending_verify(void) diff --git a/components/kdrv/apps/src/kdrv_image_walk.h b/components/kdrv/apps/src/kdrv_image_walk.h new file mode 100644 index 0000000..fc4c345 --- /dev/null +++ b/components/kdrv/apps/src/kdrv_image_walk.h @@ -0,0 +1,91 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/** + * @file kdrv_image_walk.h + * @brief An image's header chain walked by its own bytes: the one place it happens. + * + * esp_image_verify(), and esp_ota_end() which calls it, hash every segment by + * the length its header declares and compare the total with the partition + * only afterwards. A header that declares more than the image holds keeps + * them reading flash for as long as the lengths say, in the caller's own + * step. Whoever is about to hand them an image that arrived from outside + * walks its chain first and refuses one that leaves its bytes. + */ +#ifndef KDRV_IMAGE_WALK_H +#define KDRV_IMAGE_WALK_H + +#include + +#include "esp_image_format.h" +#include "esp_partition.h" + +/** @brief Bytes of the SHA-256 an image may carry after its checksum. */ +#define KDRV_IMAGE_HASH_LEN 32U + +/** @brief The boundary the checksum byte pads an image out to, less one. */ +#define KDRV_IMAGE_PAD_MASK 15U + +/** + * @brief Walk an image's header chain by its own bytes: no mmap, no hash. + * + * Magic, segment count, each segment's declared length, the checksum pad and + * the optional appended hash, read through esp_partition_read(). A length + * check, not a cryptographic one: it says whether the image the header + * describes fits in the bytes it was given, and how long it is. + * + * @param p The partition the image lies in. + * @param base Where the image starts, partition-relative. + * @param limit How many bytes from @p base the image may take. + * @return The image length (header + every segment + checksum pad + the + * optional appended hash), or 0 if the walk does not hold together: + * a bad magic or segment count, or a length that leaves @p limit. + */ +static inline uint32_t kdrv_image_walk_len(const esp_partition_t *p, uint32_t base, + uint32_t limit) +{ + esp_image_header_t header; + if ( (esp_partition_read(p, base, &header, sizeof(header)) != ESP_OK) + || (ESP_IMAGE_HEADER_MAGIC != header.magic) + || (0U == header.segment_count) + || (ESP_IMAGE_MAX_SEGMENTS < header.segment_count) ) + { + return 0U; + } + + uint32_t off = sizeof(esp_image_header_t); + for ( uint8_t i = 0U; i < header.segment_count; ++i ) + { + esp_image_segment_header_t seg; + if ( (off > limit) + || (esp_partition_read(p, base + off, &seg, sizeof(seg)) != ESP_OK) ) + { + return 0U; + } + /* Bound the addition, not its result. `data_len` comes straight off + * flash and is not to be trusted: a value near UINT32_MAX wraps `off` + * back to a small number, and a check made afterwards waves that + * through. Such a segment does not run off the end of its bytes, it + * runs around them. `off <= limit` holds above, so both subtractions + * here are safe. */ + uint32_t const room = limit - off; + if ( ((uint32_t)sizeof(seg) > room) + || ((room - (uint32_t)sizeof(seg)) < seg.data_len) ) + { + return 0U; + } + off += (uint32_t)sizeof(seg) + seg.data_len; + } + /* The trailing 1-byte checksum pads the image out to a 16-byte boundary, + * then the optional SHA-256 "simple hash" follows it. */ + off = (off | KDRV_IMAGE_PAD_MASK) + 1U; + if ( 0U != header.hash_appended ) + { + off += KDRV_IMAGE_HASH_LEN; + } + if ( off > limit ) + { + return 0U; + } + return off; +} + +#endif /* KDRV_IMAGE_WALK_H */ diff --git a/components/kdrv/apps/src/kdrv_kfw.c b/components/kdrv/apps/src/kdrv_kfw.c index f42693d..3744db3 100644 --- a/components/kdrv/apps/src/kdrv_kfw.c +++ b/components/kdrv/apps/src/kdrv_kfw.c @@ -550,6 +550,34 @@ static uint16_t target_chip(kfw_target_t t) return chip; } +/** + * @brief How long a raw image says its first segment is. + * + * The one fact about an image's size its first bytes hold: the first + * segment's header follows the image header at once, and its length is how + * many bytes of the file that segment takes. + * + * @param prefix The first bytes of the image. + * @param len How many bytes @p prefix holds. + * @param seg_len The first segment's declared length in bytes; left alone + * when the read is refused. + * @return true when the prefix starts with the ESP image magic and holds the + * first segment's header. + */ +static bool raw_first_seg(const uint8_t *prefix, size_t len, uint32_t *seg_len) +{ + esp_image_segment_header_t seg; + + if ( ((sizeof(esp_image_header_t) + sizeof(seg)) > len) + || (KFW_ESP_MAGIC != prefix[0]) ) + { + return false; + } + (void)memcpy(&seg, &prefix[sizeof(esp_image_header_t)], sizeof(seg)); + *seg_len = seg.data_len; + return true; +} + kode_status_t kfw_feed(const void *data, size_t len) { const uint8_t *p = (const uint8_t *)data; @@ -593,6 +621,24 @@ kode_status_t kfw_feed(const void *data, size_t len) s_kfw.st = KFW_ST_DEAD; return KODE_ERROR; } + /* A file cut short inside its first segment is refused + * here for the same reason: the header says how long that + * segment is and the source how long the file is. A cut + * further on, or a source of no stated length, is the + * end's to judge. */ + uint32_t seg0 = 0U; + if ( (0U != s_kfw.src_len) + && raw_first_seg(p, len, &seg0) + && (((uint64_t)sizeof(esp_image_header_t) + + (uint64_t)sizeof(esp_image_segment_header_t) + + (uint64_t)seg0) > (uint64_t)s_kfw.src_len) ) + { + KLOG_E(TAG, "that image's first segment is %u bytes and " + "the file holds %u: it was cut short", + (unsigned)seg0, (unsigned)s_kfw.src_len); + s_kfw.st = KFW_ST_DEAD; + return KODE_ERROR; + } /* Its project name, when the first chunk holds IDF's * descriptor (every medium's does): the app it replaces. */ char project[KAPP_IDF_NAME_MAX]; diff --git a/components/kdrv/apps/src/kdrv_slot.c b/components/kdrv/apps/src/kdrv_slot.c index b3559d8..4c8c80e 100644 --- a/components/kdrv/apps/src/kdrv_slot.c +++ b/components/kdrv/apps/src/kdrv_slot.c @@ -6,6 +6,7 @@ #include "kdrv_slot.h" #include "kdrv_desc_fill.h" #include "kdrv_bounce.h" +#include "kdrv_image_walk.h" #include "kdrv_kfw.h" @@ -222,6 +223,19 @@ kode_status_t kdrv_slot_end(void) { return KODE_DENIED; } + /* esp_ota_end() hashes the image by the lengths its own header declares + * (see kdrv_image_walk.h): one whose segments leave the slot is refused + * here, unhashed, and the session given up. */ + const esp_partition_t * const p = slot_part(); + + if ( (NULL == p) || (0U == kdrv_image_walk_len(p, 0U, p->size)) ) + { + KLOG_E(TAG, "the image written does not hold together in the slot; " + "it is not an update"); + (void)esp_ota_abort(s_ota); + s_ota = 0; + return KODE_ERROR; + } esp_err_t const err = esp_ota_end(s_ota); s_ota = 0; /* consumed by esp_ota_end either way */ if ( ESP_OK != err ) diff --git a/components/kqp/include/kqp_config.h b/components/kqp/include/kqp_config.h index f2e42f0..b4f5b70 100644 --- a/components/kqp/include/kqp_config.h +++ b/components/kqp/include/kqp_config.h @@ -563,6 +563,16 @@ _Static_assert(KODE_PRIO_STANDARD_READER > KODE_PRIO_USER_GUI, "the background r * over a slow link must not time out, but a host that dies mid-image must not * leave the console waiting forever. AO_AppMgr rearms it on every chunk. */ #define KODE_RECV_IDLE_MS 5000U +/* The same watchdog for a transfer that rides BLE. A link that stalls for a + * few seconds under interference is slow, not dead, and the phone app waits + * 10 s for each ack before it sends the chunk again: the bound has to outlast + * that wait, or the board gives up on a transfer the host is still retrying. */ +#define KODE_RECV_IDLE_BLE_MS 15000U +/* How long a launch after a terminal install waits for the BLE console's + * outbound bytes to leave - the `>ok` the host is waiting for - before it + * resets the board anyway. A notification still queued when the board resets + * is lost, and the phone retries against a running app. */ +#define KODE_RECV_DRAIN_BLE_MS 2000U /* How long a BLE scan listens. Long enough to catch a phone's slow * advertising interval (up to ~1.3 s) several times over, short enough that diff --git a/components/kqp/include/kqp_signals.h b/components/kqp/include/kqp_signals.h index e5d5ec1..31862d4 100644 --- a/components/kqp/include/kqp_signals.h +++ b/components/kqp/include/kqp_signals.h @@ -542,9 +542,21 @@ enum KodeSignals { APP_TIDY_TIMEOUT_SIG, /**< AO_AppMgr private QTimeEvt: AO_Storage * did not answer a folder request in * APPMGR_TIDY_MS (AppMgr_tidying). */ + APP_UNDO_STEP_SIG, /**< AO_AppMgr private: verify the next app a + * reset cut out of an install. Posted to + * self, one per app, never by anything + * external. */ + APP_BEAT_SIG, /**< AO_AppMgr private QTimeEvt: a second of + * waiting on the network has gone by - + * report the write's percent again, so the + * glass and the LED stay up. */ FLASH_REQ_SIG, /**< AO_AppMgr: one source-to-sink transfer * (FlashReqEvt). The only way to ask for * an image to be written. */ + APP_REQ_RSP_SIG, /**< To the asker: whether AO_AppMgr took an + * install (FLASH_REQ_SIG) or run + * (APP_RUN_REQ_SIG) request that named one + * (AppReqRspEvt). Posted, not published. */ STORAGE_CMD_SIG, /**< AO_Storage: do StorageCmdEvt.op. */ STORAGE_LIST_PAGE_SIG, /**< AO_Storage: post a page of the bootable * images on every medium to the asker @@ -2011,6 +2023,8 @@ typedef struct { QEvt super; char file[KODE_APP_FILE_MAX]; bool launch; /**< Boot the slot as soon as the write succeeds. */ + QActive *who; /**< APP_RUN_REQ_SIG only: where the verdict is posted + * (AppReqRspEvt), or NULL for none. */ } AppFlashEvt; /** @@ -2187,13 +2201,36 @@ typedef struct { * time AO_AppMgr reads this. */ typedef struct { - QEvt super; - uint8_t src_kind; /**< kfw_src_t, as a plain integer. */ - uint8_t dst_kind; /**< kfw_target_t, likewise. */ - bool launch; /**< Boot it once written. */ - char src[KODE_FLASH_SRC_MAX]; /**< File name, or a URL. */ + QEvt super; + uint8_t src_kind; /**< kfw_src_t, as a plain integer. */ + uint8_t dst_kind; /**< kfw_target_t, likewise. */ + bool launch; /**< Boot it once written. */ + QActive *who; /**< Where AO_AppMgr's verdict is + * posted (AppReqRspEvt), or NULL + * for none. */ + char src[KODE_FLASH_SRC_MAX]; /**< File name, or a URL. */ } FlashReqEvt; +/** @brief What AO_AppMgr made of a request that asked for a verdict (AppReqRspEvt.verdict). */ +typedef enum { + APP_REQ_TAKEN = 0, /**< Taken: the install is under way, or the app is on its way to boot. */ + APP_REQ_BUSY, /**< AO_AppMgr has work in hand: ask again. */ + APP_REQ_MISSING, /**< The file is not on the medium, or the app is not installed. */ + APP_REQ_EMPTY, /**< The mailbox holds nothing to install. */ + APP_REQ_REFUSED /**< Refused for another reason, which the log gives. */ +} app_req_verdict_t; + +/** + * @brief AO_AppMgr's verdict on an install or run request (APP_REQ_RSP_SIG). + * + * Posted to the asker alone, once, never published: only the asker waits for + * it. + */ +typedef struct { + QEvt super; + uint8_t verdict; /**< app_req_verdict_t. */ +} AppReqRspEvt; + /** * @brief One installed app, in answer to APP_LIST_REQ_SIG (APP_ENTRY_SIG). * diff --git a/tests/host/cases/test_appmgr_ao.c b/tests/host/cases/test_appmgr_ao.c index e092e4e..10b13f9 100644 --- a/tests/host/cases/test_appmgr_ao.c +++ b/tests/host/cases/test_appmgr_ao.c @@ -28,14 +28,18 @@ #include "kdrv_kfw.h" #include "kbsp_owners.h" #include "khal_crc32.h" +#include "khal_nvs.h" #include "khal_system.h" #include "kqp.h" #include "lowpower_cases.h" #include "miniz.h" #include "qf_support.h" -/** @brief Put AO_AppMgr back to idle. */ -static void appmgr_fresh(void) +/** + * @brief Construct and start AO_AppMgr over what the board holds, and leave what its + * initial transition posted in its queue. + */ +static void appmgr_make(void) { static bool s_registered; @@ -69,6 +73,12 @@ static void appmgr_fresh(void) s_registered = true; } QASM_INIT(AO_AppMgr, (void *)0, 0U); +} + +/** @brief Put AO_AppMgr back to idle. */ +static void appmgr_fresh(void) +{ + appmgr_make(); /* Drop whatever the initial transition announced, and whatever a previous * case left in the neighbour's queue - that queue is the discriminator @@ -845,6 +855,41 @@ TEST_CASE("running an installed app boots the extent the directory recorded", /** @brief The image image_named() builds; a case needs one at a time. */ static uint8_t s_named_img[512]; +/** @brief Bytes of the one segment image_chain() declares. */ +#define CHAIN_SEG_LEN 16U + +/** @brief The least bytes image_chain() needs: both headers, the segment, the checksum pad. */ +#define CHAIN_MIN_LEN 64U + +/** + * @brief Make the head of a stand-in image a header chain that holds. + * + * The region walks an image's chain of segments before it hashes it, and + * refuses one that leaves the image's own bytes. A stand-in whose case is + * about something else still has to be an image that far: one segment of + * CHAIN_SEG_LEN bytes, no appended hash, this chip. What follows the two + * headers is left as the caller filled it. + * + * @param img The image, at least CHAIN_MIN_LEN bytes. + */ +static void image_chain(uint8_t *img) +{ + esp_image_header_t header; + esp_image_segment_header_t seg; + + (void)memset(&header, 0, sizeof(header)); + header.magic = ESP_IMAGE_HEADER_MAGIC; + header.chip_id = ESP_CHIP_ID_ESP32P4; + header.segment_count = 1U; + header.hash_appended = 0U; + (void)memcpy(&img[0], &header, sizeof(header)); + + (void)memset(&seg, 0, sizeof(seg)); + seg.load_addr = 0x40000000U; + seg.data_len = CHAIN_SEG_LEN; + (void)memcpy(&img[sizeof(header)], &seg, sizeof(seg)); +} + /** * @brief Build a minimal, walkable ESP image whose descriptor names * @p project, in s_named_img. @@ -1048,6 +1093,7 @@ static void rm_rsp(const char *project, kode_status_t st); static void wipe_rsp(const char *project, kode_status_t st, uint8_t fs_files, uint8_t sd_files, uint8_t left); static void answer_stat(bool exists, uint8_t rc); +static void answer_copy_of(uint32_t bytes); /* Fix 2: adoption plans and evicts BEFORE the copy, and the copy is what the * crypto verify judges - so without a source check a torn upload (headers @@ -1113,7 +1159,7 @@ TEST_CASE("a mailbox image that fails verification does not evict an app for roo if ( NULL != s ) { QF_gc(s); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(dir.e[0].size); } run_ticks(4U); TEST_ASSERT_TRUE_MESSAGE(boot_run_request(&off, &size, &mode), @@ -1640,10 +1686,10 @@ TEST_CASE("the CRC verdict is RECV_DONE_SIG on a match, RECV_FAIL_SIG on one " * accidental match against an uninitialised want. */ payload[i] = (uint8_t)((i * 7U) + 3U); } - /* Byte 0 must be the raw-image magic - see the ack case above for why. */ - payload[0] = KFW_ESP_MAGIC; - payload[12] = (uint8_t)ESP_CHIP_ID_ESP32P4; /* its chip_id: this one */ - payload[13] = 0U; + /* Byte 0 must be the raw-image magic - see the ack case above for why - + * and the rest of the head a chain the region's walk accepts. */ + TEST_ASSERT_TRUE(sizeof(payload) >= CHAIN_MIN_LEN); + image_chain(payload); good_crc = khal_crc32(KHAL_CRC32_INIT, payload, sizeof(payload)); /* ---- Phase A: the wire matches the promise ------------------------- */ @@ -2071,9 +2117,8 @@ TEST_CASE("install dst=os writes the inactive slot, touches no directory, and " { payload[i] = (uint8_t)((i * 5U) + 1U); } - payload[0] = KFW_ESP_MAGIC; /* a bare image: straight to the sink */ - payload[12] = (uint8_t)ESP_CHIP_ID_ESP32P4; /* its chip_id: this one */ - payload[13] = 0U; + TEST_ASSERT_TRUE(sizeof(payload) >= CHAIN_MIN_LEN); + image_chain(payload); /* a bare image: straight to the sink */ Mockkdrv_slot_Init(); recv_fresh(); @@ -2597,8 +2642,8 @@ static bool klog_has(const char *needle) return false; } -/** @brief Answer a path request of kind @p op the way AO_Storage would. */ -static void answer_path(uint8_t op, uint8_t rc, bool exists, bool is_dir) +/** @brief Answer a path request of kind @p op the way AO_Storage would; @p size is a STAT's file length. */ +static void answer_path(uint8_t op, uint8_t rc, bool exists, bool is_dir, uint32_t size) { StoragePathRspEvt r; @@ -2608,13 +2653,26 @@ static void answer_path(uint8_t op, uint8_t rc, bool exists, bool is_dir) r.rc = rc; r.exists = exists; r.is_dir = is_dir; + r.size = size; QASM_DISPATCH(AO_AppMgr, &r.super, 0U); } -/** @brief Answer AppMgr's STAT: the path is there or it is not. */ +/** + * @brief Answer AppMgr's STAT when nothing is there, or the STAT failed. + * + * A file that is there has a length, and whether it is a victim's copy + * depends on it: that answer is answer_copy_of()'s. + */ static void answer_stat(bool exists, uint8_t rc) { - answer_path((uint8_t)STORAGE_PATH_STAT, rc, exists, false); + TEST_ASSERT_FALSE_MESSAGE(exists, "a file that is there has a length: answer_copy_of()"); + answer_path((uint8_t)STORAGE_PATH_STAT, rc, exists, false, 0U); +} + +/** @brief Answer AppMgr's STAT: a file of @p bytes bytes is where the copy would be. */ +static void answer_copy_of(uint32_t bytes) +{ + answer_path((uint8_t)STORAGE_PATH_STAT, (uint8_t)KODE_OK, true, false, bytes); } /** @brief Answer AppMgr's whole-buffer write. */ @@ -3094,7 +3152,7 @@ TEST_CASE("a victim already on the card is not copied again", (void)begin_install((uint8_t)KDRV_MEDIUM_FS, "new"); storage_expect(STORAGE_PATH_REQ_SIG, "the victim's path was never asked about"); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(old_bytes()); run_ticks(50U); /* the restart, and then some */ storage_seen_t const seen = storage_tally(); @@ -3109,6 +3167,63 @@ TEST_CASE("a victim already on the card is not copied again", keep_end(); } +/* A file of the victim's name is its copy only when it is as long as the image: + * an older version of the app, kept by an earlier eviction, or another file, is + * something else, and evicting the app on its account would lose it. */ +TEST_CASE("a file of the victim's name at another length is copied over, not taken for its copy", + "[appmgr_ao][keep]") +{ + Mockkdrv_slot_Init(); + keep_fresh(); + region_full_of_old(); + (void)begin_install((uint8_t)KDRV_MEDIUM_FS, "new"); + storage_expect(STORAGE_PATH_REQ_SIG, "the victim's path was never asked about"); + + answer_copy_of(old_bytes() / 2U); /* an older version, half as long */ + TEST_ASSERT_FALSE_MESSAGE(klog_has("is already on the card"), + "a file of another length was taken for the app's copy"); + TEST_ASSERT_TRUE_MESSAGE(klog_has("/sdcard/old.bin' is "), + "the file of another length went unsaid"); + const QEvt *wr = storage_wait_for(STORAGE_WRITE_REQ_SIG, 1000U); + TEST_ASSERT_NOT_NULL_MESSAGE(wr, "the app was not copied over the file of another length"); + TEST_ASSERT_EQUAL_STRING("/sdcard/old.bin", ((StorageWriteEvt const *)wr)->name); + TEST_ASSERT_EQUAL_UINT32(old_bytes(), ((StorageWriteEvt const *)wr)->len); + QF_gc(wr); + TEST_ASSERT_TRUE_MESSAGE(old_installed(), "nothing may be evicted before its copy lands"); + + answer_write((uint8_t)KODE_OK, old_bytes()); + (void)qf_support_pump(AO_AppMgr, 8U); + storage_expect(STORAGE_OPEN_REQ_SIG, "the install did not start again after the copy"); + TEST_ASSERT_TRUE(klog_has("'old' kept as /sdcard/old.bin")); + + let_the_open_expire(); + keep_end(); +} + +/* The NOTICE says both lengths, so the card is not rewritten in silence. */ +TEST_CASE("the file of another length is said with its bytes and the image's", + "[appmgr_ao][keep]") +{ + Mockkdrv_slot_Init(); + keep_fresh(); + region_full_of_old(); + (void)begin_install((uint8_t)KDRV_MEDIUM_FS, "new"); + storage_expect(STORAGE_PATH_REQ_SIG, "the victim's path was never asked about"); + + answer_copy_of(1234U); + char want[96]; + (void)snprintf(want, sizeof(want), "/sdcard/old.bin' is 1234 bytes and the image is %u", + (unsigned)old_bytes()); + TEST_ASSERT_TRUE_MESSAGE(klog_has(want), "the lengths of the two files were not told"); + const QEvt *wr = storage_wait_for(STORAGE_WRITE_REQ_SIG, 1000U); + TEST_ASSERT_NOT_NULL_MESSAGE(wr, "the app was not copied"); + QF_gc(wr); + answer_write((uint8_t)KODE_OK, old_bytes()); + (void)qf_support_pump(AO_AppMgr, 8U); + let_the_open_expire(); + keep_end(); +} + /* The card is where the new image comes from, and a failed copy on a card can * poison every later read of it: so the install is called off with nothing * evicted, and the glass is told the card is what failed. */ @@ -3220,181 +3335,1550 @@ TEST_CASE("the console install evicts without copying", "[appmgr_ao][keep]") region_full_of_old(); ao_spy_subscribe(APP_FLASH_DONE_SIG); - ask_recv(len, khal_crc32(KHAL_CRC32_INIT, s_named_img, len), false); - raw_armed(true); - feed(s_named_img, (uint16_t)len); + ask_recv(len, khal_crc32(KHAL_CRC32_INIT, s_named_img, len), false); + raw_armed(true); + feed(s_named_img, (uint16_t)len); + + unsigned const early = spy_done(&done); + tidy_settle(); /* the victim's folder goes first */ + unsigned const dones = spy_done(&done); + QActive_unsubscribe(ao_spy(), APP_FLASH_DONE_SIG); + ao_spy_drain(); + + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, early, + "the install ended before the evicted app's folder was deleted"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, storage_tally().path, + "the console path asked AO_Storage about a victim it cannot wait for"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, dones, "the install did not complete"); + TEST_ASSERT_FALSE_MESSAGE(old_installed(), "the victim was not evicted"); + TEST_ASSERT_TRUE_MESSAGE(klog_has("evicting 'old' to make room; no copy kept"), + "the victim went without a word"); + keep_end(); +} + +/** @brief How many live entries the directory holds. */ +static uint32_t live_entries(void) +{ + kapps_dir_t const *d = kdrv_apps_dir(); + uint32_t n = 0U; + + for ( uint32_t i = 0U; i < d->count; ++i ) + { + if ( (d->e[i].flags & KAPPS_ENTRY_VALID) != 0U ) + { + ++n; + } + } + return n; +} + +/** + * @brief "old" (seq 1) in the region's first 64 KB and "mine" (seq 2) in the + * rest: no gap anywhere, so an install has to evict. + */ +static void region_of_old_and_mine(void) +{ + keep_hold_dir(); + kapps_dir_t dir = s_keep_dir; + dir.count = 2U; + dir.e[0].offset = KAPPS_DATA_OFF; + dir.e[0].size = KAPPS_ALIGN; + dir.e[0].seq = 1U; + dir.e[0].flags = KAPPS_ENTRY_VALID; + dir.e[1].offset = KAPPS_DATA_OFF + KAPPS_ALIGN; + dir.e[1].size = kdrv_apps_region_size() - dir.e[1].offset; + dir.e[1].seq = 2U; + dir.e[1].flags = KAPPS_ENTRY_VALID; + plant_app(dir.e[0].offset, "old"); + plant_app(dir.e[1].offset, "mine"); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&dir)); +} + +/** @brief Whether an app named @p project is in the directory. */ +static bool named_installed(const char *project) +{ + kapps_dir_t const *d = kdrv_apps_dir(); + + for ( uint32_t i = 0U; i < d->count; ++i ) + { + kdrv_slot_desc_t desc; + if ( ((d->e[i].flags & KAPPS_ENTRY_VALID) != 0U) + && (kdrv_apps_desc(&d->e[i], &desc, NULL) == KODE_OK) + && (strcmp(desc.project, project) == 0) ) + { + return true; + } + } + return false; +} + +/** + * @brief Stream "mine" again from the console, named or not, and count the + * APP_EVICTED_SIG it publishes. + * @param name The name= the host sends. + * @param[out] last The last eviction published, when there was one. + * @return How many were published. + */ +static unsigned reinstall_mine(const char *name, AppEvictedEvt *last) +{ + unsigned n = 0U; + uint32_t const len = image_named("mine"); + + region_of_old_and_mine(); + ao_spy_subscribe(APP_EVICTED_SIG); + ask_recv_named(len, khal_crc32(KHAL_CRC32_INIT, s_named_img, len), false, + name); + raw_armed(true); + feed(s_named_img, (uint16_t)len); + tidy_settle(); + for ( ;; ) + { + const QEvt * const got = ao_spy_take(); + if ( NULL == got ) + { + break; + } + if ( (enum_t)got->sig == APP_EVICTED_SIG ) + { + *last = *(AppEvictedEvt const *)got; + ++n; + } + } + QActive_unsubscribe(ao_spy(), APP_EVICTED_SIG); + ao_spy_drain(); + return n; +} + +/* Without a name the board cannot tell the image is a new "mine": the oldest + * app goes, and the install says so on the bus. */ +TEST_CASE("an unnamed reinstall into a full region evicts the oldest app", + "[appmgr_ao]") +{ + AppEvictedEvt last; + + Mockkdrv_slot_Init(); + keep_fresh(); + TEST_ASSERT_EQUAL_UINT(1U, reinstall_mine("", &last)); + TEST_ASSERT_EQUAL_STRING("old", last.name); + TEST_ASSERT_FALSE(last.kept); + TEST_ASSERT_FALSE(named_installed("old")); + TEST_ASSERT_TRUE(named_installed("mine")); + keep_end(); +} + +/* Named, the app's own old copy makes room and nobody else pays for it. */ +TEST_CASE("a named reinstall into a full region gives up its own old copy", + "[appmgr_ao]") +{ + AppEvictedEvt last; + + Mockkdrv_slot_Init(); + keep_fresh(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, reinstall_mine("mine", &last), + "an eviction was published for the app being replaced"); + TEST_ASSERT_TRUE_MESSAGE(named_installed("old"), + "the oldest app paid for the reinstall"); + TEST_ASSERT_TRUE(named_installed("mine")); + TEST_ASSERT_EQUAL_UINT32(2U, live_entries()); + keep_end(); +} + + +/* The victim here fills the region, so the image that then fails its CRC was + * written over all of it: it is gone, and what is left is a directory without + * the victim and without the image that never verified. A failed image can + * never be left listed, or a boot request armed for it. */ +TEST_CASE("an install that evicts and then fails its CRC lists nothing new", + "[appmgr_ao][keep]") +{ + uint32_t len; + + Mockkdrv_slot_Init(); + keep_fresh(); + len = image_named("new"); + region_full_of_old(); + + ask_recv(len, khal_crc32(KHAL_CRC32_INIT, s_named_img, len) ^ 1U, false); + raw_armed(true); + feed(s_named_img, (uint16_t)len); + (void)qf_support_pump(AO_AppMgr, 8U); + + TEST_ASSERT_FALSE_MESSAGE(old_installed(), + "an app the write overwrote came back"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, live_entries(), + "an image that failed its CRC is listed"); + TEST_ASSERT_FALSE_MESSAGE(kdrv_apps_req_present(), + "a failed install left a boot request behind"); + keep_end(); +} + +/* ---- An install that fails gives back what its write never reached -------- + * The plan takes the victims out of the directory before the first byte + * arrives, but the region erases 64 KB as the data gets there: an install that + * stops early has erased only the blocks it reached. The victims on blocks it + * never touched are whole, and the failure puts their entries back. + */ + +/** @brief The bytes of the image the restore cases stream: two blocks. */ +#define UNDO_IMG_LEN (2U * KAPPS_ALIGN) + +/** @brief The restore cases' image: image_named()'s header, then zeros. */ +static uint8_t s_undo_img[UNDO_IMG_LEN]; + +/** + * @brief Five apps, one block each but the last, in install order. + * + * "v1" lies at the start of the data area, "k" one block on, then "v2", "v3" + * and the filler "f" to the end of the region. Install order is v1, v2, v3, + * k, f, so an install that needs two blocks evicts v1, v2 and v3, in that + * order, and lands on the blocks of v2 and v3 - which leaves v1, a victim + * that is never overwritten. + */ +static void region_of_victims(void) +{ + static const char *const name[5] = { "v1", "v2", "v3", "k", "f" }; + static const uint32_t at[5] = { 0U, 2U, 3U, 1U, 4U }; /* block of each */ + kapps_dir_t dir; + + if ( !s_keep_dir_held ) + { + keep_hold_dir(); + } + dir = s_keep_dir; + dir.count = 5U; + for ( uint32_t i = 0U; 5U > i; ++i ) + { + dir.e[i].offset = KAPPS_DATA_OFF + (at[i] * KAPPS_ALIGN); + dir.e[i].size = KAPPS_ALIGN; + dir.e[i].seq = i + 1U; + dir.e[i].flags = KAPPS_ENTRY_VALID; + plant_app(dir.e[i].offset, name[i]); + } + dir.e[4].size = kdrv_apps_region_size() - dir.e[4].offset; + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&dir)); +} + +/** + * @brief The start of a restore case: a fresh AO and Storage, and a spy that + * listens to nothing an earlier case asked it to. + * + * Thirty-two chunks put more on the bus than the spy's queue holds, and the + * guarantee QP gives a subscriber is an assertion when it cannot keep it. + */ +static void undo_fresh(void) +{ + keep_fresh(); + QActive_unsubscribeAll(ao_spy()); + ao_spy_drain(); +} + +/** @brief Build the image the restore cases stream, named @p project. */ +static void undo_image(const char *project) +{ + (void)image_named(project); + (void)memset(s_undo_img, 0, sizeof(s_undo_img)); + (void)memcpy(s_undo_img, s_named_img, sizeof(s_named_img)); +} + +/** @brief Stream the first @p bytes of the restore image from the console, a chunk at a time. */ +static void feed_undo_image(uint32_t bytes) +{ + for ( uint32_t off = 0U; off < bytes; off += KODE_RECV_CHUNK ) + { + feed(&s_undo_img[off], (uint16_t)KODE_RECV_CHUNK); + } +} + +/** @brief Which of the five apps are installed, as a string like "v1 k f". */ +static const char *victims_left(void) +{ + static char out[32]; + static const char *const name[5] = { "v1", "v2", "v3", "k", "f" }; + size_t n = 0U; + + out[0] = '\0'; + for ( unsigned i = 0U; 5U > i; ++i ) + { + if ( named_installed(name[i]) ) + { + if ( 0U < n ) + { + out[n++] = ' '; + } + n += (size_t)snprintf(&out[n], sizeof(out) - n, "%s", name[i]); + } + } + return out; +} + +TEST_CASE("a terminal install that fails its CRC gives back the apps it did not reach", + "[appmgr_ao][undo]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + region_of_victims(); + undo_image("new"); + + ask_recv_named(UNDO_IMG_LEN, khal_crc32(KHAL_CRC32_INIT, s_undo_img, UNDO_IMG_LEN) ^ 1U, + false, "new"); + raw_armed(true); + TEST_ASSERT_EQUAL_STRING_MESSAGE("k f", victims_left(), + "the plan did not take v1, v2 and v3 out"); + feed_undo_image(UNDO_IMG_LEN); + (void)qf_support_pump(AO_AppMgr, 8U); + + TEST_ASSERT_EQUAL_STRING_MESSAGE("v1 k f", victims_left(), + "the install's failure did not give back v1, whose blocks it never " + "touched, or gave back what it overwrote"); + TEST_ASSERT_FALSE(named_installed("new")); + TEST_ASSERT_TRUE(klog_has("kept 'v1' after the failed install")); + TEST_ASSERT_TRUE(klog_has("'v2' was overwritten and is gone")); + TEST_ASSERT_TRUE(klog_has("'v3' was overwritten and is gone")); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, storage_folder_requests(), + "a failed install deleted or wrote a data folder"); + keep_end(); +} + +TEST_CASE("a terminal install cut by the watchdog gives back every app it did not reach", + "[appmgr_ao][undo]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + region_of_victims(); + undo_image("new"); + + ask_recv_named(UNDO_IMG_LEN, khal_crc32(KHAL_CRC32_INIT, s_undo_img, UNDO_IMG_LEN), + false, "new"); + raw_armed(true); + feed_undo_image(KODE_RECV_CHUNK); /* the first block is erased, no more */ + run_ticks(APPMGR_RECV_IDLE_DEADLINE_TICKS + 50U); + + TEST_ASSERT_TRUE_MESSAGE(klog_has("the host is presumed gone"), + "the watchdog did not run"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("v1 v3 k f", victims_left(), + "a cut install lost an app whose blocks it never reached"); + TEST_ASSERT_TRUE(klog_has("kept 'v1' after the failed install")); + TEST_ASSERT_TRUE(klog_has("kept 'v3' after the failed install")); + TEST_ASSERT_TRUE(klog_has("'v2' was overwritten and is gone")); + (void)log_saw(CONSOLE_RAW_END_SIG); + keep_end(); +} + +TEST_CASE("an install from a medium that fails part-way gives back the apps it did not reach", + "[appmgr_ao][undo]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + region_of_victims(); + undo_image("new"); + + ask_flash_dst("new.bin", (uint8_t)KFW_TARGET_APP); + storage_expect(STORAGE_OPEN_REQ_SIG, "the install never asked for its file"); + answer_open(UNDO_IMG_LEN, (uint8_t)KDRV_MEDIUM_FS); + answer_data_bytes(s_undo_img, sizeof(s_named_img)); + for ( unsigned victim = 0U; 3U > victim; ++victim ) + { + storage_expect(STORAGE_PATH_REQ_SIG, "a victim's path was never asked about"); + answer_stat(false, (uint8_t)KODE_ERROR); /* no card: nothing to copy */ + (void)qf_support_pump(AO_AppMgr, 8U); + } + storage_expect(STORAGE_OPEN_REQ_SIG, "the install did not start again"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("v1 v2 v3 k f", victims_left(), + "the round evicted before the restart"); + + answer_open(UNDO_IMG_LEN, (uint8_t)KDRV_MEDIUM_FS); + answer_data_bytes(s_undo_img, sizeof(s_named_img)); + TEST_ASSERT_EQUAL_STRING_MESSAGE("k f", victims_left(), + "the second pass did not take the victims out"); + answer_eof(); /* the file ended early */ + run_ticks(4U); + + TEST_ASSERT_TRUE(klog_has("the file ended early")); + TEST_ASSERT_EQUAL_STRING_MESSAGE("v1 v3 k f", victims_left(), + "the failure did not give back what the write never reached"); + TEST_ASSERT_TRUE(klog_has("kept 'v1' after the failed install")); + TEST_ASSERT_TRUE(klog_has("kept 'v3' after the failed install")); + TEST_ASSERT_TRUE(klog_has("'v2' was overwritten and is gone")); + keep_end(); +} + +TEST_CASE("an install that lands evicts its victims for good", "[appmgr_ao][undo]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + region_of_victims(); + undo_image("new"); + + ask_recv_named(UNDO_IMG_LEN, khal_crc32(KHAL_CRC32_INIT, s_undo_img, UNDO_IMG_LEN), + false, "new"); + raw_armed(true); + feed_undo_image(UNDO_IMG_LEN); + tidy_settle(); + + TEST_ASSERT_EQUAL_STRING_MESSAGE("k f", victims_left(), + "an install that landed did not leave its victims evicted"); + TEST_ASSERT_TRUE(named_installed("new")); + TEST_ASSERT_FALSE_MESSAGE(klog_has("after the failed install"), + "an install that landed gave something back"); + keep_end(); +} + +TEST_CASE("a second failed install in a row gives back what it did not reach too", + "[appmgr_ao][undo]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + undo_image("new"); + + for ( unsigned run = 0U; 2U > run; ++run ) + { + fake_klog_reset(); + region_of_victims(); + ask_recv_named(UNDO_IMG_LEN, + khal_crc32(KHAL_CRC32_INIT, s_undo_img, UNDO_IMG_LEN) ^ 1U, + false, "new"); + raw_armed(true); + feed_undo_image(UNDO_IMG_LEN); + (void)qf_support_pump(AO_AppMgr, 8U); + TEST_ASSERT_EQUAL_STRING_MESSAGE("v1 k f", victims_left(), + "a failure in a row gave back the wrong apps"); + TEST_ASSERT_TRUE(klog_has("kept 'v1' after the failed install")); + } + keep_end(); +} + +/* ---- A request that asked for a verdict gets one --------------------------- + * `install` and `app_run` wait for AO_AppMgr: `>ok` is "taken, and under way", + * never "posted". The asker is the AO the request names, and the verdict is + * posted to it alone. + */ + +/** @brief Ask for an install of @p src, naming the spy as the asker. */ +static void ask_install_from(uint8_t src_kind, const char *src, uint8_t dst_kind) +{ + FlashReqEvt e; + + (void)memset(&e, 0, sizeof(e)); + e.super = (QEvt)QEVT_INITIALIZER(FLASH_REQ_SIG); + e.src_kind = src_kind; + e.dst_kind = dst_kind; + e.launch = false; + e.who = ao_spy(); + (void)snprintf(e.src, sizeof(e.src), "%s", src); + QASM_DISPATCH(AO_AppMgr, &e.super, 0U); +} + +/** @brief Ask to run the installed app @p name, naming the spy as the asker. */ +static void ask_run_from(const char *name) +{ + AppFlashEvt e; + + (void)memset(&e, 0, sizeof(e)); + e.super = (QEvt)QEVT_INITIALIZER(APP_RUN_REQ_SIG); + e.who = ao_spy(); + (void)snprintf(e.file, sizeof(e.file), "%s", name); + QASM_DISPATCH(AO_AppMgr, &e.super, 0U); +} + +/** @brief The verdicts the spy was posted, oldest first; the spy is drained. */ +typedef struct { + unsigned count; /**< How many APP_REQ_RSP_SIG came. */ + uint8_t first; /**< The first one's verdict. */ +} verdicts_t; + +/** @brief Count the verdicts the spy holds, and keep the first. */ +static verdicts_t spy_verdicts(void) +{ + verdicts_t v = { 0U, 0xFFU }; + + for ( ;; ) + { + const QEvt * const got = ao_spy_take(); + + if ( NULL == got ) + { + break; + } + if ( (enum_t)got->sig == APP_REQ_RSP_SIG ) + { + if ( 0U == v.count ) + { + v.first = ((AppReqRspEvt const *)got)->verdict; + } + ++v.count; + } + } + ao_spy_drain(); + return v; +} + +TEST_CASE("an https install is taken the moment it starts opening, before the server answers", + "[appmgr_ao][verdict]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + (void)net_requests(NET_STREAM_OPEN_REQ_SIG); + + ask_install_from((uint8_t)KFW_SRC_HTTP, "https://example.invalid/a.bin", + (uint8_t)KFW_TARGET_APP); + + verdicts_t const v = spy_verdicts(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, v.count, "an https install owes one verdict"); + TEST_ASSERT_EQUAL_UINT8((uint8_t)APP_REQ_TAKEN, v.first); + + let_the_open_expire(); + (void)net_requests(NET_STREAM_CLOSE_REQ_SIG); + keep_end(); +} + +TEST_CASE("an install from a medium is taken once the medium has opened the file", + "[appmgr_ao][verdict]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + + ask_install_from((uint8_t)KFW_SRC_SD, "a.bin", (uint8_t)KFW_TARGET_APP); + storage_expect(STORAGE_OPEN_REQ_SIG, "the install never asked for its file"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, spy_verdicts().count, + "the verdict came before the medium said the file is there"); + + answer_open(512U, (uint8_t)KDRV_MEDIUM_SD); + verdicts_t const v = spy_verdicts(); + TEST_ASSERT_EQUAL_UINT(1U, v.count); + TEST_ASSERT_EQUAL_UINT8((uint8_t)APP_REQ_TAKEN, v.first); + + answer_eof(); + run_ticks(4U); + keep_end(); +} + +TEST_CASE("an install of a file that is not on the medium is missing", + "[appmgr_ao][verdict]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + + ask_install_from((uint8_t)KFW_SRC_SD, "nope.bin", (uint8_t)KFW_TARGET_APP); + storage_expect(STORAGE_OPEN_REQ_SIG, "the install never asked for its file"); + answer_open_with(0U, (uint8_t)KDRV_MEDIUM_SD, (uint8_t)KODE_ERROR); + + verdicts_t const v = spy_verdicts(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, v.count, "a missing file owes one verdict"); + TEST_ASSERT_EQUAL_UINT8((uint8_t)APP_REQ_MISSING, v.first); + keep_end(); +} + +TEST_CASE("an install from an empty mailbox is empty, from a full one taken", + "[appmgr_ao][verdict]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + (void)kdrv_apps_pending_consume(); + + ask_install_from((uint8_t)KFW_SRC_PENDING, "", (uint8_t)KFW_TARGET_APP); + verdicts_t v = spy_verdicts(); + TEST_ASSERT_EQUAL_UINT(1U, v.count); + TEST_ASSERT_EQUAL_UINT8_MESSAGE((uint8_t)APP_REQ_EMPTY, v.first, + "an empty mailbox was taken for an install"); + + (void)park_pending("new"); + ask_install_from((uint8_t)KFW_SRC_PENDING, "", (uint8_t)KFW_TARGET_APP); + v = spy_verdicts(); + TEST_ASSERT_EQUAL_UINT(1U, v.count); + TEST_ASSERT_EQUAL_UINT8((uint8_t)APP_REQ_TAKEN, v.first); + + (void)qf_support_pump(AO_AppMgr, 64U); + (void)kdrv_apps_pending_consume(); + kfw_abort(); + keep_end(); +} + +TEST_CASE("an install or a run that arrives while an install is open is busy, and said", + "[appmgr_ao][verdict]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + + ask_install_from((uint8_t)KFW_SRC_SD, "a.bin", (uint8_t)KFW_TARGET_APP); + storage_expect(STORAGE_OPEN_REQ_SIG, "the install never asked for its file"); + (void)spy_verdicts(); + + fake_klog_reset(); + ask_install_from((uint8_t)KFW_SRC_SD, "b.bin", (uint8_t)KFW_TARGET_APP); + verdicts_t v = spy_verdicts(); + TEST_ASSERT_EQUAL_UINT(1U, v.count); + TEST_ASSERT_EQUAL_UINT8_MESSAGE((uint8_t)APP_REQ_BUSY, v.first, + "an install that was not taken was not refused as busy"); + TEST_ASSERT_TRUE(klog_has("busy opening")); + + ask_run_from("anything"); + v = spy_verdicts(); + TEST_ASSERT_EQUAL_UINT(1U, v.count); + TEST_ASSERT_EQUAL_UINT8((uint8_t)APP_REQ_BUSY, v.first); + + let_the_open_expire(); + keep_end(); +} + +TEST_CASE("an install or a run that arrives while an image streams in is busy, and said", + "[appmgr_ao][verdict]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + undo_image("new"); + + ask_recv_named(UNDO_IMG_LEN, 0U, false, "new"); + raw_armed(true); + fake_klog_reset(); + + ask_install_from((uint8_t)KFW_SRC_SD, "b.bin", (uint8_t)KFW_TARGET_APP); + verdicts_t v = spy_verdicts(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, v.count, "a request dropped with no answer"); + TEST_ASSERT_EQUAL_UINT8((uint8_t)APP_REQ_BUSY, v.first); + TEST_ASSERT_TRUE_MESSAGE(klog_has("busy receiving"), + "the request was dropped without a word"); + + ask_run_from("anything"); + v = spy_verdicts(); + TEST_ASSERT_EQUAL_UINT(1U, v.count); + TEST_ASSERT_EQUAL_UINT8((uint8_t)APP_REQ_BUSY, v.first); + + run_ticks(APPMGR_RECV_IDLE_DEADLINE_TICKS + 50U); + (void)log_saw(CONSOLE_RAW_END_SIG); + keep_end(); +} + +TEST_CASE("a terminal install that arrives while another transfer is open is refused, and said", + "[appmgr_ao][verdict]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + ao_spy_subscribe(RECV_FAIL_SIG); + + ask_install_from((uint8_t)KFW_SRC_SD, "a.bin", (uint8_t)KFW_TARGET_APP); + storage_expect(STORAGE_OPEN_REQ_SIG, "the install never asked for its file"); + (void)spy_verdicts(); + + fake_klog_reset(); + ask_recv(4096U, 0U, false); + TEST_ASSERT_TRUE_MESSAGE(klog_has("busy opening"), + "a terminal install met a busy AO and nobody was told"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, spy_count(RECV_FAIL_SIG), + "the console waiting for the install was left to its watchdog"); + + QActive_unsubscribe(ao_spy(), RECV_FAIL_SIG); + let_the_open_expire(); + keep_end(); +} + +TEST_CASE("running an app is taken when it is installed and missing when it is not", + "[appmgr_ao][verdict]") +{ + uint32_t off = 0U; + uint32_t size = 0U; + kboot_run_mode_t mode = KBOOT_RUN_STICKY; + + Mockkdrv_slot_Init(); + undo_fresh(); + keep_hold_dir(); + put_old(KAPPS_ALIGN); + (void)boot_run_request(&off, &size, &mode); + + ask_run_from("nothing-here"); + verdicts_t v = spy_verdicts(); + TEST_ASSERT_EQUAL_UINT(1U, v.count); + TEST_ASSERT_EQUAL_UINT8_MESSAGE((uint8_t)APP_REQ_MISSING, v.first, + "an app that is not installed was taken for a run"); + TEST_ASSERT_FALSE(boot_run_request(&off, &size, &mode)); + + ask_run_from("old"); + v = spy_verdicts(); + TEST_ASSERT_EQUAL_UINT(1U, v.count); + TEST_ASSERT_EQUAL_UINT8((uint8_t)APP_REQ_TAKEN, v.first); + TEST_ASSERT_TRUE(boot_run_request(&off, &size, &mode)); + keep_end(); +} + +/* ---- A reset in the middle of an install gives the victims back --------------- + * AO_AppMgr writes what an install evicts into NVS before it commits the + * eviction - each victim's entry, name and the CRC-32 of its extent - and takes + * it out when the install ends. A table found at start-up means a reset cut an + * install. It leaves NVS first of all; then each victim the directory no + * longer holds, and that nothing the directory holds now overlaps, is put back + * if the CRC of its extent, folded a part at a time, is the one recorded. + */ + +/** @brief The five apps of region_of_victims(), by index: where each lay is its block there. */ +static const char *const s_victim_name[5] = { "v1", "v2", "v3", "k", "f" }; + +/** @brief The CRC-32 of the extent [offset, offset + size), as the driver folds it. */ +static uint32_t extent_crc(uint32_t offset, uint32_t size) +{ + uint32_t crc = KHAL_CRC32_INIT; + + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_extent_crc(offset, size, &crc)); + return crc; +} + +/** @brief The record the table holds for app @p i of region_of_victims(), taken from the full directory as the flash holds it. */ +static appmgr_undo_rec_t victim_rec(const kapps_dir_t *full, unsigned i) +{ + appmgr_undo_rec_t r; + + (void)memset(&r, 0, sizeof(r)); + r.offset = full->e[i].offset; + r.size = full->e[i].size; + r.seq = full->e[i].seq; + r.flags = full->e[i].flags; + r.crc = extent_crc(r.offset, r.size); + (void)snprintf(r.project, sizeof(r.project), "%s", s_victim_name[i]); + return r; +} + +/** @brief Overwrite the start of the block at @p offset: what an install does to a victim it reaches. */ +static void overwrite_extent(uint32_t offset) +{ + static const uint8_t other[16] = { 0x11U, 0x22U }; + + kdrv_apps_write_abort(); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write_begin(offset, sizeof(other))); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write(other, sizeof(other))); + kdrv_apps_write_abort(); +} + +/** @brief Store @p n records as AO_AppMgr's table, with the header @p version and @p count. */ +static void seed_table_as(const void *rec, size_t rec_len, uint8_t n, uint8_t version, + uint8_t count) +{ + static uint8_t blob[APPMGR_UNDO_HDR_LEN + (APPMGR_UNDO_MAX * APPMGR_UNDO_REC_LEN)]; + appmgr_undo_hdr_t h; + + (void)memset(&h, 0, sizeof(h)); + h.version = version; + h.count = count; + (void)memcpy(blob, &h, sizeof(h)); + (void)memcpy(&blob[sizeof(h)], rec, (size_t)n * rec_len); + fake_nvs_seed(APPMGR_UNDO_NS, APPMGR_UNDO_KEY, blob, sizeof(h) + ((size_t)n * rec_len)); +} + +/** @brief Store @p n records as a table of this build's layout. */ +static void seed_table(const appmgr_undo_rec_t *rec, uint8_t n) +{ + seed_table_as(rec, sizeof(*rec), n, (uint8_t)APPMGR_UNDO_VERSION, n); +} + +/** @brief Is AO_AppMgr's table in NVS? */ +static bool table_in_nvs(void) +{ + size_t len = 0U; + + return khal_nvs_get_blob(APPMGR_UNDO_NS, APPMGR_UNDO_KEY, NULL, &len) == KODE_OK; +} + +/** @brief How many captured lines hold @p needle. */ +static unsigned klog_count_of(const char *needle) +{ + unsigned n = 0U; + + for ( unsigned i = 0U; fake_klog_count() > i; ++i ) + { + const char * const line = fake_klog_line(i); + + if ( (NULL != line) && (strstr(line, needle) != NULL) ) + { + ++n; + } + } + return n; +} + +/** + * @brief The five apps laid out, the directory holding those in @p mask (bit i = app i). + * @param full Receives the directory with all five, for the records' sake. + */ +static void region_holding(unsigned mask, kapps_dir_t *full) +{ + region_of_victims(); + *full = *kdrv_apps_dir(); + + kapps_dir_t dir = *full; + uint32_t w = 0U; + + for ( uint32_t i = 0U; i < full->count; ++i ) + { + if ( 0U != (mask & (1U << i)) ) + { + dir.e[w++] = full->e[i]; + } + } + dir.count = w; + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&dir)); +} + +/** @brief One pace of the recovery, in QP ticks, as appmgr_ao.c has it. */ +#define APPMGR_UNDO_PACE_TEST_TICKS (50U / 10U) + +/** @brief The bytes of an image one step of the recovery folds into its CRC. */ +#define APPMGR_UNDO_STEP_TEST_BYTES (512U * 1024U) + +/** @brief A start-up: the AO is made again over what NVS and the directory hold, with nothing run yet. */ +static void start_over(void) +{ + (void)qf_support_pump(AO_AppMgr, 8U); + appmgr_make(); + kfw_abort(); + kdrv_apps_write_abort(); +} + +/** @brief A start-up, run until the recovery has had every step it needs. */ +static void start_up(void) +{ + start_over(); + run_ticks(20U * APPMGR_UNDO_PACE_TEST_TICKS); +} + +/** @brief What the directory held at the moment NVS was written, and how many writes there were. */ +static unsigned s_dir_count_at_write; +static unsigned s_writes_seen; + +/** @brief Run inside every NVS write the fake is asked for. */ +static void note_directory_at_write(void) +{ + s_dir_count_at_write = kdrv_apps_dir()->count; + ++s_writes_seen; +} + +TEST_CASE("an install that evicts records its victims and their CRCs in NVS before the " + "eviction is committed", "[appmgr_ao][reset]") +{ + uint32_t want[3]; + + Mockkdrv_slot_Init(); + undo_fresh(); + region_of_victims(); + undo_image("new"); + for ( uint32_t i = 0U; 3U > i; ++i ) + { + want[i] = extent_crc(kdrv_apps_dir()->e[i].offset, kdrv_apps_dir()->e[i].size); + } + s_dir_count_at_write = 0U; + s_writes_seen = 0U; + + fake_nvs_set_hook(¬e_directory_at_write); + ask_recv_named(UNDO_IMG_LEN, khal_crc32(KHAL_CRC32_INIT, s_undo_img, UNDO_IMG_LEN), + false, "new"); + raw_armed(true); + fake_nvs_set_hook(NULL); + + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, s_writes_seen, + "an install that evicts three apps wrote NVS more or less than once"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(5U, s_dir_count_at_write, + "the table was written after the directory lost its victims"); + uint8_t blob[APPMGR_UNDO_HDR_LEN + (APPMGR_UNDO_MAX * APPMGR_UNDO_REC_LEN)]; + size_t len = sizeof(blob); + TEST_ASSERT_EQUAL_INT(KODE_OK, + khal_nvs_get_blob(APPMGR_UNDO_NS, APPMGR_UNDO_KEY, blob, &len)); + TEST_ASSERT_EQUAL_UINT(APPMGR_UNDO_HDR_LEN + (3U * APPMGR_UNDO_REC_LEN), len); + appmgr_undo_hdr_t hdr; + (void)memcpy(&hdr, blob, sizeof(hdr)); + TEST_ASSERT_EQUAL_UINT8(2U, hdr.version); + TEST_ASSERT_EQUAL_UINT8(3U, hdr.count); + appmgr_undo_rec_t rec[3]; + (void)memcpy(rec, &blob[sizeof(hdr)], sizeof(rec)); + TEST_ASSERT_EQUAL_STRING("v1", rec[0].project); + TEST_ASSERT_EQUAL_STRING("v2", rec[1].project); + TEST_ASSERT_EQUAL_STRING("v3", rec[2].project); + TEST_ASSERT_EQUAL_UINT32(2U, rec[1].seq); + TEST_ASSERT_EQUAL_UINT32(KAPPS_DATA_OFF + (2U * KAPPS_ALIGN), rec[1].offset); + TEST_ASSERT_EQUAL_UINT32(KAPPS_ALIGN, rec[1].size); + for ( uint32_t i = 0U; 3U > i; ++i ) + { + TEST_ASSERT_EQUAL_HEX32_MESSAGE(want[i], rec[i].crc, + "a record carries a CRC that is not its extent's before the eviction"); + } + + run_ticks(APPMGR_RECV_IDLE_DEADLINE_TICKS + 50U); /* the host never sends */ + (void)log_saw(CONSOLE_RAW_END_SIG); + keep_end(); +} + +TEST_CASE("a landed install takes its table out of NVS, a failed one after its restore", + "[appmgr_ao][reset]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + region_of_victims(); + undo_image("new"); + + ask_recv_named(UNDO_IMG_LEN, khal_crc32(KHAL_CRC32_INIT, s_undo_img, UNDO_IMG_LEN) ^ 1U, + false, "new"); + raw_armed(true); + TEST_ASSERT_TRUE(table_in_nvs()); + feed_undo_image(UNDO_IMG_LEN); /* the CRC fails at the end */ + (void)qf_support_pump(AO_AppMgr, 8U); + TEST_ASSERT_EQUAL_STRING("v1 k f", victims_left()); + TEST_ASSERT_FALSE_MESSAGE(table_in_nvs(), + "a failed install left its table in NVS after giving the apps back"); + + region_of_victims(); + ask_recv_named(UNDO_IMG_LEN, khal_crc32(KHAL_CRC32_INIT, s_undo_img, UNDO_IMG_LEN), + false, "new"); + raw_armed(true); + TEST_ASSERT_TRUE(table_in_nvs()); + feed_undo_image(UNDO_IMG_LEN); + tidy_settle(); + TEST_ASSERT_TRUE(named_installed("new")); + TEST_ASSERT_FALSE_MESSAGE(table_in_nvs(), + "a landed install left its table in NVS"); + keep_end(); +} + +TEST_CASE("an install that evicts nothing writes nothing to NVS, and one whose table " + "cannot be written goes on", "[appmgr_ao][reset]") +{ + uint32_t const len = image_named("new"); + + Mockkdrv_slot_Init(); + undo_fresh(); + keep_hold_dir(); + kapps_dir_t empty = *kdrv_apps_dir(); + empty.count = 0U; + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&empty)); + + ask_recv_named(len, khal_crc32(KHAL_CRC32_INIT, s_named_img, len), false, "new"); + raw_armed(true); + feed(s_named_img, (uint16_t)len); + tidy_settle(); + TEST_ASSERT_TRUE(named_installed("new")); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, fake_nvs_set_count(), + "an install with no victim wrote NVS"); + TEST_ASSERT_FALSE(table_in_nvs()); + + region_of_victims(); + undo_image("new2"); + fake_nvs_fail_next_set(); + ask_recv_named(UNDO_IMG_LEN, khal_crc32(KHAL_CRC32_INIT, s_undo_img, UNDO_IMG_LEN), + false, "new2"); + raw_armed(true); + TEST_ASSERT_TRUE_MESSAGE(klog_has("cannot record the apps this install evicts"), + "a table that could not be written went unsaid"); + feed_undo_image(UNDO_IMG_LEN); + tidy_settle(); + TEST_ASSERT_TRUE_MESSAGE(named_installed("new2"), + "an install stopped because its table could not be written"); + keep_end(); +} + +TEST_CASE("a start-up takes the table out of NVS before it checks anything", + "[appmgr_ao][reset]") +{ + kapps_dir_t full; + + Mockkdrv_slot_Init(); + undo_fresh(); + region_holding((1U << 3) | (1U << 4), &full); + appmgr_undo_rec_t const rec[2] = { victim_rec(&full, 0U), victim_rec(&full, 2U) }; + seed_table(rec, 2U); + TEST_ASSERT_TRUE(table_in_nvs()); + + start_over(); /* the initial transition and no step */ + + TEST_ASSERT_FALSE_MESSAGE(table_in_nvs(), + "the table was still in NVS when the checking began: a reset in it would loop the boot"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("k f", victims_left(), + "the initial transition did the recovery's work"); + TEST_ASSERT_FALSE(klog_has("kept 'v1'")); + + run_ticks(20U * APPMGR_UNDO_PACE_TEST_TICKS); + TEST_ASSERT_EQUAL_STRING("v1 v3 k f", victims_left()); + TEST_ASSERT_FALSE(table_in_nvs()); + keep_end(); +} + +TEST_CASE("a start-up that finds a table puts back the victims whose extent still has " + "its CRC and says which changed", "[appmgr_ao][reset]") +{ + kapps_dir_t full; + + Mockkdrv_slot_Init(); + undo_fresh(); + region_holding((1U << 3) | (1U << 4), &full); /* k and f: v1, v2, v3 were evicted */ + appmgr_undo_rec_t const rec[3] = { victim_rec(&full, 0U), victim_rec(&full, 1U), + victim_rec(&full, 2U) }; + seed_table(rec, 3U); + overwrite_extent(full.e[1].offset); /* the install reached v2 */ + + start_up(); + + TEST_ASSERT_EQUAL_STRING("v1 v3 k f", victims_left()); + TEST_ASSERT_TRUE(klog_has("kept 'v1' after an install a reset cut")); + TEST_ASSERT_TRUE(klog_has("kept 'v3' after an install a reset cut")); + TEST_ASSERT_TRUE(klog_has("'v2' was overwritten by an install a reset cut and is gone")); + TEST_ASSERT_FALSE_MESSAGE(table_in_nvs(), "the table is back in NVS"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, storage_folder_requests(), + "a recovered app's data folder was touched"); + keep_end(); +} + +TEST_CASE("a victim a reset left half written is told from one it left whole by its CRC " + "alone", "[appmgr_ao][reset]") +{ + kapps_dir_t full; + + Mockkdrv_slot_Init(); + undo_fresh(); + region_holding((1U << 3) | (1U << 4), &full); + appmgr_undo_rec_t rec[2] = { victim_rec(&full, 0U), victim_rec(&full, 2U) }; + rec[1].crc ^= 1U; /* v3's bytes are not the recorded ones */ + seed_table(rec, 2U); + + start_up(); + + TEST_ASSERT_EQUAL_STRING("v1 k f", victims_left()); + TEST_ASSERT_TRUE(klog_has("'v3' was overwritten by an install a reset cut and is gone")); + keep_end(); +} + +TEST_CASE("a start-up whose table names only apps the directory still holds changes " + "nothing and erases it", "[appmgr_ao][reset]") +{ + kapps_dir_t full; + + Mockkdrv_slot_Init(); + undo_fresh(); + region_holding(0x1FU, &full); + uint32_t const gen = kdrv_apps_dir()->gen; + appmgr_undo_rec_t const rec[2] = { victim_rec(&full, 0U), victim_rec(&full, 1U) }; + seed_table(rec, 2U); + + start_up(); + + TEST_ASSERT_EQUAL_STRING("v1 v2 v3 k f", victims_left()); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(gen, kdrv_apps_dir()->gen, + "the directory was committed with nothing to put back"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, klog_count_of("checking '"), + "an app the directory holds was checked"); + TEST_ASSERT_FALSE(table_in_nvs()); + keep_end(); +} + +TEST_CASE("a victim whose extent now overlaps an entry the directory holds is not put back", + "[appmgr_ao][reset]") +{ + kapps_dir_t full; + + Mockkdrv_slot_Init(); + undo_fresh(); + region_holding((1U << 3) | (1U << 4), &full); + appmgr_undo_rec_t const rec[3] = { victim_rec(&full, 0U), victim_rec(&full, 1U), + victim_rec(&full, 2U) }; + /* The install that was cut landed after all: its entry lies over v2. */ + kapps_dir_t dir = *kdrv_apps_dir(); + dir.e[dir.count].offset = full.e[1].offset; + dir.e[dir.count].size = 2U * KAPPS_ALIGN; + dir.e[dir.count].seq = 9U; + dir.e[dir.count].flags = KAPPS_ENTRY_VALID; + ++dir.count; + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&dir)); + seed_table(rec, 3U); + + start_up(); + + /* The entry over v2's blocks reads as "v2": its image is the one planted there. */ + TEST_ASSERT_EQUAL_STRING("v1 v2 k f", victims_left()); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(4U, kdrv_apps_dir()->count, + "v2 or v3 came back over the entry that overwrote them"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, klog_count_of("checking '"), + "an app lying under another entry was checked, or the free one was not"); + TEST_ASSERT_TRUE(klog_has("'v2' was overwritten by an install a reset cut and is gone")); + TEST_ASSERT_TRUE(klog_has("'v3' was overwritten by an install a reset cut and is gone")); + TEST_ASSERT_TRUE(klog_has("kept 'v1' after an install a reset cut")); + keep_end(); +} + +TEST_CASE("a table of another version or size is ignored and erased, with a warning; " + "the first layout's is one", "[appmgr_ao][reset]") +{ + struct { + uint32_t offset, size, seq, flags; + char project[KODE_APP_PROJECT_MAX]; + } old_rec[1]; + kapps_dir_t full; + + Mockkdrv_slot_Init(); + undo_fresh(); + region_holding((1U << 3) | (1U << 4), &full); + appmgr_undo_rec_t const rec[1] = { victim_rec(&full, 0U) }; + + seed_table_as(rec, sizeof(rec[0]), 1U, (uint8_t)APPMGR_UNDO_VERSION + 1U, 1U); + start_up(); + TEST_ASSERT_EQUAL_STRING("k f", victims_left()); + TEST_ASSERT_TRUE_MESSAGE(klog_has("not this build's"), "a table of another version went unsaid"); + TEST_ASSERT_FALSE(table_in_nvs()); + + fake_klog_reset(); + seed_table_as(rec, sizeof(rec[0]), 1U, (uint8_t)APPMGR_UNDO_VERSION, 2U); /* a count the bytes do not carry */ + start_up(); + TEST_ASSERT_EQUAL_STRING("k f", victims_left()); + TEST_ASSERT_TRUE(klog_has("not this build's")); + TEST_ASSERT_FALSE(table_in_nvs()); + + /* Version 1: records of 48 bytes, no digest. Nothing in them can be checked. */ + fake_klog_reset(); + (void)memset(old_rec, 0, sizeof(old_rec)); + old_rec[0].offset = rec[0].offset; + old_rec[0].size = rec[0].size; + old_rec[0].seq = rec[0].seq; + old_rec[0].flags = rec[0].flags; + (void)snprintf(old_rec[0].project, sizeof(old_rec[0].project), "%s", "v1"); + seed_table_as(old_rec, sizeof(old_rec[0]), 1U, 1U, 1U); + start_up(); + TEST_ASSERT_EQUAL_STRING("k f", victims_left()); + TEST_ASSERT_TRUE(klog_has("not this build's")); + TEST_ASSERT_FALSE(table_in_nvs()); + keep_end(); +} + +TEST_CASE("a start-up with no table does no work and writes nothing", "[appmgr_ao][reset]") +{ + kapps_dir_t full; + + Mockkdrv_slot_Init(); + undo_fresh(); + region_holding((1U << 3) | (1U << 4), &full); + uint32_t const gen = kdrv_apps_dir()->gen; + + start_up(); + + TEST_ASSERT_EQUAL_STRING("k f", victims_left()); + TEST_ASSERT_EQUAL_UINT32(gen, kdrv_apps_dir()->gen); + TEST_ASSERT_EQUAL_UINT(0U, klog_count_of("checking '")); + TEST_ASSERT_EQUAL_UINT(0U, fake_nvs_set_count()); + TEST_ASSERT_FALSE(klog_has("a reset cut")); + keep_end(); +} + +TEST_CASE("the recovery goes one paced step at a time, an extent in parts", "[appmgr_ao][reset]") +{ + uint32_t const big = 20U * KAPPS_ALIGN; /* 1.25 MB: three steps of 512 KB */ + + Mockkdrv_slot_Init(); + undo_fresh(); + keep_hold_dir(); + kapps_dir_t empty = *kdrv_apps_dir(); + empty.count = 0U; + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&empty)); + plant_app(KAPPS_DATA_OFF, "big"); + appmgr_undo_rec_t rec; + (void)memset(&rec, 0, sizeof(rec)); + rec.offset = KAPPS_DATA_OFF; + rec.size = big; + rec.seq = 1U; + rec.flags = KAPPS_ENTRY_VALID; + rec.crc = extent_crc(rec.offset, rec.size); + (void)snprintf(rec.project, sizeof(rec.project), "%s", "big"); + seed_table(&rec, 1U); + + start_over(); + run_ticks(APPMGR_UNDO_PACE_TEST_TICKS - 1U); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, klog_count_of("checking '"), + "a step ran before the timer's period"); + run_ticks(1U); + TEST_ASSERT_EQUAL_UINT(1U, klog_count_of("checking 'big': 524288 bytes from 0")); + run_ticks(APPMGR_UNDO_PACE_TEST_TICKS - 1U); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, klog_count_of("checking '"), + "a step followed another without waiting for the timer"); + run_ticks(1U); + TEST_ASSERT_EQUAL_UINT(2U, klog_count_of("checking '")); + TEST_ASSERT_TRUE(klog_has("checking 'big': 524288 bytes from 524288")); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, klog_count_of("kept 'big'"), + "the app was put back before its last part was read"); + + run_ticks(APPMGR_UNDO_PACE_TEST_TICKS); /* the third part: 262144 bytes */ + TEST_ASSERT_TRUE(klog_has("checking 'big': 262144 bytes from 1048576")); + TEST_ASSERT_EQUAL_UINT(3U, klog_count_of("checking '")); + run_ticks(2U * APPMGR_UNDO_PACE_TEST_TICKS); /* the end */ + TEST_ASSERT_TRUE(klog_has("kept 'big' after an install a reset cut")); + TEST_ASSERT_TRUE(named_installed("big")); + TEST_ASSERT_EQUAL_UINT(3U, klog_count_of("checking '")); + keep_end(); +} + +TEST_CASE("a list asked during the recovery is answered with the recovered directory, " + "and an install is told busy", "[appmgr_ao][reset]") +{ + kapps_dir_t full; + + Mockkdrv_slot_Init(); + undo_fresh(); + region_holding((1U << 3) | (1U << 4), &full); + appmgr_undo_rec_t const rec[2] = { victim_rec(&full, 0U), victim_rec(&full, 2U) }; + seed_table(rec, 2U); + + start_over(); /* recovering: no step has run */ + AppListReqEvt list; + (void)memset(&list, 0, sizeof(list)); + list.super = (QEvt)QEVT_INITIALIZER(APP_LIST_REQ_SIG); + list.who = ao_spy(); + QASM_DISPATCH(AO_AppMgr, &list.super, 0U); + ask_install_from((uint8_t)KFW_SRC_SD, "a.bin", (uint8_t)KFW_TARGET_APP); + + unsigned entries = 0U; + unsigned ends = 0U; + unsigned busy = 0U; + for ( const QEvt *e = ao_spy_take(); NULL != e; e = ao_spy_take() ) + { + if ( (enum_t)e->sig == APP_ENTRY_SIG ) { ++entries; } + if ( (enum_t)e->sig == APP_LIST_END_SIG ) { ++ends; } + if ( ((enum_t)e->sig == APP_REQ_RSP_SIG) + && ((uint8_t)APP_REQ_BUSY == ((AppReqRspEvt const *)e)->verdict) ) { ++busy; } + } + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, entries, + "a list was answered while the directory was about to change"); + TEST_ASSERT_EQUAL_UINT(0U, ends); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, busy, "an install was not told the AO is busy"); + ao_spy_drain(); + + run_ticks(20U * APPMGR_UNDO_PACE_TEST_TICKS); /* the recovery ends */ + for ( const QEvt *e = ao_spy_take(); NULL != e; e = ao_spy_take() ) + { + if ( (enum_t)e->sig == APP_ENTRY_SIG ) { ++entries; } + if ( (enum_t)e->sig == APP_LIST_END_SIG ) { ++ends; } + } + TEST_ASSERT_EQUAL_UINT_MESSAGE(4U, entries, + "the deferred list did not show the recovered apps, in the directory's order"); + TEST_ASSERT_EQUAL_UINT(1U, ends); + TEST_ASSERT_EQUAL_STRING("v1 v3 k f", victims_left()); + keep_end(); +} + +/* ---- Something shows from the first moment, and keeps showing ------------- + * The write card and the LED follow APP_FLASH_PROGRESS_SIG alone, and AO_Power + * takes the LED back three seconds after the last one. A download spends + * longer than that in DNS and TLS before its first byte, so the 0 % goes out + * the moment the install is accepted and the percent is said again every + * second while the network is waited on. + */ + +/** @brief What the progress events the spy holds said, in order. */ +typedef struct { + unsigned count; /**< How many. */ + unsigned pct[8]; /**< The first eight percents. */ + AppProgressEvt first; /**< The first, whole. */ +} progress_seen_t; + +/** @brief Count the APP_FLASH_PROGRESS_SIG the spy holds and keep what they said; drains the spy. */ +static progress_seen_t spy_progress(void) +{ + progress_seen_t seen; + + (void)memset(&seen, 0, sizeof(seen)); + for ( ;; ) + { + const QEvt * const got = ao_spy_take(); + + if ( NULL == got ) + { + break; + } + if ( (enum_t)got->sig == APP_FLASH_PROGRESS_SIG ) + { + AppProgressEvt const * const p = (AppProgressEvt const *)got; + + if ( 0U == seen.count ) + { + seen.first = *p; + } + if ( Q_DIM(seen.pct) > seen.count ) + { + seen.pct[seen.count] = p->percent; + } + ++seen.count; + } + } + ao_spy_drain(); + return seen; +} + +/** @brief The gap AppMgr leaves between two chunks, in QP ticks. */ +#define APPMGR_CHUNK_GAP_TEST_TICKS (10U / 10U) + +/** @brief One second of waiting on the network, in QP ticks, as appmgr_ao.c has it. */ +#define APPMGR_WAIT_BEAT_TICKS (1000U / 10U) + +/** @brief Answer the open of the download in hand: @p size bytes, status @p st. */ +static void answer_net_open(uint32_t size, uint8_t st) +{ + StorageOpenRspEvt o; + + (void)memset(&o, 0, sizeof(o)); + o.super = (QEvt)QEVT_INITIALIZER(NET_STREAM_OPEN_RSP_SIG); + o.size = size; + o.status = st; + QASM_DISPATCH(AO_AppMgr, &o.super, 0U); +} + +TEST_CASE("an https install shows its 0 % the moment it is accepted, with its label", + "[appmgr_ao][beat]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + ao_spy_subscribe(APP_FLASH_PROGRESS_SIG); + (void)net_requests(NET_STREAM_OPEN_REQ_SIG); + + ask_https("https://example.invalid/app.bin"); + + progress_seen_t const seen = spy_progress(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.count, + "the card and the LED wait for the server to answer"); + TEST_ASSERT_EQUAL_UINT(0U, seen.pct[0]); + TEST_ASSERT_EQUAL_UINT8((uint8_t)KWRITE_APP, seen.first.what); + TEST_ASSERT_TRUE_MESSAGE('\0' != seen.first.file[0], + "the 0 % does not say what is being fetched"); + + QActive_unsubscribe(ao_spy(), APP_FLASH_PROGRESS_SIG); + let_the_open_expire(); + (void)net_requests(NET_STREAM_CLOSE_REQ_SIG); + keep_end(); +} + +TEST_CASE("an https install keeps saying its percent every second while it opens, " + "and stops when it is over", "[appmgr_ao][beat]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + ao_spy_subscribe(APP_FLASH_PROGRESS_SIG); + (void)net_requests(NET_STREAM_OPEN_REQ_SIG); + + ask_https("https://example.invalid/app.bin"); + TEST_ASSERT_EQUAL_UINT(1U, spy_progress().count); + + run_ticks(APPMGR_WAIT_BEAT_TICKS - 5U); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, spy_progress().count, "a beat came early"); + run_ticks(10U); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, spy_progress().count, + "no beat after a second of waiting for the server"); + run_ticks(APPMGR_WAIT_BEAT_TICKS); + TEST_ASSERT_EQUAL_UINT(1U, spy_progress().count); + + answer_net_open(0U, (uint8_t)KODE_ERROR); /* the server says no */ + (void)spy_progress(); + run_ticks(3U * APPMGR_WAIT_BEAT_TICKS); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, spy_progress().count, + "the beat went on after the install ended"); + + QActive_unsubscribe(ao_spy(), APP_FLASH_PROGRESS_SIG); + (void)net_requests(NET_STREAM_CLOSE_REQ_SIG); + keep_end(); +} + +TEST_CASE("a download that stalls keeps its percent on the glass", + "[appmgr_ao][beat]") +{ + Mockkdrv_slot_Init(); + undo_fresh(); + keep_hold_dir(); + kapps_dir_t empty = *kdrv_apps_dir(); + empty.count = 0U; + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&empty)); + ao_spy_subscribe(APP_FLASH_PROGRESS_SIG); + (void)net_requests(NET_STREAM_OPEN_REQ_SIG); + + ask_https("https://example.invalid/app.bin"); + (void)spy_progress(); + answer_net_open(UNDO_IMG_LEN, (uint8_t)KODE_OK); /* open, then silence */ + progress_seen_t seen = spy_progress(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.count, "the write began and said nothing"); - unsigned const early = spy_done(&done); - tidy_settle(); /* the victim's folder goes first */ - unsigned const dones = spy_done(&done); - QActive_unsubscribe(ao_spy(), APP_FLASH_DONE_SIG); - ao_spy_drain(); + run_ticks((2U * APPMGR_WAIT_BEAT_TICKS) + 10U); + seen = spy_progress(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(2U, seen.count, + "a download with no bytes coming fell silent"); + TEST_ASSERT_EQUAL_UINT(0U, seen.pct[0]); - TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, early, - "the install ended before the evicted app's folder was deleted"); - TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, storage_tally().path, - "the console path asked AO_Storage about a victim it cannot wait for"); - TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, dones, "the install did not complete"); - TEST_ASSERT_FALSE_MESSAGE(old_installed(), "the victim was not evicted"); - TEST_ASSERT_TRUE_MESSAGE(klog_has("evicting 'old' to make room; no copy kept"), - "the victim went without a word"); + QActive_unsubscribe(ao_spy(), APP_FLASH_PROGRESS_SIG); + let_the_open_expire(); + (void)net_requests(NET_STREAM_CLOSE_REQ_SIG); keep_end(); } -/** @brief How many live entries the directory holds. */ -static uint32_t live_entries(void) +TEST_CASE("a percent the pool had no room for is said again, not counted as said", + "[appmgr_ao][beat]") { - kapps_dir_t const *d = kdrv_apps_dir(); - uint32_t n = 0U; + static QEvt *hoard[96]; + unsigned held = 0U; + uint32_t const total = 400000U; + uint32_t const one_percent = total / 100U; - for ( uint32_t i = 0U; i < d->count; ++i ) + Mockkdrv_slot_Init(); + undo_fresh(); + keep_hold_dir(); + kapps_dir_t empty = *kdrv_apps_dir(); + empty.count = 0U; + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&empty)); + undo_image("new"); + + ask_flash_dst("new.bin", (uint8_t)KFW_TARGET_APP); + storage_expect(STORAGE_OPEN_REQ_SIG, "the install never asked for its file"); + answer_open(total, (uint8_t)KDRV_MEDIUM_FS); + ao_spy_subscribe(APP_FLASH_PROGRESS_SIG); + (void)spy_progress(); + + while ( Q_DIM(hoard) > held ) { - if ( (d->e[i].flags & KAPPS_ENTRY_VALID) != 0U ) + AppProgressEvt * const e = Q_NEW_X(AppProgressEvt, 0U, Q_USER_SIG); + + if ( NULL == e ) { - ++n; + break; } + hoard[held] = &e->super; + ++held; } - return n; + TEST_ASSERT_TRUE_MESSAGE(Q_DIM(hoard) > held, "the pool outlasted the hoard"); + answer_data_bytes(s_undo_img, one_percent + 1U); /* 1 %: no room to say so */ + for ( unsigned i = 0U; i < held; ++i ) + { + QF_gc(hoard[i]); + } + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, spy_progress().count, "the pool was not dry"); + + run_ticks(APPMGR_CHUNK_GAP_TEST_TICKS + 2U); /* the chunk's buffer goes back */ + answer_data_bytes(&s_undo_img[one_percent + 1U], 16U); /* still 1 % */ + progress_seen_t const seen = spy_progress(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.count, + "a percent that was dropped for want of an event was taken as said"); + TEST_ASSERT_EQUAL_UINT(1U, seen.pct[0]); + + QActive_unsubscribe(ao_spy(), APP_FLASH_PROGRESS_SIG); + let_the_open_expire(); + keep_end(); } -/** - * @brief "old" (seq 1) in the region's first 64 KB and "mine" (seq 2) in the - * rest: no gap anywhere, so an install has to evict. +/* ---- A slow link is not a dead one ----------------------------------------- + * The phone app waits 10 s for an ack before it sends a chunk again, so a + * board that gave up after 5 s of silence lost installs the host was still + * retrying; and a `>ok` still queued for the link when the board reset into + * the new app never arrived, so the phone retried against a running app. */ -static void region_of_old_and_mine(void) + +/** @brief ask_recv_named(), from the port @p src (KODE_CONSOLE_SRC_*). */ +static void ask_recv_from(uint8_t src, uint32_t size, uint32_t crc32_val, + bool launch, const char *name) { - keep_hold_dir(); - kapps_dir_t dir = s_keep_dir; - dir.count = 2U; - dir.e[0].offset = KAPPS_DATA_OFF; - dir.e[0].size = KAPPS_ALIGN; - dir.e[0].seq = 1U; - dir.e[0].flags = KAPPS_ENTRY_VALID; - dir.e[1].offset = KAPPS_DATA_OFF + KAPPS_ALIGN; - dir.e[1].size = kdrv_apps_region_size() - dir.e[1].offset; - dir.e[1].seq = 2U; - dir.e[1].flags = KAPPS_ENTRY_VALID; - plant_app(dir.e[0].offset, "old"); - plant_app(dir.e[1].offset, "mine"); - TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&dir)); + AppRecvReqEvt e; + + (void)memset(&e, 0, sizeof(e)); + e.super = (QEvt)QEVT_INITIALIZER(APP_RECV_REQ_SIG); + e.dst = (uint8_t)KFW_TARGET_APP; + e.size = size; + e.crc32 = crc32_val; + e.launch = launch; + e.src = src; + (void)snprintf(e.name, sizeof(e.name), "%s", name); + QASM_DISPATCH(AO_AppMgr, &e.super, 0U); } -/** @brief Whether an app named @p project is in the directory. */ -static bool named_installed(const char *project) +TEST_CASE("a terminal install over BLE is given 15 s of silence, not the cable's 5", + "[appmgr_ao][ble]") { - kapps_dir_t const *d = kdrv_apps_dir(); + Mockkdrv_slot_Init(); + undo_fresh(); + ask_recv_from((uint8_t)KODE_CONSOLE_SRC_BLE, 4096U, 0U, false, ""); + raw_armed(true); - for ( uint32_t i = 0U; i < d->count; ++i ) - { - kdrv_slot_desc_t desc; - if ( ((d->e[i].flags & KAPPS_ENTRY_VALID) != 0U) - && (kdrv_apps_desc(&d->e[i], &desc, NULL) == KODE_OK) - && (strcmp(desc.project, project) == 0) ) - { - return true; - } - } - return false; + run_ticks(APPMGR_RECV_IDLE_DEADLINE_TICKS + 50U); /* past the cable's bound */ + TEST_ASSERT_FALSE_MESSAGE(klog_has("the host is presumed gone"), + "a BLE transfer was given up on after the cable's 5 s"); + + run_ticks(((KODE_RECV_IDLE_BLE_MS - KODE_RECV_IDLE_MS) / 10U) + 50U); + TEST_ASSERT_TRUE_MESSAGE(klog_has("no data for 15000 ms"), + "a BLE transfer that went silent was never given up on"); + (void)log_saw(CONSOLE_RAW_END_SIG); + keep_end(); } -/** - * @brief Stream "mine" again from the console, named or not, and count the - * APP_EVICTED_SIG it publishes. - * @param name The name= the host sends. - * @param[out] last The last eviction published, when there was one. - * @return How many were published. - */ -static unsigned reinstall_mine(const char *name, AppEvictedEvt *last) +/** @brief A terminal install of "new" with run=on, from @p src, landed: the AO is launching. */ +static void landed_over(uint8_t src) { - unsigned n = 0U; - uint32_t const len = image_named("mine"); + uint32_t const len = image_named("new"); - region_of_old_and_mine(); - ao_spy_subscribe(APP_EVICTED_SIG); - ask_recv_named(len, khal_crc32(KHAL_CRC32_INIT, s_named_img, len), false, - name); + undo_fresh(); + keep_hold_dir(); + kapps_dir_t empty = *kdrv_apps_dir(); + empty.count = 0U; + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&empty)); + fake_ble_console_set_backlog(0U); + ask_recv_from(src, len, khal_crc32(KHAL_CRC32_INIT, s_named_img, len), true, + "new"); raw_armed(true); feed(s_named_img, (uint16_t)len); tidy_settle(); - for ( ;; ) - { - const QEvt * const got = ao_spy_take(); - if ( NULL == got ) - { - break; - } - if ( (enum_t)got->sig == APP_EVICTED_SIG ) - { - *last = *(AppEvictedEvt const *)got; - ++n; - } - } - QActive_unsubscribe(ao_spy(), APP_EVICTED_SIG); - ao_spy_drain(); - return n; } -/* Without a name the board cannot tell the image is a new "mine": the oldest - * app goes, and the install says so on the bus. */ -TEST_CASE("an unnamed reinstall into a full region evicts the oldest app", - "[appmgr_ao]") +TEST_CASE("a launch over BLE waits for the console's bytes to leave, up to 2 s", + "[appmgr_ao][ble]") { - AppEvictedEvt last; + uint32_t off = 0U; + uint32_t size = 0U; + kboot_run_mode_t mode = KBOOT_RUN_STICKY; Mockkdrv_slot_Init(); - keep_fresh(); - TEST_ASSERT_EQUAL_UINT(1U, reinstall_mine("", &last)); - TEST_ASSERT_EQUAL_STRING("old", last.name); - TEST_ASSERT_FALSE(last.kept); - TEST_ASSERT_FALSE(named_installed("old")); - TEST_ASSERT_TRUE(named_installed("mine")); + landed_over((uint8_t)KODE_CONSOLE_SRC_BLE); + (void)boot_run_request(&off, &size, &mode); + fake_ble_console_set_backlog(40U); /* the `>ok`, still queued */ + + run_ticks(APPMGR_ANSWER_DRAIN_DEADLINE_TICKS + 2U); + TEST_ASSERT_FALSE_MESSAGE(boot_run_request(&off, &size, &mode), + "the board reset with the `>ok` still queued for the link"); + run_ticks(100U); + TEST_ASSERT_FALSE(boot_run_request(&off, &size, &mode)); + + fake_ble_console_set_backlog(0U); /* the link took it */ + run_ticks(APPMGR_ANSWER_DRAIN_DEADLINE_TICKS + 2U); + TEST_ASSERT_TRUE_MESSAGE(boot_run_request(&off, &size, &mode), + "the launch did not follow the answer once the link had it"); keep_end(); } -/* Named, the app's own old copy makes room and nobody else pays for it. */ -TEST_CASE("a named reinstall into a full region gives up its own old copy", - "[appmgr_ao]") +TEST_CASE("a launch over BLE is not held for ever by a link that never drains", + "[appmgr_ao][ble]") { - AppEvictedEvt last; + uint32_t off = 0U; + uint32_t size = 0U; + kboot_run_mode_t mode = KBOOT_RUN_STICKY; Mockkdrv_slot_Init(); - keep_fresh(); - TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, reinstall_mine("mine", &last), - "an eviction was published for the app being replaced"); - TEST_ASSERT_TRUE_MESSAGE(named_installed("old"), - "the oldest app paid for the reinstall"); - TEST_ASSERT_TRUE(named_installed("mine")); - TEST_ASSERT_EQUAL_UINT32(2U, live_entries()); + landed_over((uint8_t)KODE_CONSOLE_SRC_BLE); + (void)boot_run_request(&off, &size, &mode); + fake_ble_console_set_backlog(40U); + + run_ticks((KODE_RECV_DRAIN_BLE_MS / 10U) - 10U); + TEST_ASSERT_FALSE(boot_run_request(&off, &size, &mode)); + run_ticks(30U); + TEST_ASSERT_TRUE_MESSAGE(boot_run_request(&off, &size, &mode), + "a link that never drained held the launch past its bound"); + TEST_ASSERT_TRUE(klog_has("launching anyway")); + fake_ble_console_set_backlog(0U); keep_end(); } - -/* The victims are committed out of the directory before the first byte of - * the new image arrives, so an install that then fails has cost them: what - * is left is a directory without the victim and without the image that never - * came. Pinned here so that the day this changes, it changes on purpose - and - * so that a failed image can never be left listed, or a boot request armed - * for it. */ -TEST_CASE("an install that evicts and then fails its CRC lists nothing new", - "[appmgr_ao][keep]") +TEST_CASE("a launch over the cable does not wait for the BLE link", + "[appmgr_ao][ble]") { - uint32_t len; + uint32_t off = 0U; + uint32_t size = 0U; + kboot_run_mode_t mode = KBOOT_RUN_STICKY; Mockkdrv_slot_Init(); - keep_fresh(); - len = image_named("new"); - region_full_of_old(); - - ask_recv(len, khal_crc32(KHAL_CRC32_INIT, s_named_img, len) ^ 1U, false); - raw_armed(true); - feed(s_named_img, (uint16_t)len); - (void)qf_support_pump(AO_AppMgr, 8U); + landed_over((uint8_t)KODE_CONSOLE_SRC_SERIAL); + (void)boot_run_request(&off, &size, &mode); + fake_ble_console_set_backlog(40U); /* somebody else's bytes */ - TEST_ASSERT_FALSE_MESSAGE(old_installed(), "the victim came back"); - TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, live_entries(), - "an image that failed its CRC is listed"); - TEST_ASSERT_FALSE_MESSAGE(kdrv_apps_req_present(), - "a failed install left a boot request behind"); + run_ticks(APPMGR_ANSWER_DRAIN_DEADLINE_TICKS + 2U); + TEST_ASSERT_TRUE_MESSAGE(boot_run_request(&off, &size, &mode), + "a cable install waited on a BLE link it does not use"); + fake_ble_console_set_backlog(0U); keep_end(); } @@ -3531,7 +5015,7 @@ TEST_CASE("a restarted install that fails clears the round", "[appmgr_ao][keep]" region_full_of_old(); uint32_t const len = begin_install((uint8_t)KDRV_MEDIUM_FS, "new"); storage_expect(STORAGE_PATH_REQ_SIG, "the victim's path was never asked about"); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(old_bytes()); (void)qf_support_pump(AO_AppMgr, 8U); storage_expect(STORAGE_OPEN_REQ_SIG, "the install did not start again"); @@ -3548,7 +5032,7 @@ TEST_CASE("a restarted install that fails clears the round", "[appmgr_ao][keep]" storage_expect(STORAGE_PATH_REQ_SIG, "the next install did not start a round of its own"); - answer_stat(true, (uint8_t)KODE_OK); /* and ends it, so nothing is armed */ + answer_copy_of(old_bytes()); /* and ends it, so nothing is armed */ (void)qf_support_pump(AO_AppMgr, 8U); let_the_open_expire(); keep_end(); @@ -3608,6 +5092,9 @@ TEST_CASE("the bar does not go quiet while the card writes", "[appmgr_ao][keep]" } /** @brief A directory of 64 tiny apps, "a00" (seq 1) to "a63" (seq 64). */ +/** @brief The length of each of region_of_tiny_apps()'s apps. */ +#define TINY_APP_LEN 4096U + static void region_of_tiny_apps(void) { keep_hold_dir(); @@ -3618,7 +5105,7 @@ static void region_of_tiny_apps(void) char name[8]; (void)snprintf(name, sizeof(name), "a%02u", (unsigned)i); dir.e[i].offset = KAPPS_DATA_OFF + (i * KAPPS_ALIGN); - dir.e[i].size = 4096U; + dir.e[i].size = TINY_APP_LEN; dir.e[i].seq = i + 1U; dir.e[i].flags = KAPPS_ENTRY_VALID; plant_app(dir.e[i].offset, name); @@ -3698,7 +5185,7 @@ TEST_CASE("two victims: one already kept, one copied, one bar", TEST_ASSERT_NOT_NULL(stat); TEST_ASSERT_EQUAL_STRING("/sdcard/first.bin", ((StoragePathEvt const *)stat)->path); QF_gc(stat); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(3U * mb); stat = storage_take(STORAGE_PATH_REQ_SIG); TEST_ASSERT_NOT_NULL_MESSAGE(stat, "the second victim was not looked for"); TEST_ASSERT_EQUAL_STRING("/sdcard/second.bin", ((StoragePathEvt const *)stat)->path); @@ -3872,7 +5359,7 @@ TEST_CASE("a late write answer settles nothing", "[appmgr_ao][keep]") TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, storage_tally().open, "a late answer ended the round with a victim's STAT still out"); - answer_stat(true, (uint8_t)KODE_OK); /* the round ends properly */ + answer_copy_of(3U * mb); /* the round ends properly */ (void)qf_support_pump(AO_AppMgr, 8U); TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, storage_tally().open, "the install did not start again"); @@ -3924,7 +5411,7 @@ TEST_CASE("a late STAT answer is not taken for the next victim's", TEST_ASSERT_EQUAL_STRING("/sdcard/second.bin", ((StoragePathEvt const *)stat)->path); QF_gc(stat); - answer_stat(true, (uint8_t)KODE_OK); /* the first one's, late */ + answer_copy_of(3U * mb); /* the first one's, late */ (void)qf_support_pump(AO_AppMgr, 8U); TEST_ASSERT_FALSE_MESSAGE(klog_has("'second' is already on the card"), "the late answer was taken for the second victim"); @@ -3962,7 +5449,7 @@ static void next_install_keeps(void) storage_expect(STORAGE_PATH_REQ_SIG, "the install after the adoption did not look for its victim: " "the adoption's round was still standing"); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(old_bytes()); (void)qf_support_pump(AO_AppMgr, 8U); storage_expect(STORAGE_OPEN_REQ_SIG, "the install did not start again after its round"); @@ -4062,6 +5549,38 @@ TEST_CASE("install src=pending boots the adopted app only on run, and once", "a console install boots once: any reset must come home"); } +/* An install asked for with nothing parked does nothing, and a host that + * reads the log has to be told. The boot's own look at an empty mailbox stays + * quiet: that is every boot. */ +TEST_CASE("an install from an empty mailbox says there is nothing to install", + "[appmgr_ao]") +{ + static const QEvt adopt = QEVT_INITIALIZER(APP_ADOPT_SIG); + FlashReqEvt req; + + Mockkdrv_slot_Init(); + appmgr_fresh(); + (void)kdrv_apps_pending_consume(); + TEST_ASSERT_FALSE(kdrv_apps_pending_present(NULL, NULL)); + + fake_klog_reset(); + QASM_DISPATCH(AO_AppMgr, &adopt, 0U); + run_ticks(2U); + TEST_ASSERT_FALSE_MESSAGE(klog_has("the mailbox is empty"), + "the boot's own look at an empty mailbox said so"); + + (void)memset(&req, 0, sizeof(req)); + req.super = (QEvt)QEVT_INITIALIZER(FLASH_REQ_SIG); + req.src_kind = (uint8_t)KFW_SRC_PENDING; + req.dst_kind = (uint8_t)KFW_TARGET_APP; + (void)snprintf(req.src, sizeof(req.src), "%s", "pending"); + fake_klog_reset(); + QASM_DISPATCH(AO_AppMgr, &req.super, 0U); + run_ticks(4U); + TEST_ASSERT_TRUE_MESSAGE(klog_has("the mailbox is empty"), + "an install of nothing said nothing"); +} + /* The first plan evicts "old", so the round runs before anything is evicted: * the STAT, the copy, the write's answer, and only then the second pass. */ TEST_CASE("adoption over a full region keeps the victim first", @@ -4186,6 +5705,9 @@ TEST_CASE("adoption goes on when the victim cannot be kept", /** @brief Three apps that fill the region: "first" and "second" of 3 MB, then * "tail" up to its end. A 5 MB image has to evict the first two. */ +/** @brief The length of the first two apps of three_apps(). */ +#define THREE_APP_LEN (3U * 0x100000U) + static void three_apps(void) { uint32_t const mb = 0x100000U; @@ -4270,8 +5792,12 @@ TEST_CASE("a deflated container is planned for its image, so every victim is kep TEST_ASSERT_TRUE(klog_has("evicting 'first'")); TEST_ASSERT_TRUE(klog_has("evicting 'second'")); - TEST_ASSERT_FALSE_MESSAGE(seq_installed(1U) || seq_installed(2U), - "the second pass did not evict both"); + /* The fake inflater yields no image, so the adoption fails before its + * write erases a block: both victims come back with the failure. */ + TEST_ASSERT_TRUE_MESSAGE(seq_installed(1U) && seq_installed(2U), + "the failed adoption did not give back the apps its write never reached"); + TEST_ASSERT_TRUE(klog_has("kept 'first' after the failed install")); + TEST_ASSERT_TRUE(klog_has("kept 'second' after the failed install")); TEST_ASSERT_FALSE_MESSAGE(klog_has("no copy kept"), "a victim the card had taken went down with a warning"); TEST_ASSERT_FALSE_MESSAGE(klog_has("before this install"), @@ -4319,10 +5845,14 @@ TEST_CASE("a plan that changes under an adoption is deferred from inside it", TEST_ASSERT_TRUE_MESSAGE(kdrv_apps_pending_present(NULL, NULL), "the mailbox must be left where it was"); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(old_bytes()); run_ticks(8U); - TEST_ASSERT_TRUE(klog_has("evicting 'old'")); - TEST_ASSERT_FALSE_MESSAGE(seq_installed(1U), "the second pass did not evict"); + TEST_ASSERT_TRUE_MESSAGE(klog_has("evicting 'old'"), "the second pass did not evict"); + /* The fake inflater yields no image: the adoption fails before its write + * erases a block, and "old" comes back with the failure. */ + TEST_ASSERT_TRUE_MESSAGE(seq_installed(1U), + "the failed adoption did not give back the app its write never reached"); + TEST_ASSERT_TRUE(klog_has("kept 'old' after the failed install")); (void)kdrv_apps_pending_consume(); /* the fake inflater yields no image */ kfw_abort(); @@ -4355,7 +5885,7 @@ TEST_CASE("a copy whose write failed is unlinked, so no later round calls it kep storage_expect(STORAGE_OPEN_REQ_SIG, "the install did not start again"); /* The unlink's answer reaches the restarted install and means nothing. */ - answer_path((uint8_t)STORAGE_PATH_UNLINK, (uint8_t)KODE_OK, false, false); + answer_path((uint8_t)STORAGE_PATH_UNLINK, (uint8_t)KODE_OK, false, false, 0U); TEST_ASSERT_TRUE(old_installed()); let_the_open_expire(); keep_end(); @@ -4393,13 +5923,13 @@ TEST_CASE("an unlink's answer settles nothing while the next victim's STAT is ou /* An answer that says "not there", as a STAT's would: if it settled the * STAT the second victim would be read out and written. */ - answer_path((uint8_t)STORAGE_PATH_UNLINK, (uint8_t)KODE_OK, false, false); + answer_path((uint8_t)STORAGE_PATH_UNLINK, (uint8_t)KODE_OK, false, false, 0U); TEST_ASSERT_NULL_MESSAGE(storage_wait_for(STORAGE_WRITE_REQ_SIG, 150U), "an unlink's answer was taken for the second victim's STAT"); TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, storage_tally().open, "an unlink's answer ended the round with a STAT still out"); - answer_stat(true, (uint8_t)KODE_OK); /* the round ends properly */ + answer_copy_of(THREE_APP_LEN); /* the round ends properly */ (void)qf_support_pump(AO_AppMgr, 8U); TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, storage_tally().open, "the install did not start again"); @@ -4418,7 +5948,7 @@ TEST_CASE("a folder where the copy would go is a failed copy, not a kept one", uint32_t const len = begin_install((uint8_t)KDRV_MEDIUM_FS, "new"); storage_expect(STORAGE_PATH_REQ_SIG, "the victim's path was never asked about"); - answer_path((uint8_t)STORAGE_PATH_STAT, (uint8_t)KODE_OK, true, true); + answer_path((uint8_t)STORAGE_PATH_STAT, (uint8_t)KODE_OK, true, true, 0U); (void)qf_support_pump(AO_AppMgr, 8U); storage_seen_t const seen = storage_tally(); @@ -4774,7 +6304,7 @@ TEST_CASE("two victims come in the order they would go, one with a copy on " collect_answer(&a); TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, a.entries, "an app was reported before the card said whether it had a copy"); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(THREE_APP_LEN); stat_expect("/sdcard/second.bin", "the second victim's copy was not looked for"); answer_stat(false, (uint8_t)KODE_OK); collect_answer(&a); @@ -4826,13 +6356,15 @@ TEST_CASE("a card that is absent, fails, or holds a folder there reads as no " uint8_t rc; bool exists; bool is_dir; + bool whole; /* the file is as long as the image, when there is one */ bool on_card; } rows[] = { - { (uint8_t)KODE_OK, true, false, true }, /* the file is there */ - { (uint8_t)KODE_OK, false, false, false }, /* nothing there */ - { (uint8_t)KODE_ERROR, false, false, false }, /* no card */ - { (uint8_t)KODE_ERROR, true, false, false }, /* it did not say */ - { (uint8_t)KODE_OK, true, true, false }, /* a folder, no copy */ + { (uint8_t)KODE_OK, true, false, true, true }, /* the file is there, whole */ + { (uint8_t)KODE_OK, true, false, false, false }, /* a file of another length */ + { (uint8_t)KODE_OK, false, false, false, false }, /* nothing there */ + { (uint8_t)KODE_ERROR, false, false, false, false }, /* no card */ + { (uint8_t)KODE_ERROR, true, false, true, false }, /* it did not say */ + { (uint8_t)KODE_OK, true, true, false, false }, /* a folder, no copy */ }; Mockkdrv_slot_Init(); @@ -4846,8 +6378,14 @@ TEST_CASE("a card that is absent, fails, or holds a folder there reads as no " (void)memset(&a, 0, sizeof(a)); ask_evict(0x10000U); stat_expect("/sdcard/old.bin", "the victim's copy was not looked for"); + uint32_t file_len = old_bytes() - KAPPS_ALIGN; /* not the image's length */ + + if ( rows[i].whole ) + { + file_len = old_bytes(); + } answer_path((uint8_t)STORAGE_PATH_STAT, rows[i].rc, rows[i].exists, - rows[i].is_dir); + rows[i].is_dir, file_len); collect_answer(&a); TEST_ASSERT_EQUAL_UINT(1U, a.entries); @@ -4926,7 +6464,14 @@ TEST_CASE("a long list is cut at the cap, and the end counts every victim", (void)snprintf(path, sizeof(path), "/sdcard/a%02u.bin", i); stat_expect(path, "a listed victim's copy was not looked for"); - answer_stat((i % 2U) == 0U, (uint8_t)KODE_OK); + if ( (i % 2U) == 0U ) + { + answer_copy_of(TINY_APP_LEN); + } + else + { + answer_stat(false, (uint8_t)KODE_OK); + } } collect_answer(&a); @@ -4992,7 +6537,7 @@ TEST_CASE("a STAT nobody answers is no copy, and its late answer is not " (void)memset(&a, 0, sizeof(a)); ask_evict(0x10000U); /* idle again: not busy */ stat_expect("/sdcard/old.bin", "the second question never asked the card"); - answer_stat(true, (uint8_t)KODE_OK); /* the first STAT's, late */ + answer_copy_of(old_bytes()); /* the first STAT's, late */ collect_answer(&a); TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, a.entries, "a late answer was taken for the next victim's"); @@ -5075,7 +6620,7 @@ TEST_CASE("a question while apps are being kept on the card is answered busy", storage_expect(STORAGE_PATH_REQ_SIG, "the victim's path was never asked about"); busy_expect("keeping"); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(old_bytes()); run_ticks(50U); (void)storage_tally(); TEST_ASSERT_TRUE_MESSAGE(old_installed(), "the question moved the install on"); @@ -5116,7 +6661,7 @@ TEST_CASE("a second question while the card is being asked is answered busy, " TEST_ASSERT_EQUAL_UINT(1U, a.ends); TEST_ASSERT_TRUE(a.end.busy); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(old_bytes()); collect_answer(&a); TEST_ASSERT_EQUAL_UINT(1U, a.entries); TEST_ASSERT_EQUAL_STRING("old", a.name[0]); @@ -5227,7 +6772,7 @@ TEST_CASE("the late answer to a question's lost STAT is settled where it lands, len = image_named("new"); ask_flash_dst("new.bin", (uint8_t)KFW_TARGET_APP); storage_expect(STORAGE_OPEN_REQ_SIG, "the install never asked for its file"); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(old_bytes()); answer_open(len, (uint8_t)KDRV_MEDIUM_FS); answer_data(len); /* "old" must go */ @@ -5235,7 +6780,7 @@ TEST_CASE("the late answer to a question's lost STAT is settled where it lands, * next: it counts. Eaten as the late one, the round would sit out its * whole deadline. */ stat_expect("/sdcard/old.bin", "the victim's copy was not looked for"); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(old_bytes()); run_ticks(50U); TEST_ASSERT_TRUE_MESSAGE(klog_has("'old' is already on the card"), "keeping's answer was taken for the question's late one"); @@ -5309,7 +6854,7 @@ TEST_CASE("once the card lets a STAT go, the victims still to report are not " answer_stat(false, (uint8_t)KODE_OK); /* the late one */ collect_answer(&a); TEST_ASSERT_EQUAL_UINT(0U, a.entries); - answer_stat(true, (uint8_t)KODE_OK); /* first's own */ + answer_copy_of(THREE_APP_LEN); /* first's own */ stat_expect("/sdcard/second.bin", "the second victim's copy was not looked for"); answer_stat(false, (uint8_t)KODE_OK); collect_answer(&a); @@ -5404,7 +6949,7 @@ TEST_CASE("the install's own app is left out of a list of several, and the " ask_evict_as(5U * 0x100000U, "first"); /* first, second go for bytes */ stat_expect("/sdcard/second.bin", "the other victim's copy was not looked for"); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(THREE_APP_LEN); collect_answer(&a); TEST_ASSERT_EQUAL_UINT(1U, a.entries); @@ -5718,11 +7263,14 @@ static void install_container(uint32_t len, bool launch) /** @brief The bytes the stand-in inflater produces: the image of the deflated container. */ #define TIDY_ZIP_IMG 304U +/** @brief The image the deflated container inflates to: what the stand-in inflater is told to write. */ +static uint8_t s_zip_img[TIDY_ZIP_IMG]; + /** * @brief Build in s_cont a deflated container for an app, with no block. * * The host has no inflater (fake_tinfl.c): the stand-in turns any payload into - * TIDY_ZIP_IMG bytes of its fill byte, so the container's sha256 is that of + * the bytes it is given, s_zip_img, so the container's sha256 is that of * such an image. What matters is that the payload is shorter than the image * it becomes, so the wire and the image part ways the other way round from a * container with a block. @@ -5731,10 +7279,11 @@ static void install_container(uint32_t len, bool launch) */ static uint32_t container_deflated(void) { - static uint8_t img[TIDY_ZIP_IMG]; - kfw_hdr_t h; + uint8_t * const img = s_zip_img; + kfw_hdr_t h; - (void)memset(img, 'Z', sizeof(img)); /* fake_tinfl's default fill */ + (void)memset(img, 'Z', sizeof(s_zip_img)); + image_chain(img); /* an image as far as its chain, with no descriptor */ (void)memset(s_cont, 0xA5, sizeof(s_cont)); (void)memset(&h, 0, sizeof(h)); h.magic = KFW_MAGIC; @@ -5744,7 +7293,7 @@ static uint32_t container_deflated(void) h.comp = (uint8_t)KFW_COMP_DEFLATE; h.payload_len = TIDY_ZIP_LEN; h.image_len = TIDY_ZIP_IMG; - kfw_sha256(img, sizeof(img), h.sha256); + kfw_sha256(img, sizeof(s_zip_img), h.sha256); h.crc = kfw_crc32(&h, offsetof(kfw_hdr_t, crc)); (void)memcpy(s_cont, &h, sizeof(h)); return (uint32_t)KFW_HDR_LEN + TIDY_ZIP_LEN; @@ -6562,7 +8111,7 @@ TEST_CASE("an evicted app kept on the card keeps its folder", answer_open(len, (uint8_t)KDRV_MEDIUM_FS); answer_data_bytes(s_cont, len); storage_expect(STORAGE_PATH_REQ_SIG, "the victim's path was never asked about"); - answer_stat(true, (uint8_t)KODE_OK); /* already on the card */ + answer_copy_of(old_bytes()); /* already on the card */ (void)qf_support_pump(AO_AppMgr, 8U); storage_expect(STORAGE_OPEN_REQ_SIG, "the install did not start again"); @@ -7091,6 +8640,7 @@ TEST_CASE("a compressed container streams in: the wire is shorter than the image fake_tinfl_reset(); fake_tinfl_set((int)TINFL_STATUS_DONE, 4096U, TIDY_ZIP_IMG); uint32_t const len = container_deflated(); + fake_tinfl_set_payload(s_zip_img, sizeof(s_zip_img)); /* The header in one event, the payload in the next: the inflater runs once. */ install_container_in(len, false, (uint32_t)KFW_HDR_LEN); @@ -7224,7 +8774,7 @@ TEST_CASE("test_find_missing_asks_the_card", "[appmgr_ao][find]") stat_expect("/sdcard/welcome.bin", "the card was not asked for the missing app"); TEST_ASSERT_NULL_MESSAGE(ao_spy_take(), "answered before the card did"); - answer_stat(true, (uint8_t)KODE_OK); + answer_copy_of(KAPPS_ALIGN); /* a file of the name, whatever its length */ AppFindRspEvt const rsp = find_answer(); TEST_ASSERT_FALSE(rsp.busy); TEST_ASSERT_FALSE(rsp.installed); @@ -8300,3 +9850,35 @@ TEST_CASE("the fetch's request refuses what it cannot post", ao_spy_drain(); fetch_end(); } + +TEST_CASE("a fetch of an app's data folder shows from its first moment and ends the card", + "[appmgr_ao][beat]") +{ + fetch_fresh(); + ao_spy_subscribe(APP_FLASH_PROGRESS_SIG); + ao_spy_subscribe(APP_FLASH_FAIL_SIG); + + ask_get("snake", GET_URL); + (void)net_seen(); + progress_seen_t seen = spy_progress(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.count, + "a fetch raises nothing on the glass until it is over"); + TEST_ASSERT_EQUAL_UINT(0U, seen.pct[0]); + TEST_ASSERT_EQUAL_STRING("snake", seen.first.project); + + run_ticks((2U * APPMGR_WAIT_BEAT_TICKS) + 10U); + seen = spy_progress(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(2U, seen.count, + "a fetch waiting on the server fell silent"); + + fetch_opened((uint8_t)KODE_ERROR, 0U); /* the server says no */ + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, spy_count(APP_FLASH_FAIL_SIG), + "the card the fetch raised is left up"); + run_ticks(3U * APPMGR_WAIT_BEAT_TICKS); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, spy_progress().count, + "the beat went on after the fetch ended"); + + QActive_unsubscribe(ao_spy(), APP_FLASH_PROGRESS_SIG); + QActive_unsubscribe(ao_spy(), APP_FLASH_FAIL_SIG); + fetch_end(); +} diff --git a/tests/host/cases/test_console_apps.c b/tests/host/cases/test_console_apps.c index 7df3938..53292c0 100644 --- a/tests/host/cases/test_console_apps.c +++ b/tests/host/cases/test_console_apps.c @@ -266,6 +266,91 @@ TEST_CASE("nvs_del with a key too long for the store is refused", "[console_apps console_end(); } +/* ---- app_run ------------------------------------------------------------------ */ + +/** @brief Hand the console AO_AppMgr's verdict on a request, as it posts it. */ +static void verdict(app_req_verdict_t v) +{ + AppReqRspEvt e; + + (void)memset(&e, 0, sizeof(e)); + e.super = (QEvt)QEVT_INITIALIZER(APP_REQ_RSP_SIG); + e.verdict = (uint8_t)v; + QASM_DISPATCH(AO_Console, &e.super, 0U); +} + +/** @brief The one request of kind @p sig AO_AppMgr's queue holds, or NULL; the caller frees. */ +static const QEvt *appmgr_took(enum_t sig) +{ + const QEvt *e = NULL; + + if ( (xQueueReceive(AO_AppMgr->eQueue, &e, 0U) == pdTRUE) && ((enum_t)e->sig != sig) ) + { + QF_gc(e); + e = NULL; + } + return e; +} + +/* `>ok` on an `app_run` says AO_AppMgr has the app on its way to boot. A name + * nobody installed, or an AO_AppMgr with work in hand, is told to the person + * who typed it. */ +TEST_CASE("app_run waits for AO_AppMgr's verdict on the installed app it names", + "[console_apps]") +{ + console_up(); + ask("app_run name=\"scope\""); + + const AppFlashEvt * const req = (const AppFlashEvt *)appmgr_took(APP_RUN_REQ_SIG); + TEST_ASSERT_NOT_NULL(req); + TEST_ASSERT_EQUAL_STRING("scope", req->file); + TEST_ASSERT_TRUE_MESSAGE(AO_Console == req->who, + "the request does not say who is waiting for the verdict"); + QF_gc(&req->super); + TEST_ASSERT_FALSE_MESSAGE(replied(">ok"), "the line answered before the verdict"); + + verdict(APP_REQ_TAKEN); + TEST_ASSERT_TRUE(replied(">ok")); + console_end(); +} + +TEST_CASE("app_run of an app that is not installed, or while AO_AppMgr is busy, is an error", + "[console_apps]") +{ + console_up(); + ask("app_run name=\"ghost\""); + appmgr_queue_drained(); + verdict(APP_REQ_MISSING); + TEST_ASSERT_TRUE(replied(">err code=5 msg=missing")); + TEST_ASSERT_FALSE(replied(">ok")); + + ask("app_run name=\"scope\""); + appmgr_queue_drained(); + verdict(APP_REQ_BUSY); + TEST_ASSERT_TRUE(replied(">err code=4 msg=busy")); + TEST_ASSERT_FALSE(replied(">ok")); + console_end(); +} + +TEST_CASE("app_run of a .bin asks for it to be installed and booted, and waits the same way", + "[console_apps]") +{ + console_up(); + ask("app_run name=\"scope.bin\""); + + const FlashReqEvt * const req = (const FlashReqEvt *)appmgr_took(FLASH_REQ_SIG); + TEST_ASSERT_NOT_NULL(req); + TEST_ASSERT_EQUAL_STRING("scope.bin", req->src); + TEST_ASSERT_TRUE(req->launch); + TEST_ASSERT_TRUE(AO_Console == req->who); + QF_gc(&req->super); + TEST_ASSERT_FALSE(replied(">ok")); + + verdict(APP_REQ_MISSING); + TEST_ASSERT_TRUE(replied(">err code=5 msg=missing")); + console_end(); +} + /* ---- app_erase --------------------------------------------------------------- */ /** diff --git a/tests/host/cases/test_console_install.c b/tests/host/cases/test_console_install.c index 0cd77d3..75592a2 100644 --- a/tests/host/cases/test_console_install.c +++ b/tests/host/cases/test_console_install.c @@ -363,31 +363,120 @@ TEST_CASE("size and crc32 belong to the terminal alone", "[console_install]") TEST_ASSERT_NULL(take(AO_AppMgr, FLASH_REQ_SIG)); } -TEST_CASE("install off a medium accepts at once and hands AO_AppMgr the " - "source, the destination and run", "[console_install]") +/** @brief Hand the console AO_AppMgr's verdict, as AO_AppMgr posts it. */ +static void verdict(app_req_verdict_t v) { - const FlashReqEvt *req; + AppReqRspEvt e; + (void)memset(&e, 0, sizeof(e)); + e.super = (QEvt)QEVT_INITIALIZER(APP_REQ_RSP_SIG); + e.verdict = (uint8_t)v; + QASM_DISPATCH(AO_Console, &e.super, 0U); +} + +/** @brief Type an install off a medium and take its request out of AO_AppMgr's queue. */ +static const FlashReqEvt *install_asked(const char *line) +{ console_up(); - type_line("install src=\"sd:app.bin\" dst=app"); - TEST_ASSERT_TRUE_MESSAGE(replied(">ok"), - "off a medium the >ok is an accept"); - req = (const FlashReqEvt *)take(AO_AppMgr, FLASH_REQ_SIG); + type_line(line); + return (const FlashReqEvt *)take(AO_AppMgr, FLASH_REQ_SIG); +} + +/* The `>ok` of an install off a medium says AO_AppMgr took it. Posting the + * request says nothing of the kind: it may be busy, or the file may not be + * there. The console holds the line until AO_AppMgr's verdict. */ +TEST_CASE("install off a medium waits for AO_AppMgr's verdict and hands it the " + "source, the destination, run and who asked", "[console_install]") +{ + const FlashReqEvt *req = install_asked("install src=\"sd:app.bin\" dst=app"); + TEST_ASSERT_NOT_NULL(req); TEST_ASSERT_EQUAL_UINT8((uint8_t)KFW_SRC_SD, req->src_kind); TEST_ASSERT_EQUAL_STRING("app.bin", req->src); TEST_ASSERT_EQUAL_UINT8((uint8_t)KFW_TARGET_APP, req->dst_kind); - TEST_ASSERT_FALSE_MESSAGE(req->launch, - "run defaults off, as from the wire"); + TEST_ASSERT_FALSE_MESSAGE(req->launch, "run defaults off, as from the wire"); + TEST_ASSERT_TRUE_MESSAGE(AO_Console == req->who, + "the request does not say who is waiting for the verdict"); QF_gc(&req->super); + TEST_ASSERT_FALSE_MESSAGE(replied(">ok"), + "the line answered before AO_AppMgr said whether it took the install"); + TEST_ASSERT_FALSE(replied(">err")); + + verdict(APP_REQ_TAKEN); + TEST_ASSERT_TRUE(replied(">ok")); fake_klog_reset(); - type_line("install src=\"https://example.com/app.bin\" dst=app run=on"); - req = (const FlashReqEvt *)take(AO_AppMgr, FLASH_REQ_SIG); + req = install_asked("install src=\"https://example.com/app.bin\" dst=app run=on"); TEST_ASSERT_NOT_NULL(req); TEST_ASSERT_EQUAL_UINT8((uint8_t)KFW_SRC_HTTP, req->src_kind); TEST_ASSERT_TRUE(req->launch); QF_gc(&req->super); + verdict(APP_REQ_TAKEN); + TEST_ASSERT_TRUE(replied(">ok")); +} + +TEST_CASE("install off a medium says why AO_AppMgr did not take it", + "[console_install]") +{ + static const struct { + const char *line; + app_req_verdict_t verdict; + const char *reply; + } cases[] = { + { "install src=\"sd:app.bin\" dst=app", APP_REQ_BUSY, ">err code=4 msg=busy" }, + { "install src=\"sd:app.bin\" dst=app", APP_REQ_MISSING, ">err code=5 msg=missing" }, + { "install src=\"fs:app.bin\" dst=os", APP_REQ_MISSING, ">err code=5 msg=missing" }, + { "install src=\"pending\" dst=app", APP_REQ_EMPTY, ">err code=5 msg=empty" }, + { "install src=\"sd:app.bin\" dst=app", APP_REQ_REFUSED, ">err code=5 msg=refused" }, + }; + + for ( unsigned i = 0U; (sizeof(cases) / sizeof(cases[0])) > i; ++i ) + { + const FlashReqEvt * const req = install_asked(cases[i].line); + + TEST_ASSERT_NOT_NULL_MESSAGE(req, cases[i].line); + QF_gc(&req->super); + verdict(cases[i].verdict); + TEST_ASSERT_TRUE_MESSAGE(replied(cases[i].reply), cases[i].reply); + TEST_ASSERT_FALSE_MESSAGE(replied(">ok"), cases[i].reply); + } + + /* The command is closed: another one is not met with busy. */ + fake_klog_reset(); + type_line("nosuchverb"); + TEST_ASSERT_TRUE(replied(">err code=1")); +} + +TEST_CASE("an install that waits holds the console, and a verdict nobody waits " + "for says nothing", "[console_install]") +{ + const FlashReqEvt *req = install_asked("install src=\"sd:app.bin\" dst=app"); + + TEST_ASSERT_NOT_NULL(req); + QF_gc(&req->super); + fake_klog_reset(); + type_line("ping"); + TEST_ASSERT_TRUE_MESSAGE(replied(">err code=4"), + "a second command ran while the first waited for its verdict"); + + verdict(APP_REQ_TAKEN); + fake_klog_reset(); + verdict(APP_REQ_TAKEN); + TEST_ASSERT_FALSE_MESSAGE(replied(">ok"), "a late verdict answered a command nobody typed"); +} + +/** @brief The console's wait for a verdict, in QP ticks, past which it gives up. */ +#define VERDICT_WAIT_TICKS (((KODE_APPMGR_OPEN_MS + 5000U) / 10U) + 1U) + +TEST_CASE("an install whose verdict never comes ends with the verb named, not " + "in silence", "[console_install]") +{ + const FlashReqEvt *req = install_asked("install src=\"sd:app.bin\" dst=app"); + + TEST_ASSERT_NOT_NULL(req); + QF_gc(&req->super); + (void)qf_support_tick(AO_Console, VERDICT_WAIT_TICKS); + TEST_ASSERT_TRUE(replied(">err code=6 msg=install")); } TEST_CASE("the old verbs are gone", "[console_install]") diff --git a/tests/host/cases/test_kdrv_apps_region.c b/tests/host/cases/test_kdrv_apps_region.c index 5e6b60b..2163ca2 100644 --- a/tests/host/cases/test_kdrv_apps_region.c +++ b/tests/host/cases/test_kdrv_apps_region.c @@ -16,11 +16,13 @@ #include "esp_private/partition_linux.h" #include "esp_rom_crc.h" +#include "fakes.h" #include "kapp_desc.h" #include "kapps_fmt.h" #include "kdrv_apps.h" #include "kdrv_bounce.h" #include "kdrv_kfw.h" +#include "khal_crc32.h" /** @brief The region, found the same way the driver finds it. */ static const esp_partition_t *region(void) @@ -794,6 +796,103 @@ TEST_CASE("erasing follows the write across a block boundary, never behind it", TEST_ASSERT_EQUAL_MEMORY(out, in, sizeof(out)); } +/* What an install may take back after it fails is decided by which blocks the + * write got to erase, so the driver says it: an extent is reached when any + * block of the session's run has been erased over it, and never before the + * data gets there. */ +TEST_CASE("an extent is reached once the write erases a block of it, and not before", + "[apps_region]") +{ + static uint8_t out[0x40]; + uint32_t const at = KAPPS_DATA_OFF + (2U * KAPPS_ALIGN); + + apps_ready(); + kapps_dir_t same = *kdrv_apps_dir(); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&same)); /* starts the record over */ + (void)memset(out, 0x5AU, sizeof(out)); + + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write_begin(at, 3U * KAPPS_ALIGN)); + TEST_ASSERT_FALSE_MESSAGE(kdrv_apps_write_reached(at, KAPPS_ALIGN), + "an extent under a session that has written nothing is reached"); + + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write(out, sizeof(out))); + TEST_ASSERT_TRUE_MESSAGE(kdrv_apps_write_reached(at, KAPPS_ALIGN), + "the block the first bytes landed in is not reached"); + TEST_ASSERT_TRUE_MESSAGE(kdrv_apps_write_reached(at + 0x10U, 1U), + "a one-byte extent inside the erased block is not reached"); + TEST_ASSERT_TRUE_MESSAGE(kdrv_apps_write_reached(at - KAPPS_ALIGN, 2U * KAPPS_ALIGN), + "an extent that runs into the erased block is not reached"); + TEST_ASSERT_FALSE_MESSAGE(kdrv_apps_write_reached(at - KAPPS_ALIGN, KAPPS_ALIGN), + "the extent just below the session is reached"); + TEST_ASSERT_FALSE_MESSAGE(kdrv_apps_write_reached(at + KAPPS_ALIGN, KAPPS_ALIGN), + "a block the write has not got to is reached"); + TEST_ASSERT_FALSE_MESSAGE(kdrv_apps_write_reached(at + (2U * KAPPS_ALIGN), 4U), + "the last block of the session is reached before any byte lands in it"); + TEST_ASSERT_FALSE_MESSAGE(kdrv_apps_write_reached(at, 0U), + "an extent of no bytes is reached"); + kdrv_apps_write_abort(); + + TEST_ASSERT_TRUE_MESSAGE(kdrv_apps_write_reached(at, KAPPS_ALIGN), + "an abandoned session forgot what it had erased"); +} + +TEST_CASE("the erases of the last session are not the next directory's", + "[apps_region]") +{ + static uint8_t out[0x40]; + uint32_t const at = KAPPS_DATA_OFF; + + apps_ready(); + (void)memset(out, 0x5AU, sizeof(out)); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write_begin(at, KAPPS_ALIGN)); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write(out, sizeof(out))); + kdrv_apps_write_abort(); + TEST_ASSERT_TRUE(kdrv_apps_write_reached(at, 1U)); + + kapps_dir_t same = *kdrv_apps_dir(); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&same)); + TEST_ASSERT_FALSE_MESSAGE(kdrv_apps_write_reached(at, 1U), + "a committed directory kept the erases made before it"); + + /* A commit that fails changes nothing: the record stands. */ + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write_begin(at, KAPPS_ALIGN)); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write(out, sizeof(out))); + kdrv_apps_write_abort(); + esp_partition_fail_after(0U, ESP_PARTITION_FAIL_AFTER_MODE_BOTH); + TEST_ASSERT_NOT_EQUAL(KODE_OK, kdrv_apps_dir_commit(&same)); + cut_off(); + TEST_ASSERT_TRUE_MESSAGE(kdrv_apps_write_reached(at, 1U), + "a commit that failed wiped the record"); + + /* A new session starts its own record, and says nothing of the old one. */ + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write_begin(at + KAPPS_ALIGN, KAPPS_ALIGN)); + TEST_ASSERT_FALSE_MESSAGE(kdrv_apps_write_reached(at, 1U), + "a session that has erased nothing inherited the last one's"); + kdrv_apps_write_abort(); +} + +TEST_CASE("an erase that fails counts as reaching its block", + "[apps_region]") +{ + static uint8_t out[0x40]; + uint32_t const at = KAPPS_DATA_OFF + KAPPS_ALIGN; + + apps_ready(); + kapps_dir_t same = *kdrv_apps_dir(); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_dir_commit(&same)); + (void)memset(out, 0x5AU, sizeof(out)); + + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write_begin(at, 2U * KAPPS_ALIGN)); + esp_partition_fail_after(0U, ESP_PARTITION_FAIL_AFTER_MODE_BOTH); + TEST_ASSERT_NOT_EQUAL(KODE_OK, kdrv_apps_write(out, sizeof(out))); + cut_off(); + kdrv_apps_write_abort(); + + TEST_ASSERT_TRUE_MESSAGE(kdrv_apps_write_reached(at, KAPPS_ALIGN), + "a block a failed erase touched is called untouched"); + TEST_ASSERT_FALSE(kdrv_apps_write_reached(at + KAPPS_ALIGN, KAPPS_ALIGN)); +} + /** @brief A lead-in that leaves the next write starting mid-session. */ #define LEAD_LEN 0x37U /** @brief One write two bounces and a part long, crossing every boundary. */ @@ -808,6 +907,178 @@ static void fill_pattern(uint8_t *buf, size_t len) } } +/* ---- The CRC of an extent -------------------------------------------------- */ + +/** @brief The extent the CRC cases fold: odd on purpose, so a read cannot end on a chunk. */ +#define EXTENT_LEN ((3U * 4096U) + 7U) + +/** @brief What lies right after the extent: a pattern no extent byte matches. */ +#define GUARD_LEN 64U + +/* A digest is what can be trusted over data that is not known to be an image: + * it reads the bytes it is given and nothing else. The guard after the + * extent is the proof: a read that ran on would fold it in. */ +TEST_CASE("an extent's CRC folds exactly its bytes and never the ones after it", + "[apps_region]") +{ + static uint8_t data[EXTENT_LEN + GUARD_LEN]; + uint32_t crc = KHAL_CRC32_INIT; + + apps_ready(); + fill_pattern(data, EXTENT_LEN); + (void)memset(&data[EXTENT_LEN], 0xEEU, GUARD_LEN); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write_begin(KAPPS_DATA_OFF, sizeof(data))); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write(data, sizeof(data))); + kdrv_apps_write_abort(); + + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_extent_crc(KAPPS_DATA_OFF, EXTENT_LEN, &crc)); + TEST_ASSERT_EQUAL_HEX32_MESSAGE(khal_crc32(KHAL_CRC32_INIT, data, EXTENT_LEN), crc, + "the CRC is not that of the extent's bytes"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(khal_crc32(KHAL_CRC32_INIT, data, sizeof(data)), crc, + "the guard after the extent was folded in"); + + /* In parts it is the same CRC, and a seed continues one. */ + uint32_t part = KHAL_CRC32_INIT; + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_extent_crc(KAPPS_DATA_OFF, 5000U, &part)); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_extent_crc(KAPPS_DATA_OFF + 5000U, + EXTENT_LEN - 5000U, &part)); + TEST_ASSERT_EQUAL_HEX32_MESSAGE(crc, part, "an extent taken in two parts differs"); +} + +TEST_CASE("an extent that leaves the data area is refused and leaves the CRC alone", + "[apps_region]") +{ + uint32_t crc = 0xDEADBEEFU; + + apps_ready(); + uint32_t const end = kdrv_apps_region_size(); + + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, + kdrv_apps_extent_crc(KAPPS_DATA_OFF - 1U, 16U, &crc), "below the data area"); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, + kdrv_apps_extent_crc(0U, 16U, &crc), "the directory is not an extent"); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, + kdrv_apps_extent_crc(end - 15U, 16U, &crc), "one byte past the region"); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, + kdrv_apps_extent_crc(KAPPS_DATA_OFF, end, &crc), "longer than the region"); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, + kdrv_apps_extent_crc(UINT32_MAX - 7U, 16U, &crc), "an extent that wraps"); + TEST_ASSERT_EQUAL_HEX32_MESSAGE(0xDEADBEEFU, crc, "a refusal changed the CRC"); + + TEST_ASSERT_EQUAL_INT(KODE_DENIED, kdrv_apps_extent_crc(KAPPS_DATA_OFF, 16U, NULL)); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_OK, kdrv_apps_extent_crc(KAPPS_DATA_OFF, 0U, &crc), + "an extent of no bytes is refused"); + TEST_ASSERT_EQUAL_HEX32(0xDEADBEEFU, crc); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_OK, kdrv_apps_extent_crc(end - 16U, 16U, &crc), + "the last 16 bytes of the region are an extent"); +} + +/* ---- An image that claims more than it holds ------------------------------- */ + +/** @brief Bytes the image of these cases is given in the region. */ +#define CLAIM_IMG_LEN 4096U + +/** + * @brief Write the head of an image at the first data block and close the session unverified. + * @param segments What the image header claims. + * @param data_len What its first segment header claims. + */ +static void claim_image(uint8_t segments, uint32_t data_len) +{ + static uint8_t img[CLAIM_IMG_LEN]; + esp_image_header_t h; + esp_image_segment_header_t seg; + + (void)memset(img, 0, sizeof(img)); + (void)memset(&h, 0, sizeof(h)); + h.magic = ESP_IMAGE_HEADER_MAGIC; + h.segment_count = segments; + h.hash_appended = 0U; + (void)memcpy(&img[0], &h, sizeof(h)); + (void)memset(&seg, 0, sizeof(seg)); + seg.load_addr = 0x40000000U; + seg.data_len = data_len; + (void)memcpy(&img[sizeof(h)], &seg, sizeof(seg)); + + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write_begin(KAPPS_DATA_OFF, sizeof(img))); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write(img, sizeof(img))); + kdrv_apps_write_abort(); +} + +/* The verifier hashes every segment by the length its header declares and + * compares the total with the partition only afterwards, so a header that + * declares more than the image holds keeps it reading flash for as long as the + * lengths say. The segment chain is walked first, by its own bytes alone, and + * an image whose chain leaves them never reaches the verifier: the fake one + * here says yes to everything, so a refusal is the walk's. */ +TEST_CASE("an image whose segments leave its own bytes is refused unhashed", + "[apps_region]") +{ + apps_ready(); + + claim_image(1U, 0x7FFFFFF0U); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, + kdrv_apps_verify(KAPPS_DATA_OFF, CLAIM_IMG_LEN, NULL), "a segment of 2 GB"); + + claim_image(1U, 0xFFFFFFFCU); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, + kdrv_apps_verify(KAPPS_DATA_OFF, CLAIM_IMG_LEN, NULL), "a length that wraps"); + + claim_image(1U, CLAIM_IMG_LEN); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, + kdrv_apps_verify(KAPPS_DATA_OFF, CLAIM_IMG_LEN, NULL), + "a segment as long as the whole image, after its headers"); + + claim_image(0U, 16U); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, + kdrv_apps_verify(KAPPS_DATA_OFF, CLAIM_IMG_LEN, NULL), "no segments"); + + claim_image((uint8_t)(ESP_IMAGE_MAX_SEGMENTS + 1U), 16U); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, + kdrv_apps_verify(KAPPS_DATA_OFF, CLAIM_IMG_LEN, NULL), "more segments than an image has"); +} + +/* The walk is a length check and nothing more: an image whose chain holds is + * still the verifier's to judge. */ +TEST_CASE("an image whose segments fit is the verifier's to judge", "[apps_region]") +{ + uint32_t len = 0U; + + apps_ready(); + claim_image(1U, 256U); + + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_verify(KAPPS_DATA_OFF, CLAIM_IMG_LEN, &len)); + + fake_image_verify_set_result(ESP_FAIL); + TEST_ASSERT_EQUAL_INT(KODE_ERROR, kdrv_apps_verify(KAPPS_DATA_OFF, CLAIM_IMG_LEN, NULL)); + fake_image_verify_set_result(ESP_OK); +} + +/* An install ends with the same check: the session's own close refuses what + * the walk refuses. */ +TEST_CASE("a write session does not close on an image that claims more than it holds", + "[apps_region]") +{ + static uint8_t img[CLAIM_IMG_LEN]; + esp_image_header_t h; + esp_image_segment_header_t seg; + + apps_ready(); + (void)memset(img, 0, sizeof(img)); + (void)memset(&h, 0, sizeof(h)); + h.magic = ESP_IMAGE_HEADER_MAGIC; + h.segment_count = 1U; + (void)memcpy(&img[0], &h, sizeof(h)); + (void)memset(&seg, 0, sizeof(seg)); + seg.load_addr = 0x40000000U; + seg.data_len = 0x00800000U; + (void)memcpy(&img[sizeof(h)], &seg, sizeof(seg)); + + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write_begin(KAPPS_DATA_OFF, sizeof(img))); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_apps_write(img, sizeof(img))); + TEST_ASSERT_EQUAL_INT(KODE_ERROR, kdrv_apps_write_end()); +} + /* A write longer than a bounce reaches the flash in pieces, and each piece * must land where it belongs: after whatever the session already wrote, and * after the pieces before it in the same write. */ diff --git a/tests/host/cases/test_kdrv_kfw_decode.c b/tests/host/cases/test_kdrv_kfw_decode.c index 10e9003..0264cec 100644 --- a/tests/host/cases/test_kdrv_kfw_decode.c +++ b/tests/host/cases/test_kdrv_kfw_decode.c @@ -145,6 +145,37 @@ static size_t build_stream(uint8_t *out, const uint8_t *body, uint32_t len, /* ---- Telling a container from a raw image ---------------------------------- */ +/** + * @brief Say how long a raw image's first segment is, in its segment header. + * @param raw The image, already filled. + * @param seg The length the segment header declares. + */ +static void raw_seg_len(uint8_t *raw, uint32_t seg) +{ + esp_image_segment_header_t h; + + (void)memset(&h, 0, sizeof(h)); + h.load_addr = 0x40000000U; + h.data_len = seg; + (void)memcpy(&raw[sizeof(esp_image_header_t)], &h, sizeof(h)); +} + +/** + * @brief Give a stand-in raw image a first segment that fits it. + * + * The decoder refuses a raw image whose first segment is longer than the + * source, so a stand-in that is filler from end to end has to say a length + * that fits: here, everything behind the two headers. + * + * @param raw The image, already filled. + * @param len Its length, at least the two headers. + */ +static void raw_first_seg(uint8_t *raw, size_t len) +{ + raw_seg_len(raw, (uint32_t)(len - (sizeof(esp_image_header_t) + + sizeof(esp_image_segment_header_t)))); +} + /* The first byte settles it. An ESP image starts 0xE9 and has no header to * read, so the decoder must not sit waiting for 72 bytes that will never * describe anything. */ @@ -157,6 +188,7 @@ TEST_CASE("a raw ESP image is recognised from its first byte", "[kfw_decode]") raw[0] = 0xE9U; raw[12] = (uint8_t)ESP_CHIP_ID_ESP32P4; /* chip_id, little-endian */ raw[13] = 0U; + raw_first_seg(raw, sizeof(raw)); TEST_ASSERT_EQUAL_INT(KODE_OK, kfw_begin(&RECORDER, NULL, KFW_TARGET_APP, sizeof(raw), NULL, NULL)); @@ -189,6 +221,7 @@ TEST_CASE("a raw image built for another chip is refused before it is placed", raw[0] = 0xE9U; raw[12] = (uint8_t)ESP_CHIP_ID_ESP32C5; raw[13] = 0U; + raw_first_seg(raw, sizeof(raw)); TEST_ASSERT_EQUAL_INT(KODE_OK, kfw_begin(&RECORDER, NULL, KFW_TARGET_APP, sizeof(raw), NULL, NULL)); @@ -205,6 +238,60 @@ TEST_CASE("a raw image built for another chip is refused before it is placed", TEST_ASSERT_EQUAL_INT(KODE_OK, kfw_end()); } +/* A file cut short is refused from its header too, when the cut falls inside + * the first segment: the header says how long that segment is, and the source + * says how long the file is. The verify at the end would say the same, after + * the apps the image displaced were gone. */ +/** @brief Bytes of the two headers a raw image starts with: its own and its first segment's. */ +#define RAW_HEADS_LEN ((uint32_t)(sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t))) + +TEST_CASE("a raw image cut inside its first segment is refused before it is placed", + "[kfw_decode]") +{ + static uint8_t raw[64]; + + sink_fresh(); + (void)memset(raw, 0, sizeof(raw)); + raw[0] = 0xE9U; + raw[1] = 1U; /* one segment */ + raw[12] = (uint8_t)ESP_CHIP_ID_ESP32P4; + raw_seg_len(raw, (uint32_t)sizeof(raw) - RAW_HEADS_LEN + 1U); /* one byte more than the file has */ + + TEST_ASSERT_EQUAL_INT(KODE_OK, + kfw_begin(&RECORDER, NULL, KFW_TARGET_APP, sizeof(raw), NULL, NULL)); + TEST_ASSERT_EQUAL_INT(KODE_ERROR, kfw_feed(raw, sizeof(raw))); + TEST_ASSERT_FALSE(sink_saw(SINK_BEGIN)); + kfw_abort(); + + /* One byte less and the segment fits: the image goes through. */ + sink_fresh(); + raw_seg_len(raw, (uint32_t)sizeof(raw) - RAW_HEADS_LEN); + TEST_ASSERT_EQUAL_INT(KODE_OK, + kfw_begin(&RECORDER, NULL, KFW_TARGET_APP, sizeof(raw), NULL, NULL)); + TEST_ASSERT_EQUAL_INT(KODE_OK, kfw_feed(raw, sizeof(raw))); + TEST_ASSERT_TRUE(sink_saw(SINK_BEGIN)); + TEST_ASSERT_EQUAL_INT(KODE_OK, kfw_end()); + + /* A first chunk that ends before the segment's header does says nothing + * of its length, and is the end's to judge. */ + sink_fresh(); + raw_seg_len(raw, (uint32_t)sizeof(raw) - RAW_HEADS_LEN + 1U); + TEST_ASSERT_EQUAL_INT(KODE_OK, + kfw_begin(&RECORDER, NULL, KFW_TARGET_APP, sizeof(raw), NULL, NULL)); + TEST_ASSERT_EQUAL_INT(KODE_OK, kfw_feed(raw, RAW_HEADS_LEN - 1U)); + TEST_ASSERT_TRUE(sink_saw(SINK_BEGIN)); + kfw_abort(); + + /* A source of no stated length cannot be cut short of it. */ + sink_fresh(); + raw_seg_len(raw, 0x00100000U); /* 1 MB */ + TEST_ASSERT_EQUAL_INT(KODE_OK, + kfw_begin(&RECORDER, NULL, KFW_TARGET_APP, 0U, NULL, NULL)); + TEST_ASSERT_EQUAL_INT(KODE_OK, kfw_feed(raw, sizeof(raw))); + TEST_ASSERT_TRUE(sink_saw(SINK_BEGIN)); + kfw_abort(); +} + /* A raw image carries no digest, so there is nothing to check at the end - * and the decoder must not invent a failure out of that. */ TEST_CASE("a raw image is not held to a digest it does not carry", @@ -925,6 +1012,7 @@ TEST_CASE("a raw image reports raw", "[kfw_decode]") raw[0] = 0xE9U; raw[12] = (uint8_t)ESP_CHIP_ID_ESP32P4; /* chip_id, little-endian */ raw[13] = 0U; + raw_first_seg(raw, sizeof(raw)); TEST_ASSERT_EQUAL_INT(KODE_OK, kfw_begin(&RECORDER, NULL, KFW_TARGET_APP, sizeof(raw), NULL, NULL)); diff --git a/tests/host/cases/test_radio_ao.c b/tests/host/cases/test_radio_ao.c index 97d80c0..589562d 100644 --- a/tests/host/cases/test_radio_ao.c +++ b/tests/host/cases/test_radio_ao.c @@ -22,6 +22,7 @@ #include "Mockesp_event.h" #include "esp_app_desc.h" /* to lay IDF's descriptor in a test image */ +#include "esp_app_format.h" /* and its image and segment headers */ #include "ao_spy.h" #include "ask_support.h" @@ -2651,6 +2652,14 @@ static void make_image(uint16_t chip, const char *project, const char *version) s_img[0] = 0xE9U; /* ESP_IMAGE_HEADER_MAGIC */ s_img[12] = (uint8_t)(chip & 0xFFU); s_img[13] = (uint8_t)(chip >> 8); + /* Its first segment is the descriptor and no more: a length that fits + * the file, which is what the decoder holds a raw image to. */ + esp_image_segment_header_t seg; + + (void)memset(&seg, 0, sizeof(seg)); + seg.load_addr = 0x40000000U; + seg.data_len = (uint32_t)sizeof(d); + (void)memcpy(&s_img[sizeof(esp_image_header_t)], &seg, sizeof(seg)); (void)memset(&d, 0, sizeof(d)); d.magic_word = ESP_APP_DESC_MAGIC_WORD; (void)snprintf(d.project_name, sizeof(d.project_name), "%s", project); diff --git a/tests/host/main/CMakeLists.txt b/tests/host/main/CMakeLists.txt index 4a3c275..dc04ab4 100644 --- a/tests/host/main/CMakeLists.txt +++ b/tests/host/main/CMakeLists.txt @@ -446,10 +446,15 @@ set_source_files_properties( # AO_AppMgr reads a fetched file out of AO_Net's stream buffers, which exist # only once a real AO_Net has opened a transfer. Its one call that reads them # is renamed here, for this source alone, to a function test_appmgr_ao.c -# defines over a buffer of its own. +# defines over a buffer of its own. The same property switches on the BLE +# console: a launch after a terminal install over BLE waits for the console's +# outbound bytes to leave and asks kdrv_ble_console_backlog() - the one call +# into kdrv_ble.h this file makes, answered by the fake. One property, because +# a second set_source_files_properties() replaces the first. set_source_files_properties( "${SDK_DIR}/components/kaos/appmgr/src/appmgr_ao.c" - PROPERTIES COMPILE_DEFINITIONS "net_stream_buf=host_net_stream_buf") + PROPERTIES COMPILE_DEFINITIONS + "net_stream_buf=host_net_stream_buf;CONFIG_KODEOS_BLE_CONSOLE=1") # The standard layer's Active Object is built for the APP role only, and the # project-wide role above is the OS one. This source is the app build, so the diff --git a/tests/host/main/fakes/fake_kdrv_ble.c b/tests/host/main/fakes/fake_kdrv_ble.c index 02ea38b..078a42f 100644 --- a/tests/host/main/fakes/fake_kdrv_ble.c +++ b/tests/host/main/fakes/fake_kdrv_ble.c @@ -485,9 +485,16 @@ bool kdrv_ble_console_attached(void) return s_attached; } +static uint16_t s_console_backlog; + +void fake_ble_console_set_backlog(uint16_t bytes) +{ + s_console_backlog = bytes; +} + uint16_t kdrv_ble_console_backlog(void) { - return 0U; + return s_console_backlog; } uint16_t kdrv_ble_console_retry(void) diff --git a/tests/host/main/fakes/fakes.h b/tests/host/main/fakes/fakes.h index bf3cf6e..6693b42 100644 --- a/tests/host/main/fakes/fakes.h +++ b/tests/host/main/fakes/fakes.h @@ -895,6 +895,11 @@ void fake_ble_mirror_reset(void); /** @brief Whether kdrv_ble_mirror_attached() reports a central subscribed. */ void fake_ble_mirror_set_attached(bool attached); +/** @brief What kdrv_ble_console_backlog() answers: the console's bytes the + * link has not carried yet. 0 until a case says otherwise; a case + * that sets one puts 0 back. */ +void fake_ble_console_set_backlog(uint16_t bytes); + /** @brief Every kdrv_ble_mirror_write() from here answers KODE_DENIED - the * unretryable "nobody is subscribed any more" a dropped central * leaves behind (kdrv_ble.h's own contract). */ diff --git a/tests/host_slot/cases/test_kdrv_slot.c b/tests/host_slot/cases/test_kdrv_slot.c index c0421e2..e1ad502 100644 --- a/tests/host_slot/cases/test_kdrv_slot.c +++ b/tests/host_slot/cases/test_kdrv_slot.c @@ -10,6 +10,7 @@ #include "Mockesp_ota_ops.h" +#include "esp_image_format.h" #include "esp_partition.h" #include "kdrv_slot.h" @@ -64,6 +65,36 @@ static void slot_done(void) Mockesp_ota_ops_Destroy(); } +/** + * @brief Put the head of an image at the start of the writable slot. + * + * esp_ota_write() is a mock here, so nothing a case "writes" reaches the + * emulated flash; what the end of a session reads there is what this lays. + * + * @param sub The writable slot's subtype. + * @param data_len What the image's one segment claims to hold. + */ +static void slot_chain(esp_partition_subtype_t sub, uint32_t data_len) +{ + const esp_partition_t * const p = app_part(sub); + esp_image_header_t h; + esp_image_segment_header_t seg; + + TEST_ASSERT_EQUAL_INT(ESP_OK, esp_partition_erase_range(p, 0U, 0x1000U)); + (void)memset(&h, 0, sizeof(h)); + h.magic = ESP_IMAGE_HEADER_MAGIC; + h.segment_count = 1U; + h.hash_appended = 0U; + TEST_ASSERT_EQUAL_INT(ESP_OK, esp_partition_write(p, 0U, &h, sizeof(h))); + (void)memset(&seg, 0, sizeof(seg)); + seg.load_addr = 0x40000000U; + seg.data_len = data_len; + TEST_ASSERT_EQUAL_INT(ESP_OK, esp_partition_write(p, sizeof(h), &seg, sizeof(seg))); +} + +/** @brief A segment length that fits any slot: the chain of an image that holds together. */ +#define SLOT_SEG_LEN 16U + /* ---- Which slot ------------------------------------------------------------ */ /* The ping-pong in one case. Running from ota_0 means the writable slot is @@ -236,6 +267,7 @@ TEST_CASE("a failed end still consumes the session", "[kdrv_slot]") expect_begin_ok(); TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_slot_begin()); + slot_chain(ESP_PARTITION_SUBTYPE_APP_OTA_1, SLOT_SEG_LEN); esp_ota_end_ExpectAnyArgsAndReturn(ESP_ERR_OTA_VALIDATE_FAILED); TEST_ASSERT_EQUAL_INT(KODE_ERROR, kdrv_slot_end()); @@ -733,6 +765,32 @@ static void expect_desc(esp_app_desc_t *desc, const char *project) esp_ota_get_partition_description_ReturnThruPtr_app_desc(desc); } +/* esp_ota_end() hashes every segment by the length its header declares and + * looks at the partition's size only afterwards, so an image that claims more + * than the slot holds keeps it reading flash for as long as the lengths say. + * The chain is walked first and such an image never reaches esp_ota_end(): + * none is expected here, so a call fails the case. */ +TEST_CASE("an image whose segments leave the slot is refused before it is hashed", + "[kdrv_slot]") +{ + slot_fresh(ESP_PARTITION_SUBTYPE_APP_OTA_0); + + expect_begin_ok(); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_slot_begin()); + slot_chain(ESP_PARTITION_SUBTYPE_APP_OTA_1, 0x7FFFFFF0U); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, kdrv_slot_end(), "a segment of 2 GB"); + + /* The session is over all the same, and a new one opens. */ + TEST_ASSERT_EQUAL_INT(KODE_DENIED, kdrv_slot_end()); + + expect_begin_ok(); + TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_slot_begin()); + slot_chain(ESP_PARTITION_SUBTYPE_APP_OTA_1, 0xFFFFFFFCU); + TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, kdrv_slot_end(), "a length that wraps"); + + slot_done(); +} + /* The slot is where this firmware's own update goes, and the next boot hands * the board to whatever it holds. What esp_ota_end() accepts is any image * built for the chip, an app's included, so the project's name is what tells @@ -748,6 +806,7 @@ TEST_CASE("an image of the running project ends well", "[kdrv_slot]") expect_begin_ok(); TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_slot_begin()); + slot_chain(ESP_PARTITION_SUBTYPE_APP_OTA_1, SLOT_SEG_LEN); esp_ota_end_ExpectAnyArgsAndReturn(ESP_OK); expect_desc(&desc, running.project); TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_slot_end()); @@ -764,6 +823,7 @@ TEST_CASE("an image of another project is refused at the end", "[kdrv_slot]") expect_begin_ok(); TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_slot_begin()); + slot_chain(ESP_PARTITION_SUBTYPE_APP_OTA_1, SLOT_SEG_LEN); esp_ota_end_ExpectAnyArgsAndReturn(ESP_OK); expect_desc(&desc, "buttons_led"); TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, kdrv_slot_end(), @@ -785,6 +845,7 @@ TEST_CASE("an image with no description to read is refused at the end", expect_begin_ok(); TEST_ASSERT_EQUAL_INT(KODE_OK, kdrv_slot_begin()); + slot_chain(ESP_PARTITION_SUBTYPE_APP_OTA_1, SLOT_SEG_LEN); esp_ota_end_ExpectAnyArgsAndReturn(ESP_OK); esp_ota_get_partition_description_ExpectAnyArgsAndReturn(ESP_ERR_NOT_FOUND); TEST_ASSERT_EQUAL_INT(KODE_ERROR, kdrv_slot_end());