@mikrojs/firmware 0.18.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/mikrojs/CMakeLists.txt +12 -1
- package/components/mikrojs/include/mik_http_internal.h +47 -0
- package/components/mikrojs/include/mik_ota_native.h +19 -0
- package/components/mikrojs/mik_config.cpp +28 -5
- package/components/mikrojs/mik_http.cpp +199 -8
- package/components/mikrojs/mik_nvs_kv.cpp +28 -5
- package/components/mikrojs/mik_ota.cpp +305 -106
- package/components/mikrojs/mik_ota_client_module.cpp +743 -0
- package/components/mikrojs/mik_ota_env.cpp +370 -0
- package/components/mikrojs/test/ota_host/run.sh +7 -2
- package/package.json +3 -3
- package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32/mikrojs.bin +0 -0
- package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c3/mikrojs.bin +0 -0
- package/prebuilds/esp32c5/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c5/mikrojs.bin +0 -0
- package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c6/mikrojs.bin +0 -0
- package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32s3/mikrojs.bin +0 -0
- package/sdkconfig.defaults +58 -0
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
#include "mikrojs/app_store.h"
|
|
30
30
|
#include "mikrojs/mikrojs.h"
|
|
31
|
+
#include "mikrojs/ota_env.h"
|
|
31
32
|
#include "mikrojs/platform.h"
|
|
32
33
|
#include "mikrojs_esp32.h"
|
|
33
34
|
#include "quickjs.h"
|
|
@@ -1004,49 +1005,45 @@ bool valid_checksum(const char* s) {
|
|
|
1004
1005
|
return i == 64;
|
|
1005
1006
|
}
|
|
1006
1007
|
|
|
1007
|
-
//
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1008
|
+
// ── Staging cores ────────────────────────────────────────────────────────────
|
|
1009
|
+
// The install machinery, with no JSContext in sight. Two callers sit on top: the
|
|
1010
|
+
// native:mikro/ota JS bindings below, and the MIKOtaEnv install ops the native
|
|
1011
|
+
// OTA client drives (mik__ota_fill_install_ops). One implementation, so the two
|
|
1012
|
+
// can never drift on something as unforgiving as install safety.
|
|
1013
|
+
|
|
1014
|
+
struct CoreErr {
|
|
1015
|
+
const char* msg = nullptr;
|
|
1016
|
+
OtaErr kind = OtaErr::kTransient;
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
bool core_fail(CoreErr* e, const char* msg, OtaErr kind = OtaErr::kTransient) {
|
|
1020
|
+
if (e) {
|
|
1021
|
+
e->msg = msg;
|
|
1022
|
+
e->kind = kind;
|
|
1017
1023
|
}
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
bool core_stage_begin(const char* checksum, int64_t size, int64_t* out_resume, CoreErr* e) {
|
|
1028
|
+
if (!checksum) return core_fail(e, "checksum not a string");
|
|
1018
1029
|
if (!valid_checksum(checksum)) {
|
|
1019
|
-
JS_FreeCString(ctx, checksum);
|
|
1020
1030
|
// kTransient, not the default: the build is fine, the offer is
|
|
1021
1031
|
// malformed, so this must not count against the build's retry budget.
|
|
1022
|
-
return
|
|
1023
|
-
OtaErr::kTransient);
|
|
1024
|
-
}
|
|
1025
|
-
int64_t size = 0;
|
|
1026
|
-
if (JS_ToInt64(ctx, &size, argv[1]) || size <= 0) {
|
|
1027
|
-
JS_FreeCString(ctx, checksum);
|
|
1028
|
-
// JS_ToInt64 throws on a value with a throwing valueOf. We report through
|
|
1029
|
-
// the result object, so clear it rather than strand it on the context for
|
|
1030
|
-
// some unrelated later operation to surface.
|
|
1031
|
-
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
1032
|
-
return err_obj(ctx, "invalid size");
|
|
1032
|
+
return core_fail(e, "checksum must be 64 lowercase hex characters", OtaErr::kTransient);
|
|
1033
1033
|
}
|
|
1034
|
-
|
|
1035
|
-
//
|
|
1034
|
+
if (size <= 0) return core_fail(e, "invalid size");
|
|
1035
|
+
// Reject an impossible size while it is still int64: stgSize is u32, so a
|
|
1036
|
+
// bogus offer above 4 GB would otherwise truncate into a small cap.
|
|
1036
1037
|
long fs_total = 0;
|
|
1037
1038
|
long fs_free = 0;
|
|
1038
1039
|
if (fs_space(&fs_total, &fs_free) && size > fs_total) {
|
|
1039
|
-
|
|
1040
|
-
return err_obj(ctx, "offered build is larger than the filesystem");
|
|
1040
|
+
return core_fail(e, "offered build is larger than the filesystem");
|
|
1041
1041
|
}
|
|
1042
1042
|
|
|
1043
1043
|
stage_close(); // flush any prior staging file before stat/unlink below
|
|
1044
1044
|
|
|
1045
1045
|
nvs_handle_t h;
|
|
1046
|
-
if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK)
|
|
1047
|
-
JS_FreeCString(ctx, checksum);
|
|
1048
|
-
return err_obj(ctx, "nvs open failed");
|
|
1049
|
-
}
|
|
1046
|
+
if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK) return core_fail(e, "nvs open failed");
|
|
1050
1047
|
|
|
1051
1048
|
// Resume only if the in-progress staging file is for the same checksum+size
|
|
1052
1049
|
// and hasn't already grown past the declared size.
|
|
@@ -1064,35 +1061,25 @@ JSValue js_stage_begin(JSContext* ctx, JSValueConst, int argc, JSValueConst* arg
|
|
|
1064
1061
|
nvs_commit(h);
|
|
1065
1062
|
}
|
|
1066
1063
|
nvs_close(h);
|
|
1067
|
-
JS_FreeCString(ctx, checksum);
|
|
1068
1064
|
|
|
1069
1065
|
g_stage_cap = (uint32_t)size;
|
|
1070
1066
|
g_stage_have = resume;
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
JS_SetPropertyStr(ctx, o, "resumeOffset", JS_NewInt64(ctx, resume));
|
|
1074
|
-
return o;
|
|
1067
|
+
if (out_resume) *out_resume = resume;
|
|
1068
|
+
return true;
|
|
1075
1069
|
}
|
|
1076
1070
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
if (
|
|
1080
|
-
size_t len = 0;
|
|
1081
|
-
const uint8_t* data = JS_GetUint8Array(ctx, &len, argv[0]);
|
|
1082
|
-
if (!data) {
|
|
1083
|
-
JS_FreeValue(ctx, JS_GetException(ctx)); // JS_GetUint8Array throws; we report via err_obj
|
|
1084
|
-
return err_obj(ctx, "bytes not a Uint8Array");
|
|
1085
|
-
}
|
|
1086
|
-
if (len == 0) return ok_obj(ctx);
|
|
1071
|
+
bool core_stage_write(const uint8_t* data, size_t len, CoreErr* e) {
|
|
1072
|
+
if (!data) return core_fail(e, "bytes not a Uint8Array");
|
|
1073
|
+
if (len == 0) return true;
|
|
1087
1074
|
|
|
1088
1075
|
// Enforce the size cap from stageBegin so a runaway download can't fill flash.
|
|
1089
1076
|
if (g_stage_cap > 0 && (uint64_t)g_stage_have + len > g_stage_cap) {
|
|
1090
|
-
return
|
|
1077
|
+
return core_fail(e, "staged write exceeds declared size", OtaErr::kTransient);
|
|
1091
1078
|
}
|
|
1092
1079
|
|
|
1093
1080
|
if (!g_stage_file) {
|
|
1094
1081
|
g_stage_file = fopen(kStaging, "ab");
|
|
1095
|
-
if (!g_stage_file) return
|
|
1082
|
+
if (!g_stage_file) return core_fail(e, "open staging file", OtaErr::kTransient);
|
|
1096
1083
|
}
|
|
1097
1084
|
size_t wrote = fwrite(data, 1, len, g_stage_file);
|
|
1098
1085
|
if (wrote != len) {
|
|
@@ -1105,27 +1092,13 @@ JSValue js_stage_write(JSContext* ctx, JSValueConst, int argc, JSValueConst* arg
|
|
|
1105
1092
|
stage_close();
|
|
1106
1093
|
long actual = file_size(kStaging);
|
|
1107
1094
|
g_stage_have = actual < 0 ? g_stage_have + (long)wrote : actual;
|
|
1108
|
-
return
|
|
1095
|
+
return core_fail(e, "write staging file (disk full?)", OtaErr::kTransient);
|
|
1109
1096
|
}
|
|
1110
1097
|
g_stage_have += (long)len;
|
|
1111
|
-
return
|
|
1098
|
+
return true;
|
|
1112
1099
|
}
|
|
1113
1100
|
|
|
1114
|
-
|
|
1115
|
-
// -> { ok } | { ok:false, error, kind }
|
|
1116
|
-
// Verifies SHA-256 + size over the whole staged file, then either installs in
|
|
1117
|
-
// place now (installNow) or marks a verified build staged-for-install so the
|
|
1118
|
-
// next boot's reconcile installs it on a clean heap.
|
|
1119
|
-
JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
1120
|
-
int64_t trial_boots = 1;
|
|
1121
|
-
if (argc >= 1 && JS_ToInt64(ctx, &trial_boots, argv[0])) {
|
|
1122
|
-
// A throwing valueOf leaves trial_boots indeterminate and an exception
|
|
1123
|
-
// pending; we report through the result object, so clear it and default.
|
|
1124
|
-
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
1125
|
-
trial_boots = 1;
|
|
1126
|
-
}
|
|
1127
|
-
bool require_confirm = argc >= 2 && JS_ToBool(ctx, argv[1]);
|
|
1128
|
-
bool install_now = argc >= 3 && JS_ToBool(ctx, argv[2]);
|
|
1101
|
+
bool core_stage_finish(int64_t trial_boots, bool require_confirm, bool install_now, CoreErr* e) {
|
|
1129
1102
|
if (trial_boots < 0) trial_boots = 0;
|
|
1130
1103
|
// A confirm-gated trial needs at least one boot to run in. Boot reconcile
|
|
1131
1104
|
// evaluates the trial before the app loads, so trialBoots:0 with confirm set
|
|
@@ -1135,12 +1108,12 @@ JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* ar
|
|
|
1135
1108
|
if (trial_boots > 255) trial_boots = 255;
|
|
1136
1109
|
|
|
1137
1110
|
if (!stage_close()) {
|
|
1138
|
-
return
|
|
1111
|
+
return core_fail(e, "write staging file (disk full?)", OtaErr::kTransient);
|
|
1139
1112
|
}
|
|
1140
1113
|
|
|
1141
1114
|
nvs_handle_t h;
|
|
1142
1115
|
if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK) {
|
|
1143
|
-
return
|
|
1116
|
+
return core_fail(e, "nvs open failed", OtaErr::kTransient);
|
|
1144
1117
|
}
|
|
1145
1118
|
char want_chk[96];
|
|
1146
1119
|
bool has_chk = nvs_str(h, kKeyStgChk, want_chk, sizeof(want_chk));
|
|
@@ -1148,32 +1121,32 @@ JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* ar
|
|
|
1148
1121
|
|
|
1149
1122
|
if (!has_chk) {
|
|
1150
1123
|
nvs_close(h);
|
|
1151
|
-
return
|
|
1124
|
+
return core_fail(e, "no staged build", OtaErr::kTransient);
|
|
1152
1125
|
}
|
|
1153
1126
|
long have = file_size(kStaging);
|
|
1154
1127
|
if (have < 0) {
|
|
1155
1128
|
nvs_close(h);
|
|
1156
|
-
return
|
|
1129
|
+
return core_fail(e, "staging file missing", OtaErr::kTransient);
|
|
1157
1130
|
}
|
|
1158
1131
|
if ((uint32_t)have != want_size) {
|
|
1159
1132
|
nvs_close(h);
|
|
1160
|
-
return
|
|
1133
|
+
return core_fail(e, "staged size mismatch", OtaErr::kCorrupt);
|
|
1161
1134
|
}
|
|
1162
1135
|
char got_chk[65];
|
|
1163
1136
|
if (!sha256_file(kStaging, got_chk)) {
|
|
1164
1137
|
nvs_close(h);
|
|
1165
|
-
return
|
|
1138
|
+
return core_fail(e, "read staging file", OtaErr::kTransient);
|
|
1166
1139
|
}
|
|
1167
1140
|
if (strcmp(got_chk, want_chk) != 0) {
|
|
1168
1141
|
nvs_close(h);
|
|
1169
|
-
return
|
|
1142
|
+
return core_fail(e, "checksum mismatch (corrupt download)", OtaErr::kCorrupt);
|
|
1170
1143
|
}
|
|
1171
1144
|
|
|
1172
1145
|
// Verified. Promote the staging file to the pending build.
|
|
1173
1146
|
unlink(kPending);
|
|
1174
1147
|
if (rename(kStaging, kPending) != 0) {
|
|
1175
1148
|
nvs_close(h);
|
|
1176
|
-
return
|
|
1149
|
+
return core_fail(e, "stage pending build", OtaErr::kTransient);
|
|
1177
1150
|
}
|
|
1178
1151
|
g_stage_cap = 0;
|
|
1179
1152
|
g_stage_have = 0;
|
|
@@ -1202,7 +1175,7 @@ JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* ar
|
|
|
1202
1175
|
}
|
|
1203
1176
|
nvs_commit(h);
|
|
1204
1177
|
nvs_close(h);
|
|
1205
|
-
return
|
|
1178
|
+
return core_fail(e, err, kind);
|
|
1206
1179
|
}
|
|
1207
1180
|
// New app is live but unproven: enter the trial. The caller restarts.
|
|
1208
1181
|
nvs_set_u8(h, kKeyState, kStateTrial);
|
|
@@ -1214,7 +1187,7 @@ JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* ar
|
|
|
1214
1187
|
clear_trial_failure(h); // fresh trial: this build has not failed yet
|
|
1215
1188
|
nvs_commit(h);
|
|
1216
1189
|
nvs_close(h);
|
|
1217
|
-
return
|
|
1190
|
+
return true;
|
|
1218
1191
|
}
|
|
1219
1192
|
|
|
1220
1193
|
// Defer: install at the next boot on a clean heap.
|
|
@@ -1222,11 +1195,10 @@ JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* ar
|
|
|
1222
1195
|
nvs_set_u8(h, kKeyInstLeft, kInstallBudget);
|
|
1223
1196
|
nvs_commit(h);
|
|
1224
1197
|
nvs_close(h);
|
|
1225
|
-
return
|
|
1198
|
+
return true;
|
|
1226
1199
|
}
|
|
1227
1200
|
|
|
1228
|
-
|
|
1229
|
-
JSValue js_stage_abort(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
1201
|
+
void core_stage_abort(void) {
|
|
1230
1202
|
stage_close();
|
|
1231
1203
|
g_stage_cap = 0;
|
|
1232
1204
|
g_stage_have = 0;
|
|
@@ -1238,14 +1210,13 @@ JSValue js_stage_abort(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
|
1238
1210
|
nvs_commit(h);
|
|
1239
1211
|
nvs_close(h);
|
|
1240
1212
|
}
|
|
1241
|
-
return JS_UNDEFINED;
|
|
1242
1213
|
}
|
|
1243
1214
|
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1246
|
-
|
|
1215
|
+
// Confirm the running trial: promote it to GOOD and make the pending build the
|
|
1216
|
+
// new revert target.
|
|
1217
|
+
void core_mark_valid(void) {
|
|
1247
1218
|
nvs_handle_t h;
|
|
1248
|
-
if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK) return
|
|
1219
|
+
if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK) return;
|
|
1249
1220
|
if (nvs_u8(h, kKeyState, kStateGood) == kStateTrial) {
|
|
1250
1221
|
// Stay in the trial if the promotion failed: going GOOD anyway would
|
|
1251
1222
|
// point instChk at a build that has no .tgz left to revert to.
|
|
@@ -1263,15 +1234,13 @@ JSValue js_mark_valid(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
|
1263
1234
|
nvs_commit(h);
|
|
1264
1235
|
}
|
|
1265
1236
|
nvs_close(h);
|
|
1266
|
-
return JS_UNDEFINED;
|
|
1267
1237
|
}
|
|
1268
1238
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
if (!path_exists(kLastGood)) return err_obj(ctx, "no last-good build to revert to");
|
|
1239
|
+
bool core_revert(CoreErr* e) {
|
|
1240
|
+
if (!path_exists(kLastGood)) return core_fail(e, "no last-good build to revert to");
|
|
1272
1241
|
const char* err = nullptr;
|
|
1273
1242
|
OtaErr kind = OtaErr::kTransient;
|
|
1274
|
-
if (!install_build(kLastGood, &err, &kind)) return
|
|
1243
|
+
if (!install_build(kLastGood, &err, &kind)) return core_fail(e, err, kind);
|
|
1275
1244
|
nvs_handle_t h;
|
|
1276
1245
|
if (nvs_open(kNs, NVS_READWRITE, &h) == ESP_OK) {
|
|
1277
1246
|
nvs_set_u8(h, kKeyState, kStateGood);
|
|
@@ -1282,40 +1251,36 @@ JSValue js_revert(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
|
1282
1251
|
nvs_commit(h);
|
|
1283
1252
|
nvs_close(h);
|
|
1284
1253
|
}
|
|
1285
|
-
return
|
|
1254
|
+
return true;
|
|
1286
1255
|
}
|
|
1287
1256
|
|
|
1288
|
-
|
|
1289
|
-
JSValue js_running(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
1290
|
-
JSValue o = JS_NewObject(ctx);
|
|
1257
|
+
void core_running(char* chk, size_t chk_len, bool* out_trial) {
|
|
1291
1258
|
bool trial = false;
|
|
1292
|
-
|
|
1259
|
+
if (chk && chk_len) chk[0] = '\0';
|
|
1293
1260
|
nvs_handle_t h;
|
|
1294
1261
|
if (nvs_open(kNs, NVS_READONLY, &h) == ESP_OK) {
|
|
1295
1262
|
trial = nvs_u8(h, kKeyState, kStateGood) == kStateTrial;
|
|
1296
|
-
nvs_str(h, trial ? kKeyPendChk : kKeyInstChk, chk,
|
|
1263
|
+
if (chk && chk_len) nvs_str(h, trial ? kKeyPendChk : kKeyInstChk, chk, chk_len);
|
|
1297
1264
|
nvs_close(h);
|
|
1298
1265
|
}
|
|
1299
|
-
if (
|
|
1300
|
-
JS_SetPropertyStr(ctx, o, "trial", JS_NewBool(ctx, trial));
|
|
1301
|
-
return o;
|
|
1266
|
+
if (out_trial) *out_trial = trial;
|
|
1302
1267
|
}
|
|
1303
1268
|
|
|
1304
|
-
//
|
|
1305
|
-
//
|
|
1306
|
-
|
|
1307
|
-
|
|
1269
|
+
// Read the outcome the boot-time reconcile recorded, then clear it so a second
|
|
1270
|
+
// call reads empty.
|
|
1271
|
+
void core_reconcile(char* installed, size_t installed_len, bool* out_reverted, char* reason,
|
|
1272
|
+
size_t reason_len, char* detail, size_t detail_len) {
|
|
1273
|
+
if (installed && installed_len) installed[0] = '\0';
|
|
1274
|
+
if (reason && reason_len) reason[0] = '\0';
|
|
1275
|
+
if (detail && detail_len) detail[0] = '\0';
|
|
1308
1276
|
bool reverted = false;
|
|
1309
|
-
char installed[96] = {0};
|
|
1310
|
-
char reason[96] = {0};
|
|
1311
|
-
char detail[160] = {0};
|
|
1312
1277
|
|
|
1313
1278
|
nvs_handle_t h;
|
|
1314
1279
|
if (nvs_open(kNs, NVS_READWRITE, &h) == ESP_OK) {
|
|
1315
1280
|
reverted = nvs_u8(h, kKeyORevert, 0) != 0;
|
|
1316
|
-
nvs_str(h, kKeyOInst, installed,
|
|
1317
|
-
nvs_str(h, kKeyORsn, reason,
|
|
1318
|
-
nvs_str(h, kKeyODetail, detail,
|
|
1281
|
+
if (installed && installed_len) nvs_str(h, kKeyOInst, installed, installed_len);
|
|
1282
|
+
if (reason && reason_len) nvs_str(h, kKeyORsn, reason, reason_len);
|
|
1283
|
+
if (detail && detail_len) nvs_str(h, kKeyODetail, detail, detail_len);
|
|
1319
1284
|
nvs_erase_key(h, kKeyOInst);
|
|
1320
1285
|
nvs_erase_key(h, kKeyORevert);
|
|
1321
1286
|
nvs_erase_key(h, kKeyORsn);
|
|
@@ -1323,6 +1288,125 @@ JSValue js_reconcile(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
|
1323
1288
|
nvs_commit(h);
|
|
1324
1289
|
nvs_close(h);
|
|
1325
1290
|
}
|
|
1291
|
+
if (out_reverted) *out_reverted = reverted;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// ── JS bindings ──────────────────────────────────────────────────────────────
|
|
1295
|
+
// Argument coercion and result shaping only; the work is in the cores above.
|
|
1296
|
+
|
|
1297
|
+
JSValue core_err_obj(JSContext* ctx, const CoreErr& e) {
|
|
1298
|
+
return err_obj_kind(ctx, e.msg ? e.msg : "ota error", e.kind);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// stageBegin(checksum, size) -> { ok, resumeOffset } | { ok:false, error }
|
|
1302
|
+
JSValue js_stage_begin(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
1303
|
+
if (argc < 2) return err_obj(ctx, "missing checksum/size");
|
|
1304
|
+
const char* checksum = JS_ToCString(ctx, argv[0]);
|
|
1305
|
+
if (!checksum) {
|
|
1306
|
+
// JS_ToCString throws on a value with a throwing toString. Clear it
|
|
1307
|
+
// rather than strand it on the context for some unrelated later
|
|
1308
|
+
// operation to surface, the same way the size path below does.
|
|
1309
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
1310
|
+
return err_obj(ctx, "checksum not a string");
|
|
1311
|
+
}
|
|
1312
|
+
int64_t size = 0;
|
|
1313
|
+
if (JS_ToInt64(ctx, &size, argv[1])) {
|
|
1314
|
+
JS_FreeCString(ctx, checksum);
|
|
1315
|
+
// JS_ToInt64 throws on a value with a throwing valueOf. We report through
|
|
1316
|
+
// the result object, so clear it rather than strand it on the context.
|
|
1317
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
1318
|
+
return err_obj(ctx, "invalid size");
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
CoreErr e;
|
|
1322
|
+
int64_t resume = 0;
|
|
1323
|
+
bool ok = core_stage_begin(checksum, size, &resume, &e);
|
|
1324
|
+
JS_FreeCString(ctx, checksum);
|
|
1325
|
+
if (!ok) return core_err_obj(ctx, e);
|
|
1326
|
+
|
|
1327
|
+
JSValue o = ok_obj(ctx);
|
|
1328
|
+
JS_SetPropertyStr(ctx, o, "resumeOffset", JS_NewInt64(ctx, resume));
|
|
1329
|
+
return o;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// stageWrite(bytes) -> { ok } | { ok:false, error, kind? }
|
|
1333
|
+
JSValue js_stage_write(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
1334
|
+
if (argc < 1) return err_obj(ctx, "missing bytes");
|
|
1335
|
+
size_t len = 0;
|
|
1336
|
+
const uint8_t* data = JS_GetUint8Array(ctx, &len, argv[0]);
|
|
1337
|
+
if (!data) {
|
|
1338
|
+
JS_FreeValue(ctx, JS_GetException(ctx)); // JS_GetUint8Array throws; we report via err_obj
|
|
1339
|
+
return err_obj(ctx, "bytes not a Uint8Array");
|
|
1340
|
+
}
|
|
1341
|
+
CoreErr e;
|
|
1342
|
+
if (!core_stage_write(data, len, &e)) return core_err_obj(ctx, e);
|
|
1343
|
+
return ok_obj(ctx);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// stageFinish(trialBoots, requireConfirm, installNow)
|
|
1347
|
+
// -> { ok } | { ok:false, error, kind }
|
|
1348
|
+
// Verifies SHA-256 + size over the whole staged file, then either installs in
|
|
1349
|
+
// place now (installNow) or marks a verified build staged-for-install so the
|
|
1350
|
+
// next boot's reconcile installs it on a clean heap.
|
|
1351
|
+
JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
1352
|
+
int64_t trial_boots = 1;
|
|
1353
|
+
if (argc >= 1 && JS_ToInt64(ctx, &trial_boots, argv[0])) {
|
|
1354
|
+
// A throwing valueOf leaves trial_boots indeterminate and an exception
|
|
1355
|
+
// pending; we report through the result object, so clear it and default.
|
|
1356
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
1357
|
+
trial_boots = 1;
|
|
1358
|
+
}
|
|
1359
|
+
bool require_confirm = argc >= 2 && JS_ToBool(ctx, argv[1]);
|
|
1360
|
+
bool install_now = argc >= 3 && JS_ToBool(ctx, argv[2]);
|
|
1361
|
+
|
|
1362
|
+
CoreErr e;
|
|
1363
|
+
if (!core_stage_finish(trial_boots, require_confirm, install_now, &e)) {
|
|
1364
|
+
return core_err_obj(ctx, e);
|
|
1365
|
+
}
|
|
1366
|
+
return ok_obj(ctx);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// stageAbort() -> void
|
|
1370
|
+
JSValue js_stage_abort(JSContext*, JSValueConst, int, JSValueConst*) {
|
|
1371
|
+
core_stage_abort();
|
|
1372
|
+
return JS_UNDEFINED;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// markValid() -> void
|
|
1376
|
+
JSValue js_mark_valid(JSContext*, JSValueConst, int, JSValueConst*) {
|
|
1377
|
+
core_mark_valid();
|
|
1378
|
+
return JS_UNDEFINED;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// revert() -> { ok } | { ok:false, error }. Re-install the last-good build.
|
|
1382
|
+
JSValue js_revert(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
1383
|
+
CoreErr e;
|
|
1384
|
+
// Historically reported without a kind, and the policy maps every revert
|
|
1385
|
+
// failure to transient anyway.
|
|
1386
|
+
if (!core_revert(&e)) return err_obj(ctx, e.msg ? e.msg : "revert failed");
|
|
1387
|
+
return ok_obj(ctx);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// running() -> { checksum?, trial }
|
|
1391
|
+
JSValue js_running(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
1392
|
+
JSValue o = JS_NewObject(ctx);
|
|
1393
|
+
char chk[96] = {0};
|
|
1394
|
+
bool trial = false;
|
|
1395
|
+
core_running(chk, sizeof(chk), &trial);
|
|
1396
|
+
if (chk[0]) JS_SetPropertyStr(ctx, o, "checksum", JS_NewString(ctx, chk));
|
|
1397
|
+
JS_SetPropertyStr(ctx, o, "trial", JS_NewBool(ctx, trial));
|
|
1398
|
+
return o;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// reconcile() -> { installed?, reverted, diagnostic? }
|
|
1402
|
+
JSValue js_reconcile(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
1403
|
+
JSValue o = JS_NewObject(ctx);
|
|
1404
|
+
char installed[96] = {0};
|
|
1405
|
+
char reason[96] = {0};
|
|
1406
|
+
char detail[160] = {0};
|
|
1407
|
+
bool reverted = false;
|
|
1408
|
+
core_reconcile(installed, sizeof(installed), &reverted, reason, sizeof(reason), detail,
|
|
1409
|
+
sizeof(detail));
|
|
1326
1410
|
|
|
1327
1411
|
if (installed[0]) JS_SetPropertyStr(ctx, o, "installed", JS_NewString(ctx, installed));
|
|
1328
1412
|
JS_SetPropertyStr(ctx, o, "reverted", JS_NewBool(ctx, reverted));
|
|
@@ -1335,6 +1419,109 @@ JSValue js_reconcile(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
|
1335
1419
|
return o;
|
|
1336
1420
|
}
|
|
1337
1421
|
|
|
1422
|
+
// ── MIKOtaEnv install ops ────────────────────────────────────────────────────
|
|
1423
|
+
// The native OTA client reaches the staging machinery through these. They are
|
|
1424
|
+
// the same cores the JS bindings above call, so nothing can drift between the
|
|
1425
|
+
// two implementations while both are in the tree.
|
|
1426
|
+
|
|
1427
|
+
namespace {
|
|
1428
|
+
|
|
1429
|
+
int env_err_kind(OtaErr kind) {
|
|
1430
|
+
switch (kind) {
|
|
1431
|
+
case OtaErr::kCorrupt:
|
|
1432
|
+
return MIK_OTA_ERR_CORRUPT;
|
|
1433
|
+
case OtaErr::kOom:
|
|
1434
|
+
return MIK_OTA_ERR_OOM;
|
|
1435
|
+
case OtaErr::kTransient:
|
|
1436
|
+
break;
|
|
1437
|
+
}
|
|
1438
|
+
return MIK_OTA_ERR_TRANSIENT;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
void env_copy_err(const CoreErr& e, char* err_buf, size_t err_len) {
|
|
1442
|
+
if (err_buf && err_len) snprintf(err_buf, err_len, "%s", e.msg ? e.msg : "ota error");
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
bool env_stage_begin(void*, const char* checksum, size_t size, size_t* out_resume, char* err_buf,
|
|
1446
|
+
size_t err_len) {
|
|
1447
|
+
CoreErr e;
|
|
1448
|
+
int64_t resume = 0;
|
|
1449
|
+
if (!core_stage_begin(checksum, (int64_t)size, &resume, &e)) {
|
|
1450
|
+
env_copy_err(e, err_buf, err_len);
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
if (out_resume) *out_resume = (size_t)resume;
|
|
1454
|
+
return true;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
bool env_stage_write(void*, const uint8_t* data, size_t len, char* err_buf, size_t err_len) {
|
|
1458
|
+
CoreErr e;
|
|
1459
|
+
if (!core_stage_write(data, len, &e)) {
|
|
1460
|
+
env_copy_err(e, err_buf, err_len);
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
return true;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
bool env_stage_finish(void*, int trial_boots, bool require_confirm, bool install_now, char* err_buf,
|
|
1467
|
+
size_t err_len, int* out_err_kind) {
|
|
1468
|
+
CoreErr e;
|
|
1469
|
+
if (!core_stage_finish(trial_boots, require_confirm, install_now, &e)) {
|
|
1470
|
+
env_copy_err(e, err_buf, err_len);
|
|
1471
|
+
if (out_err_kind) *out_err_kind = env_err_kind(e.kind);
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
void env_stage_abort(void*) { core_stage_abort(); }
|
|
1478
|
+
void env_mark_valid(void*) { core_mark_valid(); }
|
|
1479
|
+
|
|
1480
|
+
bool env_revert(void*, char* err_buf, size_t err_len) {
|
|
1481
|
+
CoreErr e;
|
|
1482
|
+
if (!core_revert(&e)) {
|
|
1483
|
+
env_copy_err(e, err_buf, err_len);
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
return true;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
bool env_running(void*, MIKOtaRunningBuild* out) {
|
|
1490
|
+
if (!out) return false;
|
|
1491
|
+
*out = {};
|
|
1492
|
+
char chk[96] = {0};
|
|
1493
|
+
bool trial = false;
|
|
1494
|
+
core_running(chk, sizeof(chk), &trial);
|
|
1495
|
+
// The checksum field is a 64-hex digest plus its terminator; a longer value
|
|
1496
|
+
// could only come from corrupted NVS, and truncating it is what the JS path
|
|
1497
|
+
// effectively did too.
|
|
1498
|
+
snprintf(out->checksum, sizeof(out->checksum), "%s", chk);
|
|
1499
|
+
out->trial = trial;
|
|
1500
|
+
// version is left empty on purpose: the policy fills it from the app's
|
|
1501
|
+
// package.json through read_app_version.
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
void env_reconcile(void*, MIKOtaReconcileOutcome* out) {
|
|
1506
|
+
if (!out) return;
|
|
1507
|
+
*out = {};
|
|
1508
|
+
char installed[96] = {0};
|
|
1509
|
+
char reason[96] = {0};
|
|
1510
|
+
char detail[160] = {0};
|
|
1511
|
+
bool reverted = false;
|
|
1512
|
+
core_reconcile(installed, sizeof(installed), &reverted, reason, sizeof(reason), detail,
|
|
1513
|
+
sizeof(detail));
|
|
1514
|
+
snprintf(out->installed, sizeof(out->installed), "%s", installed);
|
|
1515
|
+
out->reverted = reverted;
|
|
1516
|
+
if (reason[0]) {
|
|
1517
|
+
out->has_diagnostic = true;
|
|
1518
|
+
snprintf(out->diagnostic.reason, sizeof(out->diagnostic.reason), "%s", reason);
|
|
1519
|
+
snprintf(out->diagnostic.detail, sizeof(out->diagnostic.detail), "%s", detail);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
} // namespace
|
|
1524
|
+
|
|
1338
1525
|
// ── module registration ──────────────────────────────────────────────────────
|
|
1339
1526
|
int mik__ota_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
1340
1527
|
JS_SetModuleExport(ctx, m, "stageBegin",
|
|
@@ -1482,6 +1669,18 @@ void adopt_fail(nvs_handle_t h, const char* chk, const char* reason, const char*
|
|
|
1482
1669
|
|
|
1483
1670
|
} // namespace
|
|
1484
1671
|
|
|
1672
|
+
void mik__ota_fill_install_ops(MIKOtaEnv* env) {
|
|
1673
|
+
if (!env) return;
|
|
1674
|
+
env->stage_begin = env_stage_begin;
|
|
1675
|
+
env->stage_write = env_stage_write;
|
|
1676
|
+
env->stage_finish = env_stage_finish;
|
|
1677
|
+
env->stage_abort = env_stage_abort;
|
|
1678
|
+
env->mark_valid = env_mark_valid;
|
|
1679
|
+
env->revert = env_revert;
|
|
1680
|
+
env->running = env_running;
|
|
1681
|
+
env->reconcile = env_reconcile;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1485
1684
|
// Stage a streamed .tgz for adopt install at the next boot (the cable-deploy
|
|
1486
1685
|
// path, distinct from an OTA download which runs a trial). The install itself
|
|
1487
1686
|
// runs from mik__ota_boot_reconcile() on a clean heap: doing it here, with the
|