diff --git a/components/kaos/appmgr/src/appmgr_ao.c b/components/kaos/appmgr/src/appmgr_ao.c index 0fb421b..81dc9cd 100644 --- a/components/kaos/appmgr/src/appmgr_ao.c +++ b/components/kaos/appmgr/src/appmgr_ao.c @@ -82,7 +82,15 @@ * 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) +#define APPMGR_WAIT_BEAT_MS 1000U +#define APPMGR_WAIT_BEAT_TICKS KQP_TICKS_PER_MS(APPMGR_WAIT_BEAT_MS) +/* How many of those beats a fetch's card waits for the install the fetch + * announced (appmgr_appdata_get_request()'s `install`): the beat that says the + * card again is the one that counts its time. */ +#define APPMGR_HOLD_BEATS (KODE_APPMGR_HOLD_MS / APPMGR_WAIT_BEAT_MS) +_Static_assert(((KODE_APPMGR_HOLD_MS % APPMGR_WAIT_BEAT_MS) == 0U) + && (0U < APPMGR_HOLD_BEATS) && (UINT8_MAX >= APPMGR_HOLD_BEATS), + "a held card's wait is a whole number of beats that fits its count"); /* 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 @@ -556,6 +564,7 @@ typedef struct { * AO_Storage is handed a copy of it for each try. * The base URL is kept in PSRAM too, for the icon's open (rule 8: 200 * bytes of internal RAM held for a fetch that rarely happens). */ + bool get_install; /**< An install follows: the card is its. */ QActive *get_who; /**< Who asked. */ char get_project[KODE_APP_PROJECT_MAX]; /**< Whose folder. */ char *get_url; /**< The base URL, KODE_APPDATA_URL_MAX of @@ -569,6 +578,17 @@ typedef struct { uint8_t get_tries; /**< Times the put was asked again after a * busy refusal. */ bool get_open; /**< A transfer is open with AO_Net. */ + + /* ---- The card a fetch kept up for the install it announced ----------- + * Between `appdata_get install=on` and the install the host sends next, + * the glass would empty and fill again: the fetch's card stays at 0 %, + * said again every second on `beat`, while AppMgr_idle waits. An + * install from a medium or a URL that begins takes the card over + * (hold_release()); anything else that idle turns to, a terminal install + * included, or APPMGR_HOLD_BEATS beats with no install, ends it + * (hold_end()). */ + uint8_t hold_left; /**< Beats the held card still waits; 0 when + * no card is held. */ } AppMgr; static AppMgr l_appMgr; @@ -728,7 +748,7 @@ bool appmgr_keep_only_request(const char *project, QActive *who) } kode_status_t appmgr_appdata_get_request(const char *project, const char *url, - QActive *who) + bool install, QActive *who) { if ( (NULL == project) || (NULL == url) || (NULL == who) || (strlen(url) >= (size_t)KODE_APPDATA_URL_MAX) ) @@ -743,7 +763,8 @@ kode_status_t appmgr_appdata_get_request(const char *project, const char *url, } (void)snprintf(e->project, sizeof(e->project), "%s", project); (void)snprintf(e->url, sizeof(e->url), "%s", url); - e->who = who; + e->who = who; + e->install = install; if ( !QACTIVE_POST_X(AO_AppMgr, &e->super, 0U, NULL) ) { KLOG_W(TAG, "AO_AppMgr's queue is full; the fetch of a data folder " @@ -3314,6 +3335,36 @@ static void beat_say(AppMgr * const me) (void)QTimeEvt_rearm(&me->beat, APPMGR_WAIT_BEAT_TICKS); } +/** + * @brief Let go of the card a fetch kept up, publishing nothing. + * + * What the install that took it over says from here is what the glass shows: + * from the fetch's 0 % to the install's, the card never empties. + * + * @param me The AO. + */ +static void hold_release(AppMgr * const me) +{ + me->hold_left = 0U; + (void)QTimeEvt_disarm(&me->beat); +} + +/** + * @brief End the card a fetch kept up, when it is still up. + * + * The done names no app, as the fetch's own would have: nothing was installed. + * + * @param me The AO. + */ +static void hold_end(AppMgr * const me) +{ + if ( 0U < me->hold_left ) + { + hold_release(me); + QACTIVE_PUBLISH(&s_flash_done_evt.super, &me->super); + } +} + /** * @brief Write one delivered chunk into the region. * @return true while more chunks are wanted. @@ -3762,6 +3813,10 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) bool const to_pending = ((uint8_t)KODE_FLASH_DST_PENDING == req->dst); + /* A terminal install announces itself at its ready, not before: + * it raises its own card there, and one it took over would have + * nobody to end it if the wire were refused. */ + hold_end(me); if ( NULL == me->chunk ) { KLOG_E(TAG, "install failed: no chunk buffer (see the boot)"); @@ -4071,6 +4126,8 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) AppFlashEvt const * const req = Q_EVT_CAST(AppFlashEvt); kapps_dir_t const *d = kdrv_apps_dir(); int const idx = appmgr_find_idx(d, req->file); + + hold_end(me); /* before anything this request publishes */ if ( 0 > idx ) { KLOG_E(TAG, "'%s' is not installed - `app list` knows what is", @@ -4087,10 +4144,15 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) break; } case APP_ERASE_REQ_SIG: { + hold_end(me); /* before anything this request publishes */ status = erase_begin(me, Q_EVT_CAST(AppFlashEvt)); break; } case APPDATA_GET_REQ_SIG: { + /* Another fetch is another app's, and raises its own card: the + * held one ends first, or the new 0 % would reach the glass + * under the old face and the done after it would close it. */ + hold_end(me); status = get_begin(me, Q_EVT_CAST(AppDataGetReqEvt)); break; } @@ -4099,6 +4161,7 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) break; } case APP_KEEP_ONLY_REQ_SIG: { + hold_end(me); /* before anything this request publishes */ status = keep_only_ask(me, e); break; } @@ -4305,11 +4368,60 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) status = Q_TRAN(&AppMgr_adopting); break; } + case Q_ENTRY_SIG: { + if ( 0U < me->hold_left ) + { + /* A fetch that announced an install ended on the way here. */ + (void)QTimeEvt_rearm(&me->beat, APPMGR_WAIT_BEAT_TICKS); + } + status = Q_HANDLED(); + break; + } + case APP_BEAT_SIG: { + /* Only the held card's: a beat queued behind another state's + * last disarm has nothing to say here. Each beat says the card + * again, and the last one of its wait ends it. */ + if ( 1U == me->hold_left ) + { + KLOG_W(TAG, "the install '%s' was fetched for did not come; " + "its card ends", me->project); + hold_end(me); + } + else if ( 1U < me->hold_left ) + { + --me->hold_left; + progress_publish(me, 0U); + (void)QTimeEvt_rearm(&me->beat, APPMGR_WAIT_BEAT_TICKS); + } + else + { + /* No card is held. */ + } + status = Q_HANDLED(); + break; + } + case Q_EXIT_SIG: { + /* Whatever else takes this AO out of idle - a question about the + * card, an adoption - is not the install the card waited for: + * one that was took the card over below. The requests that + * publish on their way in end the hold in their own case. */ + hold_end(me); + status = Q_HANDLED(); + break; + } #if CONFIG_KODE_LOWPOWER case LOWPOWER_ROUND_SIG: { - /* Idle: no image is being written, kept, received or launched. */ + /* Idle: no image is being written, kept, received or launched; + * a card waiting for its install is the one thing in hand. */ + bool const held = (0U < me->hold_left); + const char *why = NULL; + + if ( held ) + { + why = "install announced"; + } lowpower_ready(me->lp_who, Q_EVT_CAST(LowPowerRoundEvt)->round, - false, NULL); + held, why); status = Q_HANDLED(); break; } @@ -4319,6 +4431,20 @@ static QState AppMgr_idle(AppMgr * const me, QEvt const * const e) break; } } + /* The card a fetch kept up is the next install's from a medium or a URL, + * and only one that begins takes it over; one refused before it began + * ends it here, where the refusal is known, rather than at the deadline. */ + if ( (0U < me->hold_left) && ((QSignal)FLASH_REQ_SIG == e->sig) ) + { + if ( Q_RET_TRAN == status ) + { + hold_release(me); + } + else + { + hold_end(me); + } + } return status; } @@ -5745,6 +5871,37 @@ static void get_open_file(AppMgr * const me, const char *file) net_stream_open_request(url, &me->super); } +/** + * @brief End the card a fetch raised, or leave it up for the install it + * announced. + * + * The done names no app, as a park's does: nothing was installed. A fetch + * asked with `install` publishes neither, whatever its end: the folder is + * what the install looks like, never what lets it happen, and the host sends + * the install all the same. AppMgr_idle keeps the card up for it. + * + * @param me The AO. + * @param ok The folder was written. + */ +static void get_card_end(AppMgr * const me, bool ok) +{ + if ( me->get_install ) + { + me->get_install = false; + /* AppMgr_idle's entry arms the beat that counts it down. */ + me->hold_left = (uint8_t)APPMGR_HOLD_BEATS; + KLOG_N(TAG, "the card of '%s' waits for its install", me->get_project); + } + else if ( ok ) + { + QACTIVE_PUBLISH(&s_flash_done_evt.super, &me->super); + } + else + { + QACTIVE_PUBLISH(&s_flash_fail_evt.super, &me->super); + } +} + /** * @brief End a fetch that did not make a folder. * @@ -5761,7 +5918,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 */ + get_card_end(me, false); return Q_TRAN(&AppMgr_idle); } @@ -5986,6 +6143,7 @@ static QState get_begin(AppMgr * const me, AppDataGetReqEvt const * const req) me->get_icon_len = 0U; me->get_tries = 0U; me->get_open = false; + me->get_install = req->install; (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); @@ -6088,16 +6246,7 @@ 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); - } + get_card_end(me, APPDATA_GET_OK == why); status = Q_TRAN(&AppMgr_idle); } } diff --git a/components/kaos/console/src/console_ao.c b/components/kaos/console/src/console_ao.c index 6b24f1f..cb47dc8 100644 --- a/components/kaos/console/src/console_ao.c +++ b/components/kaos/console/src/console_ao.c @@ -73,7 +73,8 @@ KAOS_WEAK void kaos_net_start(void); KAOS_WEAK void appdata_request(const char *project, QActive *who); KAOS_WEAK kode_status_t appdata_rm_request(const char *project, QActive *who); KAOS_WEAK kode_status_t appmgr_appdata_get_request(const char *project, - const char *url, QActive *who); + const char *url, bool install, + QActive *who); KAOS_WEAK const uint8_t *storage_buf(uint8_t idx); KAOS_WEAK void storage_close_request(void); KAOS_WEAK void storage_desc_request(const char *name, QActive *who); @@ -2239,9 +2240,11 @@ static void h_appdata_rm(Console *me, const kcmd_t *c, const karg_val_t *v) * answers every fetch, so this only bounds a lost answer. */ #define KCLI_APPDATA_GET_MS ((2U * KODE_APPMGR_OPEN_MS) + APPMGR_TIDY_MS + KCLI_CMD_TIMEOUT_MS) -/** @brief appdata_get name= url=: fetch the app's data folder - * from `app.json` and `icon.k64`; closed by - * APPDATA_GET_RSP_SIG, or at once when the request is refused. */ +/** @brief appdata_get name= url= [install=on]: fetch the app's + * data folder from `app.json` and `icon.k64`; closed by + * APPDATA_GET_RSP_SIG, or at once when the request is refused. + * `install=on` says an install of the app comes next, and the card the + * fetch raises stays up for it (appmgr_appdata_get_request()). */ static void h_appdata_get(Console *me, const kcmd_t *c, const karg_val_t *v) { if ( console_owner_absent(&kaos_storage_start) ) @@ -2258,7 +2261,9 @@ static void h_appdata_get(Console *me, const kcmd_t *c, const karg_val_t *v) return; } cmd_begin(me, KCLI_APPDATA_GET, c->verb); - kode_status_t const rc = appmgr_appdata_get_request(v[0].s, v[1].s, &me->super); + bool const install = v[2].present && v[2].b; + kode_status_t const rc = appmgr_appdata_get_request(v[0].s, v[1].s, install, + &me->super); if ( KODE_OK != rc ) { /* Nothing was posted, so nobody will answer. */ @@ -6260,9 +6265,12 @@ _Static_assert((sizeof(A_EXP) / sizeof(A_EXP[0])) <= (size_t)KCLI_MAX_KEYS, static const karg_spec_t A_NAME[] = { { .key = "name", .required = true, .type = KARG_STRING }, }; +/* `install=on` when the host sends the app's install next: the fetch's card + * waits for it instead of closing between the two. */ static const karg_spec_t A_APPDATA_GET[] = { - { .key = "name", .required = true, .type = KARG_STRING }, - { .key = "url", .required = true, .type = KARG_STRING }, + { .key = "name", .required = true, .type = KARG_STRING }, + { .key = "url", .required = true, .type = KARG_STRING }, + { .key = "install", .required = false, .type = KARG_BOOL, .def = "off" }, }; /* `ls` may be typed bare (the cwd). `cd` and `stat` need a path, so a * mistyped key is not taken as the cwd. */ @@ -6452,7 +6460,7 @@ static const kcmd_t CMDS[] = { { "appdata_ls", NULL, 0U, h_appdata_ls }, { "appdata_info", A_NAME, 1U, h_appdata_info }, { "appdata_rm", A_NAME, 1U, h_appdata_rm }, - { "appdata_get", A_APPDATA_GET, 2U, h_appdata_get }, + { "appdata_get", A_APPDATA_GET, 3U, h_appdata_get }, { "variable", A_VARIABLE, 2U, h_variable }, { "variable_list", NULL, 0U, h_variable_list }, { "function", A_FUNCTION, 2U, h_function }, diff --git a/components/kaos/core/include/kaos.h b/components/kaos/core/include/kaos.h index e49981c..e2eafdc 100644 --- a/components/kaos/core/include/kaos.h +++ b/components/kaos/core/include/kaos.h @@ -3891,6 +3891,8 @@ typedef struct { * names are added to it. */ QActive *who; /**< Where APPDATA_GET_RSP_SIG is * posted. */ + bool install; /**< An install of the app follows: + * the card stays up for it. */ } AppDataGetReqEvt; /** @brief How a fetch of one app's data folder ended (APPDATA_GET_RSP_SIG). */ @@ -3916,16 +3918,28 @@ typedef struct { * and again every second while it waits on the network, ended by an unnamed * APP_FLASH_DONE_SIG or by APP_FLASH_FAIL_SIG. * + * A fetch asked with @p install is the first step of an install, and its card + * is the install's: whatever the fetch's end, neither event goes out. The card + * stays at 0 %, said again every second, until the next install from a medium + * or a URL takes it over - its own reports carry it from there, so the glass + * never empties between the two - or until AO_AppMgr turns to anything else, + * a refused install and a terminal install included, or KODE_APPMGR_HOLD_MS + * goes by; then the unnamed APP_FLASH_DONE_SIG ends it. A terminal install + * raises its own card at its ready. The reports of the install that took the + * card name no project until its image is read. The answer to the fetch is + * the same either way. + * * @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. + * @param install true when an install of the app follows the fetch. * @param who The Active Object the answer is posted to. Not NULL. * @return KODE_OK when the request is posted and an answer is owed; KODE_ERROR * for a NULL argument or a URL that does not fit; KODE_NO_RESOURCES when * the pool is dry or the queue full. Nothing was posted for the last two. */ kode_status_t appmgr_appdata_get_request(const char *project, const char *url, - QActive *who); + bool install, QActive *who); /* ---- AO_Boot: owner of otadata AND the apps boot request ---------------- * kodeOS lives in `factory` or its OTA slot; apps live in the `apps` region. diff --git a/components/kqp/include/kqp_config.h b/components/kqp/include/kqp_config.h index b4f5b70..446eb8f 100644 --- a/components/kqp/include/kqp_config.h +++ b/components/kqp/include/kqp_config.h @@ -1631,6 +1631,12 @@ _Static_assert(KODE_IR_TEST_BUDGET_MS > (KODE_IR_LINK_WAIT_MS + (KODE_IR_TEST_TR #define KODE_APPMGR_PUT_TRIES 3U /** @brief The pause before each of those tries, in ms. */ #define KODE_APPMGR_PUT_RETRY_MS 500U +/** @brief How long the card of a fetch that announced an install + * (`appdata_get install=on`) waits for that install to begin, in ms: + * a whole number of seconds, counted on the beat that says the card. + * The host sends it the moment the fetch is answered; this only bounds + * a host that never does, so the card does not stand for ever. */ +#define KODE_APPMGR_HOLD_MS 15000U /* ---- The factory reset's budgets ---------------------------------------- * AO_Boot walks the reset one step at a time and arms one timer per step; a diff --git a/components/kqp/include/kqp_signals.h b/components/kqp/include/kqp_signals.h index 31862d4..3931b86 100644 --- a/components/kqp/include/kqp_signals.h +++ b/components/kqp/include/kqp_signals.h @@ -547,7 +547,8 @@ enum KodeSignals { * 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 - + * waiting on the network, or for the + * install a fetch announced, 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 diff --git a/tests/host/cases/test_appmgr_ao.c b/tests/host/cases/test_appmgr_ao.c index d04e95a..0a9f016 100644 --- a/tests/host/cases/test_appmgr_ao.c +++ b/tests/host/cases/test_appmgr_ao.c @@ -10088,8 +10088,11 @@ TEST_CASE("a kodeOS write names no project", "[appmgr_ao][project]") #define GET_URL "http://example.invalid/apps/snake/" -/** @brief Ask AO_AppMgr for @p project's folder from @p url, answered to the spy. */ -static void ask_get(const char *project, const char *url) +/** + * @brief Ask AO_AppMgr for @p project's folder from @p url, answered to the + * spy; @p install says an install of the app follows. + */ +static void ask_get_as(const char *project, const char *url, bool install) { AppDataGetReqEvt e; @@ -10097,10 +10100,17 @@ static void ask_get(const char *project, const char *url) e.super = (QEvt)QEVT_INITIALIZER(APPDATA_GET_REQ_SIG); (void)snprintf(e.project, sizeof(e.project), "%s", project); (void)snprintf(e.url, sizeof(e.url), "%s", url); - e.who = ao_spy(); + e.who = ao_spy(); + e.install = install; QASM_DISPATCH(AO_AppMgr, &e.super, 0U); } +/** @brief Ask AO_AppMgr for @p project's folder from @p url, answered to the spy. */ +static void ask_get(const char *project, const char *url) +{ + ask_get_as(project, url, false); +} + /** @brief What AO_Net's queue holds: the opens and closes, and the last URL opened. */ typedef struct { uint32_t opens; @@ -10199,14 +10209,15 @@ const uint8_t *host_net_stream_buf(uint8_t idx) /** * @brief Run a fetch of "snake" up to the put it asks AO_Storage for: app.json - * arrives whole in one chunk and names no icon. + * arrives whole in one chunk and names no icon. @p install says an + * install of the app follows. */ -static void fetch_to_the_put(void) +static void fetch_to_the_put_as(bool install) { StorageDataEvt d; uint32_t const len = (uint32_t)(sizeof(GET_JSON_BODY) - 1U); - ask_get("snake", GET_URL); + ask_get_as("snake", GET_URL, install); (void)net_seen(); fetch_opened((uint8_t)KODE_OK, len); @@ -10220,6 +10231,12 @@ static void fetch_to_the_put(void) QASM_DISPATCH(AO_AppMgr, &d.super, 0U); } +/** @brief fetch_to_the_put_as() for a fetch that announces nothing. */ +static void fetch_to_the_put(void) +{ + fetch_to_the_put_as(false); +} + /** * @brief AO_Storage reads the put AO_AppMgr asked for: it must name "snake" * and carry app.json whole, and it gives the buffer back. @@ -10590,15 +10607,15 @@ TEST_CASE("the fetch's request refuses what it cannot post", (void)memset(long_url, 'u', sizeof(long_url) - 1U); long_url[sizeof(long_url) - 1U] = '\0'; - TEST_ASSERT_EQUAL_INT(KODE_ERROR, appmgr_appdata_get_request(NULL, GET_URL, ao_spy())); - TEST_ASSERT_EQUAL_INT(KODE_ERROR, appmgr_appdata_get_request("snake", NULL, ao_spy())); - TEST_ASSERT_EQUAL_INT(KODE_ERROR, appmgr_appdata_get_request("snake", GET_URL, NULL)); + TEST_ASSERT_EQUAL_INT(KODE_ERROR, appmgr_appdata_get_request(NULL, GET_URL, false, ao_spy())); + TEST_ASSERT_EQUAL_INT(KODE_ERROR, appmgr_appdata_get_request("snake", NULL, false, ao_spy())); + TEST_ASSERT_EQUAL_INT(KODE_ERROR, appmgr_appdata_get_request("snake", GET_URL, false, NULL)); TEST_ASSERT_EQUAL_INT_MESSAGE(KODE_ERROR, - appmgr_appdata_get_request("snake", long_url, ao_spy()), + appmgr_appdata_get_request("snake", long_url, false, ao_spy()), "a URL that does not fit the request was cut, not refused"); TEST_ASSERT_EQUAL_UINT(0U, (unsigned)uxQueueMessagesWaiting(AO_AppMgr->eQueue)); - TEST_ASSERT_EQUAL_INT(KODE_OK, appmgr_appdata_get_request("snake", GET_URL, ao_spy())); + TEST_ASSERT_EQUAL_INT(KODE_OK, appmgr_appdata_get_request("snake", GET_URL, false, ao_spy())); (void)qf_support_pump(AO_AppMgr, 8U); net_seen_t const n = net_seen(); TEST_ASSERT_EQUAL_UINT(1U, n.opens); @@ -10639,3 +10656,392 @@ TEST_CASE("a fetch of an app's data folder shows from its first moment and ends QActive_unsubscribe(ao_spy(), APP_FLASH_FAIL_SIG); fetch_end(); } + +/* ---- A fetch that announces an install -------------------------------------- + * `appdata_get install=on`: the host sends the app's install the moment the + * fetch is answered, and the glass must not empty between the two. The fetch's + * card stays at 0 % for the install; an install from a medium or a URL that + * begins takes it over, and anything else - a refusal, a terminal install, + * another request, the deadline - ends it with the unnamed done. */ + +/** @brief KODE_APPMGR_HOLD_MS in QP ticks, as appmgr_ao.c has it. */ +#define APPMGR_HOLD_TEST_TICKS (KODE_APPMGR_HOLD_MS / 10U) + +/** @brief What the card was told, in the order the spy holds it. */ +typedef struct { + unsigned reports; /**< APP_FLASH_PROGRESS_SIG. */ + unsigned dones; /**< APP_FLASH_DONE_SIG. */ + unsigned fails; /**< APP_FLASH_FAIL_SIG. */ + unsigned answers; /**< APPDATA_GET_RSP_SIG. */ + uint8_t why; /**< The last answer's appdata_get_why_t. */ + enum_t first; /**< The first of the card's three, or 0. */ + char project[KODE_APP_PROJECT_MAX]; /**< The last report's. */ +} card_seen_t; + +/** @brief Read what the spy holds about the card and the fetch, draining it. */ +static card_seen_t card_seen(void) +{ + card_seen_t seen; + + (void)memset(&seen, 0, sizeof(seen)); + for ( const QEvt *e = ao_spy_take(); NULL != e; e = ao_spy_take() ) + { + enum_t const sig = (enum_t)e->sig; + + if ( sig == APP_FLASH_PROGRESS_SIG ) + { + ++seen.reports; + (void)snprintf(seen.project, sizeof(seen.project), "%s", + ((AppProgressEvt const *)e)->project); + } + else if ( sig == APP_FLASH_DONE_SIG ) + { + ++seen.dones; + } + else if ( sig == APP_FLASH_FAIL_SIG ) + { + ++seen.fails; + } + else if ( sig == APPDATA_GET_RSP_SIG ) + { + ++seen.answers; + seen.why = ((AppDataGetRspEvt const *)e)->why; + continue; + } + else + { + continue; + } + if ( 0 == seen.first ) + { + seen.first = sig; + } + } + return seen; +} + +/** @brief The spy hears the card: every report, done and failure. */ +static void card_listen(void) +{ + ao_spy_subscribe(APP_FLASH_PROGRESS_SIG); + ao_spy_subscribe(APP_FLASH_DONE_SIG); + ao_spy_subscribe(APP_FLASH_FAIL_SIG); +} + +/** @brief A fetch of "snake" that announced an install, answered ok, its card held. */ +static void card_held(void) +{ + fetch_fresh(); + card_listen(); + fetch_to_the_put_as(true); + fetch_put_is_whole(); + put_rsp("snake", KODE_OK); + + card_seen_t const seen = card_seen(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.answers, "the fetch is answered as ever"); + TEST_ASSERT_EQUAL_INT(APPDATA_GET_OK, seen.why); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.dones, "the card closed before the install"); + TEST_ASSERT_EQUAL_UINT(0U, seen.fails); +} + +TEST_CASE("a fetch that announces nothing ends its card with the unnamed done", + "[appmgr_ao][appdata_get][hold]") +{ + fetch_fresh(); + card_listen(); + fetch_to_the_put(); + fetch_put_is_whole(); + put_rsp("snake", KODE_OK); + + card_seen_t const seen = card_seen(); + TEST_ASSERT_EQUAL_UINT(1U, seen.answers); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.dones, "the card the fetch raised is left up"); + run_ticks(3U * APPMGR_WAIT_BEAT_TICKS); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, card_seen().reports, + "a fetch that announced nothing kept saying its percent"); + + QActive_unsubscribeAll(ao_spy()); + fetch_end(); +} + +TEST_CASE("a fetch that announces an install keeps its card at 0 % after its answer", + "[appmgr_ao][appdata_get][hold]") +{ + card_held(); + + run_ticks((2U * APPMGR_WAIT_BEAT_TICKS) + 10U); + card_seen_t const seen = card_seen(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(2U, seen.reports, + "the held card fell silent, and the LED with it"); + TEST_ASSERT_EQUAL_STRING("snake", seen.project); + TEST_ASSERT_EQUAL_UINT(0U, seen.dones); + + QActive_unsubscribeAll(ao_spy()); + fetch_end(); +} + +TEST_CASE("the install a fetch announced takes its card over without closing it", + "[appmgr_ao][appdata_get][hold]") +{ + card_held(); + + ask_https("https://example.invalid/snake.bin"); + card_seen_t seen = card_seen(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.dones, + "the card closed between the fetch and its install"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.reports, "the install said nothing of its own"); + /* A URL is not its project: the card's reports name nobody until the + * image's first chunk is read, and the glass keeps the fetch's face. */ + TEST_ASSERT_EQUAL_STRING_MESSAGE("", seen.project, + "the install named an app before it read its image"); + + let_the_open_expire(); /* past the hold's deadline too */ + seen = card_seen(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.fails, "the install did not end its own card"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.dones, + "the hold outlived the install that took it over"); + + QActive_unsubscribeAll(ao_spy()); + (void)net_seen(); + fetch_end(); +} + +TEST_CASE("a terminal install ends the card a fetch kept and raises its own at its ready", + "[appmgr_ao][appdata_get][hold]") +{ + uint32_t const len = image_named("snake"); + + card_held(); + + ask_recv_named(len, 0xDEADBEEFU, false, "snake"); + card_seen_t seen = card_seen(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.dones, + "a terminal install took the card a fetch kept, and owes it no end before its ready"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.reports, + "a terminal install announced itself before the wire was its"); + + raw_armed(true); + seen = card_seen(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.reports, + "the install's own card did not rise at its ready"); + TEST_ASSERT_EQUAL_UINT(0U, seen.dones); + + feed(s_named_img, (uint16_t)len); /* the write ends, CRC refused */ + run_ticks(APPMGR_ANSWER_DRAIN_DEADLINE_TICKS + 2U); + seen = card_seen(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.fails, "the install did not end its own card"); + run_ticks(APPMGR_HOLD_TEST_TICKS + 10U); + seen = card_seen(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.dones, "the card was ended twice"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.reports, + "the hold kept saying 0 % after the install came"); + QActive_unsubscribeAll(ao_spy()); + fetch_end(); +} + +TEST_CASE("a terminal install refused the wire leaves no card behind it", + "[appmgr_ao][appdata_get][hold]") +{ + uint32_t const len = image_named("snake"); + + card_held(); + + ask_recv_named(len, 0xDEADBEEFU, false, "snake"); + raw_armed(false); /* AO_Log refuses to arm */ + card_seen_t seen = card_seen(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.dones, + "the card a fetch kept outlived an install that never took the wire"); + TEST_ASSERT_EQUAL_UINT(0U, seen.fails); + + run_ticks(APPMGR_HOLD_TEST_TICKS + 10U); + seen = card_seen(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.dones, "the card was ended twice"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.reports, + "a card nobody will end is still being said"); + QActive_unsubscribeAll(ao_spy()); + fetch_end(); +} + +TEST_CASE("the card a fetch kept ends when its install does not come", + "[appmgr_ao][appdata_get][hold]") +{ + card_held(); + + run_ticks(APPMGR_HOLD_TEST_TICKS - 50U); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, card_seen().dones, "the card closed early"); + + run_ticks(100U); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, card_seen().dones, + "a card waiting for an install that never comes stands for ever"); + run_ticks(3U * APPMGR_WAIT_BEAT_TICKS); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, card_seen().reports, + "the beat went on after the card ended"); + + QActive_unsubscribeAll(ao_spy()); + fetch_end(); +} + +TEST_CASE("an install refused before it begins ends the card a fetch kept", + "[appmgr_ao][appdata_get][hold]") +{ + card_held(); + + ask_recv(0U, 0U, false); /* no size: refused in idle */ + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, card_seen().dones, + "a refused install left the card waiting for it"); + run_ticks(APPMGR_HOLD_TEST_TICKS + 10U); + card_seen_t const seen = card_seen(); + TEST_ASSERT_EQUAL_UINT(0U, seen.reports); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.dones, "the card was ended twice"); + + QActive_unsubscribeAll(ao_spy()); + fetch_end(); +} + +TEST_CASE("another request ends the card a fetch kept before it is taken", + "[appmgr_ao][appdata_get][hold]") +{ + card_held(); + + ask_get("ladder", GET_URL); /* another app's folder */ + card_seen_t const seen = card_seen(); + TEST_ASSERT_EQUAL_UINT(1U, seen.dones); + TEST_ASSERT_EQUAL_INT_MESSAGE(APP_FLASH_DONE_SIG, seen.first, + "the next card rose over the one before, which wore another app's face"); + TEST_ASSERT_EQUAL_STRING("ladder", seen.project); + + (void)net_seen(); + fetch_opened((uint8_t)KODE_ERROR, 0U); /* the second fetch ends */ + QActive_unsubscribeAll(ao_spy()); + ao_spy_drain(); + fetch_end(); +} + +TEST_CASE("a fetch that announces an install and fails leaves its card to the install", + "[appmgr_ao][appdata_get][hold]") +{ + fetch_fresh(); + card_listen(); + + ask_get_as("snake", GET_URL, true); + (void)net_seen(); + fetch_opened((uint8_t)KODE_ERROR, 0U); /* the server says no */ + card_seen_t seen = card_seen(); + TEST_ASSERT_EQUAL_UINT(1U, seen.answers); + TEST_ASSERT_EQUAL_INT_MESSAGE(APPDATA_GET_FETCH, seen.why, + "the fetch's own answer changed"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.fails, + "the app's data is not the install: its failure raised the failure sheet"); + TEST_ASSERT_EQUAL_UINT(0U, seen.dones); + + run_ticks(APPMGR_HOLD_TEST_TICKS + 10U); + seen = card_seen(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.dones, "the deadline did not end the card"); + + QActive_unsubscribeAll(ao_spy()); + fetch_end(); +} + +/** @brief The held card has just been ended, once, by @p who, and stays ended. */ +static void card_expect_ended(const char *who) +{ + card_seen_t seen = card_seen(); + + TEST_ASSERT_EQUAL_UINT_MESSAGE(1U, seen.dones, who); + TEST_ASSERT_EQUAL_INT_MESSAGE(APP_FLASH_DONE_SIG, seen.first, who); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.fails, who); + run_ticks(APPMGR_HOLD_TEST_TICKS + 10U); + seen = card_seen(); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.dones, "the card was ended twice"); + TEST_ASSERT_EQUAL_UINT_MESSAGE(0U, seen.reports, "an ended card is still being said"); +} + +TEST_CASE("an install from a medium refused before it begins ends the card a fetch kept", + "[appmgr_ao][appdata_get][hold]") +{ + char long_name[KODE_FLASH_SRC_MAX]; + + (void)memset(long_name, 'n', sizeof(long_name) - 1U); + long_name[sizeof(long_name) - 1U] = '\0'; + + card_held(); + + /* Longer than a file's name may be: refused in idle, with no failure. */ + ask_install_from((uint8_t)KFW_SRC_FS, long_name, (uint8_t)KFW_TARGET_APP); + card_expect_ended("a refused install left the card waiting for it"); + + QActive_unsubscribeAll(ao_spy()); + ao_spy_drain(); + fetch_end(); +} + +TEST_CASE("a run ends the card a fetch kept", "[appmgr_ao][appdata_get][hold]") +{ + card_held(); + + ask_run_from("ghost"); /* not installed: answered in idle */ + card_expect_ended("a run left the card of an install that is not it"); + + QActive_unsubscribeAll(ao_spy()); + ao_spy_drain(); + fetch_end(); +} + +TEST_CASE("an erase ends the card a fetch kept", "[appmgr_ao][appdata_get][hold]") +{ + card_held(); + + ask_erase(""); /* no name: answered in idle */ + card_expect_ended("an erase left the card of an install that is not it"); + + QActive_unsubscribeAll(ao_spy()); + ao_spy_drain(); + fetch_end(); +} + +TEST_CASE("a keep-only ends the card a fetch kept", "[appmgr_ao][appdata_get][hold]") +{ + card_held(); + + TEST_ASSERT_TRUE(appmgr_keep_only_request("ghost", ao_spy())); + (void)qf_support_pump(AO_AppMgr, 8U); /* not installed: answered in idle */ + card_expect_ended("a keep-only left the card of an install that is not it"); + + QActive_unsubscribeAll(ao_spy()); + ao_spy_drain(); + fetch_end(); +} + +TEST_CASE("a held card answers a round busy, and an ended one is ready", + "[appmgr_ao][appdata_get][hold][lowpower]") +{ + card_held(); + + lp_round(AO_AppMgr, KODE_LP_ASLEEP); + lp_expect_answer(true, "install announced"); + lp_expect_no_more(); + + run_ticks(APPMGR_HOLD_TEST_TICKS + 10U); /* the install never came */ + lp_round(AO_AppMgr, KODE_LP_ASLEEP); + lp_expect_answer(false, NULL); + lp_expect_no_more(); + + QActive_unsubscribeAll(ao_spy()); + ao_spy_drain(); + fetch_end(); +} + +TEST_CASE("the fetch's request carries whether an install follows", + "[appmgr_ao][appdata_get][hold]") +{ + fetch_fresh(); + TEST_ASSERT_EQUAL_INT(KODE_OK, + appmgr_appdata_get_request("snake", GET_URL, true, ao_spy())); + + const QEvt *e = NULL; + TEST_ASSERT_TRUE(pdTRUE == xQueueReceive(AO_AppMgr->eQueue, &e, 0U)); + TEST_ASSERT_EQUAL_INT(APPDATA_GET_REQ_SIG, (int)e->sig); + TEST_ASSERT_TRUE(((AppDataGetReqEvt const *)e)->install); + QF_gc(e); + fetch_end(); +} diff --git a/tests/host/cases/test_console_appdata.c b/tests/host/cases/test_console_appdata.c index 03fc331..a2c47bf 100644 --- a/tests/host/cases/test_console_appdata.c +++ b/tests/host/cases/test_console_appdata.c @@ -685,6 +685,50 @@ TEST_CASE("appdata_get needs both its keys", "[console_appdata]") TEST_ASSERT_FALSE(get_asked(&req)); } +TEST_CASE("appdata_get says an install follows only when it is told so", + "[console_appdata]") +{ + static const struct { + const char *tail; + bool install; + } CASES[] = { + { "", false }, + { " install=off", false }, + { " install=on", true }, + }; + static char line[KODE_CONSOLE_LINE_MAX]; + AppDataGetReqEvt req; + + for ( size_t i = 0U; (sizeof(CASES) / sizeof(CASES[0])) > i; ++i ) + { + console_up_once(); + (void)get_asked(&req); + (void)snprintf(line, sizeof(line), + "appdata_get name=\"snake\" url=\"http://example.invalid/s/\"%s", + CASES[i].tail); + + type_line(line); + + TEST_ASSERT_TRUE_MESSAGE(get_asked(&req), line); + TEST_ASSERT_EQUAL_MESSAGE(CASES[i].install, req.install, line); + get_answered("snake", APPDATA_GET_OK, false); + TEST_ASSERT_TRUE(replied(">ok")); + } +} + +TEST_CASE("appdata_get refuses an install that is neither on nor off", + "[console_appdata]") +{ + AppDataGetReqEvt req; + + console_up_once(); + (void)get_asked(&req); + + type_line("appdata_get name=\"snake\" url=\"http://example.invalid/s/\" install=yes"); + TEST_ASSERT_TRUE(replied(">err code=3 msg=install")); + TEST_ASSERT_FALSE_MESSAGE(get_asked(&req), "a fetch was asked for on a word it cannot read"); +} + TEST_CASE("an answer to a fetch nobody typed says nothing", "[console_appdata]") { console_up_once();