@mikrojs/native 0.20.1 → 0.20.2-next.20260907205905
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/include/mikrojs/mikrojs.h +3 -1
- package/include/mikrojs/ota_env.h +8 -6
- package/include/mikrojs/ota_policy.h +16 -0
- package/package.json +9 -9
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/kv/shared.ts +13 -11
- package/src/mik_abort.cpp +1 -2
- package/src/mik_console.cpp +192 -115
- package/src/mik_inspect.cpp +86 -9
- package/src/mik_ota_client.cpp +5 -2
- package/src/mik_ota_policy.cpp +65 -18
- package/src/mikrojs.cpp +26 -13
- package/src/modules.cpp +6 -6
|
@@ -93,7 +93,9 @@ void MIK_FreeRuntime(MIKRuntime* mik_rt);
|
|
|
93
93
|
* -EINVAL runtime or entry is null/empty
|
|
94
94
|
* -ENOENT entry (and its .bjs variant) not on disk
|
|
95
95
|
* -EFAULT module evaluation threw
|
|
96
|
-
*
|
|
96
|
+
* A synchronous throw (a missing import, a syntax error) is reported like an
|
|
97
|
+
* uncaught exception found by MIK_Loop: error handler, dump, MIK_Stop. A
|
|
98
|
+
* rejected eval promise is reported by the next loop's rejection flush. */
|
|
97
99
|
int MIK_RunEntry(MIKRuntime* mik_rt, const char* entry);
|
|
98
100
|
|
|
99
101
|
/* MIK_RunEntry variant that, on -EFAULT, copies the thrown exception's
|
|
@@ -146,13 +146,15 @@ struct MIKOtaEnv {
|
|
|
146
146
|
MIKOtaKvStatus (*kv_get_blob)(void* opaque, const char* key, uint8_t* out_buf,
|
|
147
147
|
size_t* inout_len);
|
|
148
148
|
bool (*kv_set_blob)(void* opaque, const char* key, const uint8_t* data, size_t len);
|
|
149
|
-
/*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
149
|
+
/* Same contract as kv_get_blob: absent and failed are different answers.
|
|
150
|
+
* The retry budget lives behind these two, and every fallback a caller
|
|
151
|
+
* would pick for "absent" grants the device more than it has earned: an
|
|
152
|
+
* unspent budget, a url that looks new, no bad build on record. Stored
|
|
153
|
+
* bytes that do not decode report MIK_OTA_KV_ERROR too: garbage is not
|
|
154
|
+
* absence either. */
|
|
155
|
+
MIKOtaKvStatus (*kv_get_str)(void* opaque, const char* key, char* out_buf, size_t max_len);
|
|
154
156
|
bool (*kv_set_str)(void* opaque, const char* key, const char* val);
|
|
155
|
-
|
|
157
|
+
MIKOtaKvStatus (*kv_get_i32)(void* opaque, const char* key, int32_t* out_val);
|
|
156
158
|
bool (*kv_set_i32)(void* opaque, const char* key, int32_t val);
|
|
157
159
|
bool (*kv_remove)(void* opaque, const char* key);
|
|
158
160
|
|
|
@@ -92,8 +92,24 @@ public:
|
|
|
92
92
|
void SetDecline(const MIKOtaDeclineRecord& record);
|
|
93
93
|
void ClearDecline();
|
|
94
94
|
|
|
95
|
+
/* True once any read on this store has failed (as opposed to finding the
|
|
96
|
+
* key absent). Each getter has to answer with something, and the honest
|
|
97
|
+
* fallback for an unreadable key is the same everywhere: nothing stored.
|
|
98
|
+
* That reads as an unspent budget, a url that never matched, no abandoned
|
|
99
|
+
* build, every one of them more permission than the device has earned.
|
|
100
|
+
* Callers that are about to widen what the device may do consult this
|
|
101
|
+
* instead, and do nothing on an unreadable store; the next boot re-reads
|
|
102
|
+
* the real values. Latches for the life of one policy operation, which is
|
|
103
|
+
* how long a store lives. */
|
|
104
|
+
bool ReadFailed() const { return read_failed_; }
|
|
105
|
+
|
|
95
106
|
private:
|
|
107
|
+
/* Runs every read through one place so the latch cannot be forgotten. */
|
|
108
|
+
MIKOtaKvStatus ReadStr(const char* key, char* out, size_t max_len) const;
|
|
109
|
+
MIKOtaKvStatus ReadI32(const char* key, int32_t* out) const;
|
|
110
|
+
|
|
96
111
|
const MIKOtaEnv* env_;
|
|
112
|
+
mutable bool read_failed_ = false;
|
|
97
113
|
};
|
|
98
114
|
|
|
99
115
|
/* Validate untrusted offer fields into a well-formed MIKOtaOffer */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikrojs/native",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2-next.20260907205905",
|
|
4
4
|
"description": "Mikro.js C++ runtime library and Node.js native addon",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esp32",
|
|
@@ -84,18 +84,18 @@
|
|
|
84
84
|
"./runtime/wifi/types": "./runtime/wifi/types.ts"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
+
"@mikrojs/quickjs": "0.20.2-next.20260907205905+8b6f502",
|
|
87
88
|
"cmake-js": "^8.0.0",
|
|
88
|
-
"node-addon-api": "^8.
|
|
89
|
-
"node-gyp-build": "^4.8.4"
|
|
90
|
-
"@mikrojs/quickjs": "0.20.1"
|
|
89
|
+
"node-addon-api": "^8.9.2",
|
|
90
|
+
"node-gyp-build": "^4.8.4"
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
|
-
"@
|
|
93
|
+
"@mikrojs/registry": "0.20.2-next.20260907205905+8b6f502",
|
|
94
|
+
"@mikrojs/schema": "0.20.2-next.20260907205905+8b6f502",
|
|
95
|
+
"@swc/core": "^1.16.2",
|
|
94
96
|
"@types/node": "^24.12.2",
|
|
95
|
-
"esbuild": "^0.28.
|
|
96
|
-
"terser": "^5.
|
|
97
|
-
"@mikrojs/registry": "0.20.1",
|
|
98
|
-
"@mikrojs/schema": "0.20.1"
|
|
97
|
+
"esbuild": "^0.28.2",
|
|
98
|
+
"terser": "^5.51.2"
|
|
99
99
|
},
|
|
100
100
|
"engines": {
|
|
101
101
|
"node": ">=24.0.0"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/runtime/kv/shared.ts
CHANGED
|
@@ -58,18 +58,12 @@ export function makeCreateValue(native: NativeKvFns) {
|
|
|
58
58
|
const onReadError = options?.onReadError ?? (() => undefined)
|
|
59
59
|
|
|
60
60
|
function doGet(): unknown {
|
|
61
|
+
let v: unknown
|
|
62
|
+
// Only the native read is guarded: a throw from parse() or from the
|
|
63
|
+
// handler itself is a caller bug, and treating it as corruption would
|
|
64
|
+
// delete data that read back fine.
|
|
61
65
|
try {
|
|
62
|
-
|
|
63
|
-
if (v === undefined) return hasInitialValue ? initialValue : undefined
|
|
64
|
-
if (schema) {
|
|
65
|
-
const result = parse(schema, v)
|
|
66
|
-
if (!result.ok) {
|
|
67
|
-
// Schema mismatch: data decoded fine, don't delete it
|
|
68
|
-
return onReadError(result.error)
|
|
69
|
-
}
|
|
70
|
-
return result.value
|
|
71
|
-
}
|
|
72
|
-
return v
|
|
66
|
+
v = native.get(key)
|
|
73
67
|
} catch (e) {
|
|
74
68
|
// TypeError is the native's corruption marker (stored bytes that do
|
|
75
69
|
// not decode): delete and heal. Anything else is a transient read
|
|
@@ -83,6 +77,14 @@ export function makeCreateValue(native: NativeKvFns) {
|
|
|
83
77
|
}
|
|
84
78
|
return onReadError(e)
|
|
85
79
|
}
|
|
80
|
+
if (v === undefined) return hasInitialValue ? initialValue : undefined
|
|
81
|
+
if (schema) {
|
|
82
|
+
const result = parse(schema, v)
|
|
83
|
+
// Schema mismatch: data decoded fine, don't delete it
|
|
84
|
+
if (!result.ok) return onReadError(result.error)
|
|
85
|
+
return result.value
|
|
86
|
+
}
|
|
87
|
+
return v
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
function doSet(value: unknown) {
|
package/src/mik_abort.cpp
CHANGED
|
@@ -52,8 +52,7 @@ static JSValue mik__abort_lazy_get(JSContext* ctx, JSValue this_val, int magic)
|
|
|
52
52
|
* would surface a bare 'null'. Mirrors the module loader's
|
|
53
53
|
* missing-builtin fallback. */
|
|
54
54
|
if (!JS_HasException(ctx)) {
|
|
55
|
-
|
|
56
|
-
"Builtin module 'mikro/abort' is not available in this build");
|
|
55
|
+
JS_ThrowTypeError(ctx, "Failed to resolve module specifier 'mikro/abort'");
|
|
57
56
|
}
|
|
58
57
|
JS_FreeValue(ctx, global_obj);
|
|
59
58
|
return JS_EXCEPTION;
|
package/src/mik_console.cpp
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
#include <cstring>
|
|
4
4
|
#include <string>
|
|
5
5
|
#include <unistd.h>
|
|
6
|
-
#include <vector>
|
|
7
6
|
|
|
8
7
|
#include <quickjs.h>
|
|
9
8
|
|
|
@@ -122,41 +121,6 @@ static std::string mik__format(JSContext* ctx, int argc, JSValue* argv, bool col
|
|
|
122
121
|
return result;
|
|
123
122
|
}
|
|
124
123
|
|
|
125
|
-
/* ── Error formatting (port of console.ts formatError) ───────────── */
|
|
126
|
-
|
|
127
|
-
static std::string mik__format_error(JSContext* ctx, JSValue error) {
|
|
128
|
-
JSValue stack_val = JS_GetPropertyStr(ctx, error, "stack");
|
|
129
|
-
JSValue msg_val = JS_GetPropertyStr(ctx, error, "message");
|
|
130
|
-
|
|
131
|
-
const char* stack = JS_ToCString(ctx, stack_val);
|
|
132
|
-
const char* msg = JS_ToCString(ctx, msg_val);
|
|
133
|
-
|
|
134
|
-
std::string result;
|
|
135
|
-
if (stack && msg && strstr(stack, msg)) {
|
|
136
|
-
/* Stack already includes message */
|
|
137
|
-
result = stack;
|
|
138
|
-
} else {
|
|
139
|
-
JSValue name_val = JS_GetPropertyStr(ctx, error, "name");
|
|
140
|
-
const char* name = JS_ToCString(ctx, name_val);
|
|
141
|
-
result = name ? name : "Error";
|
|
142
|
-
result += ": ";
|
|
143
|
-
result += msg ? msg : "";
|
|
144
|
-
if (stack) {
|
|
145
|
-
result += "\n";
|
|
146
|
-
result += stack;
|
|
147
|
-
}
|
|
148
|
-
if (name) JS_FreeCString(ctx, name);
|
|
149
|
-
JS_FreeValue(ctx, name_val);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (stack) JS_FreeCString(ctx, stack);
|
|
153
|
-
if (msg) JS_FreeCString(ctx, msg);
|
|
154
|
-
JS_FreeValue(ctx, stack_val);
|
|
155
|
-
JS_FreeValue(ctx, msg_val);
|
|
156
|
-
|
|
157
|
-
return result;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
124
|
/* ── Console methods ─────────────────────────────────────────────── */
|
|
161
125
|
|
|
162
126
|
static JSValue mik__console_log(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
@@ -205,24 +169,9 @@ static JSValue mik__console_error_warn(JSContext* ctx, JSValue this_val, int arg
|
|
|
205
169
|
int magic) {
|
|
206
170
|
uint8_t msg_type = magic == 0 ? MIK_MSG_ERROR : MIK_MSG_WARN;
|
|
207
171
|
|
|
208
|
-
/*
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
for (int i = 0; i < argc; i++) {
|
|
213
|
-
if (JS_IsError(argv[i])) {
|
|
214
|
-
std::string err_str = mik__format_error(ctx, argv[i]);
|
|
215
|
-
mapped.push_back(JS_NewString(ctx, err_str.c_str()));
|
|
216
|
-
} else {
|
|
217
|
-
mapped.push_back(JS_DupValue(ctx, argv[i]));
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
std::string output = mik__format(ctx, argc, mapped.data(), true);
|
|
222
|
-
|
|
223
|
-
for (auto& v : mapped) {
|
|
224
|
-
JS_FreeValue(ctx, v);
|
|
225
|
-
}
|
|
172
|
+
/* Error arguments render through inspect: name, message, extra fields,
|
|
173
|
+
* stack and cause chain. Same output as console.log for the same value. */
|
|
174
|
+
std::string output = mik__format(ctx, argc, argv, true);
|
|
226
175
|
|
|
227
176
|
if (mik__repl_is_protocol_mode()) {
|
|
228
177
|
mik__repl_proto_send_output(msg_type, output.c_str(), output.size());
|
|
@@ -236,6 +185,152 @@ static JSValue mik__console_error_warn(JSContext* ctx, JSValue this_val, int arg
|
|
|
236
185
|
|
|
237
186
|
/* ── Uncaught error reporting ─────────────────────────────────────── */
|
|
238
187
|
|
|
188
|
+
/* Bounded stack-buffer writer for the uncaught path: never touches the
|
|
189
|
+
* C++ heap (see mik__report_uncaught). Silently truncates when full. */
|
|
190
|
+
struct MIKBoundedBuf {
|
|
191
|
+
char* buf;
|
|
192
|
+
size_t cap;
|
|
193
|
+
size_t pos = 0;
|
|
194
|
+
void append(const char* s, size_t len) {
|
|
195
|
+
if (pos >= cap - 1) return;
|
|
196
|
+
size_t avail = cap - 1 - pos;
|
|
197
|
+
size_t n = len < avail ? len : avail;
|
|
198
|
+
memcpy(buf + pos, s, n);
|
|
199
|
+
pos += n;
|
|
200
|
+
}
|
|
201
|
+
void cstr(const char* s) {
|
|
202
|
+
if (s) append(s, strlen(s));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/* One primitive as a log token (strings quoted). Objects print as
|
|
207
|
+
* [object]: nested inspection would need the C++ heap. */
|
|
208
|
+
static void mik__append_primitive(JSContext* ctx, JSValue v, MIKBoundedBuf& out) {
|
|
209
|
+
if (JS_IsObject(v)) {
|
|
210
|
+
out.cstr("[object]");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
/* Allocation-free spellings first: a null rejection is how QuickJS
|
|
214
|
+
* reports OOM, so this must work with no heap at all */
|
|
215
|
+
if (JS_IsNull(v)) {
|
|
216
|
+
out.cstr("null");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (JS_IsUndefined(v)) {
|
|
220
|
+
out.cstr("undefined");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (JS_IsBool(v)) {
|
|
224
|
+
out.cstr(JS_ToBool(ctx, v) ? "true" : "false");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (JS_VALUE_GET_TAG(v) == JS_TAG_INT) {
|
|
228
|
+
char num[16];
|
|
229
|
+
int n = snprintf(num, sizeof(num), "%d", JS_VALUE_GET_INT(v));
|
|
230
|
+
if (n > 0) out.append(num, (size_t)n);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
bool quote = JS_IsString(v);
|
|
234
|
+
/* JS-heap allocation only; returns NULL on OOM instead of throwing */
|
|
235
|
+
const char* s = JS_ToCString(ctx, v);
|
|
236
|
+
if (!s) {
|
|
237
|
+
/* Symbols reject ToString */
|
|
238
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
239
|
+
out.cstr("[primitive]");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (quote) out.cstr("'");
|
|
243
|
+
out.cstr(s);
|
|
244
|
+
if (quote) out.cstr("'");
|
|
245
|
+
JS_FreeCString(ctx, s);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* "Name: message { extra: fields }" then the stack frames, `indent`
|
|
249
|
+
* prefixed to every frame line. Also fits a plain object (a Result error
|
|
250
|
+
* as a panic cause has name/message and no stack). */
|
|
251
|
+
static void mik__append_error_head(JSContext* ctx, JSValue exc, const char* indent, MIKBoundedBuf& out) {
|
|
252
|
+
JSValue name_val = JS_GetPropertyStr(ctx, exc, "name");
|
|
253
|
+
JSValue msg_val = JS_GetPropertyStr(ctx, exc, "message");
|
|
254
|
+
if (JS_IsException(name_val) || JS_IsException(msg_val)) {
|
|
255
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
256
|
+
}
|
|
257
|
+
/* Non-string name/message count as absent (a native Result error has
|
|
258
|
+
* code + message and no name) */
|
|
259
|
+
const char* name = JS_IsString(name_val) ? JS_ToCString(ctx, name_val) : nullptr;
|
|
260
|
+
const char* emsg = JS_IsString(msg_val) ? JS_ToCString(ctx, msg_val) : nullptr;
|
|
261
|
+
|
|
262
|
+
if (name && name[0]) {
|
|
263
|
+
out.cstr(name);
|
|
264
|
+
if (emsg && emsg[0]) {
|
|
265
|
+
out.cstr(": ");
|
|
266
|
+
out.cstr(emsg);
|
|
267
|
+
}
|
|
268
|
+
} else if (emsg && emsg[0]) {
|
|
269
|
+
out.cstr(emsg);
|
|
270
|
+
} else {
|
|
271
|
+
out.cstr("[object]");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (name) JS_FreeCString(ctx, name);
|
|
275
|
+
if (emsg) JS_FreeCString(ctx, emsg);
|
|
276
|
+
JS_FreeValue(ctx, name_val);
|
|
277
|
+
JS_FreeValue(ctx, msg_val);
|
|
278
|
+
|
|
279
|
+
/* Own enumerable fields beyond the standard ones: code, errno, path... */
|
|
280
|
+
JSPropertyEnum* ptab = nullptr;
|
|
281
|
+
uint32_t plen = 0;
|
|
282
|
+
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, exc, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) ==
|
|
283
|
+
0) {
|
|
284
|
+
bool first = true;
|
|
285
|
+
for (uint32_t i = 0; i < plen; i++) {
|
|
286
|
+
const char* key = JS_AtomToCString(ctx, ptab[i].atom);
|
|
287
|
+
if (key && strcmp(key, "name") != 0 && strcmp(key, "message") != 0 &&
|
|
288
|
+
strcmp(key, "stack") != 0 && strcmp(key, "cause") != 0) {
|
|
289
|
+
JSValue v = JS_GetProperty(ctx, exc, ptab[i].atom);
|
|
290
|
+
if (JS_IsException(v)) {
|
|
291
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
292
|
+
} else {
|
|
293
|
+
out.cstr(first ? " { " : ", ");
|
|
294
|
+
first = false;
|
|
295
|
+
out.cstr(key);
|
|
296
|
+
out.cstr(": ");
|
|
297
|
+
mik__append_primitive(ctx, v, out);
|
|
298
|
+
}
|
|
299
|
+
JS_FreeValue(ctx, v);
|
|
300
|
+
}
|
|
301
|
+
if (key) JS_FreeCString(ctx, key);
|
|
302
|
+
JS_FreeAtom(ctx, ptab[i].atom);
|
|
303
|
+
}
|
|
304
|
+
if (!first) out.cstr(" }");
|
|
305
|
+
js_free(ctx, ptab);
|
|
306
|
+
} else {
|
|
307
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
JSValue stack_val = JS_GetPropertyStr(ctx, exc, "stack");
|
|
311
|
+
if (JS_IsString(stack_val)) {
|
|
312
|
+
const char* stack = JS_ToCString(ctx, stack_val);
|
|
313
|
+
if (stack) {
|
|
314
|
+
const char* p = stack;
|
|
315
|
+
while (*p) {
|
|
316
|
+
const char* nl = strchr(p, '\n');
|
|
317
|
+
size_t len = nl ? (size_t)(nl - p) : strlen(p);
|
|
318
|
+
if (len) {
|
|
319
|
+
out.cstr("\n");
|
|
320
|
+
out.cstr(indent);
|
|
321
|
+
out.append(p, len);
|
|
322
|
+
}
|
|
323
|
+
if (!nl) break;
|
|
324
|
+
p = nl + 1;
|
|
325
|
+
}
|
|
326
|
+
JS_FreeCString(ctx, stack);
|
|
327
|
+
}
|
|
328
|
+
} else if (JS_IsException(stack_val)) {
|
|
329
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
330
|
+
}
|
|
331
|
+
JS_FreeValue(ctx, stack_val);
|
|
332
|
+
}
|
|
333
|
+
|
|
239
334
|
/* Dedup across report paths: one error can reach this function more than
|
|
240
335
|
* once for a single failure. The sync throw path (mik_dump_error) can report
|
|
241
336
|
* an error whose promise still sits in the deferred flush queue, e.g. an
|
|
@@ -275,77 +370,59 @@ bool mik__report_uncaught(JSContext* ctx, JSValue exc, bool in_promise) {
|
|
|
275
370
|
* abort() — masking the real OOM the reporter was trying to surface.
|
|
276
371
|
* Messages longer than the buffer get truncated, which beats
|
|
277
372
|
* aborting. */
|
|
278
|
-
char buf[
|
|
279
|
-
|
|
280
|
-
auto append = [&](const char* s, size_t len) {
|
|
281
|
-
if (pos >= sizeof(buf) - 1) return;
|
|
282
|
-
size_t avail = sizeof(buf) - 1 - pos;
|
|
283
|
-
size_t n = len < avail ? len : avail;
|
|
284
|
-
memcpy(buf + pos, s, n);
|
|
285
|
-
pos += n;
|
|
286
|
-
};
|
|
287
|
-
auto append_cstr = [&](const char* s) {
|
|
288
|
-
if (s) append(s, strlen(s));
|
|
289
|
-
};
|
|
373
|
+
char buf[1024];
|
|
374
|
+
MIKBoundedBuf out{buf, sizeof(buf)};
|
|
290
375
|
|
|
291
|
-
|
|
376
|
+
out.cstr(in_promise ? "Uncaught (in promise) " : "Uncaught ");
|
|
292
377
|
|
|
293
378
|
if (JS_IsObject(exc)) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
379
|
+
mik__append_error_head(ctx, exc, "", out);
|
|
380
|
+
|
|
381
|
+
/* Cause chain: a panic's cause is the Result error that triggered
|
|
382
|
+
* it, so this is usually the line that says what actually failed.
|
|
383
|
+
* Bounded depth; `seen` cuts cycles. */
|
|
384
|
+
static const char* const indents[] = {"", " ", " ", " ", " "};
|
|
385
|
+
void* seen[countof(indents)] = {JS_VALUE_GET_PTR(exc)};
|
|
386
|
+
JSValue cur = JS_DupValue(ctx, exc);
|
|
387
|
+
for (size_t level = 1; level < countof(indents); level++) {
|
|
388
|
+
JSValue cause = JS_GetPropertyStr(ctx, cur, "cause");
|
|
389
|
+
JS_FreeValue(ctx, cur);
|
|
390
|
+
cur = cause;
|
|
391
|
+
if (JS_IsException(cause)) {
|
|
392
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
393
|
+
break;
|
|
304
394
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
append_cstr("\n");
|
|
321
|
-
append_cstr(stack);
|
|
395
|
+
if (JS_IsUndefined(cause)) break;
|
|
396
|
+
out.cstr("\n");
|
|
397
|
+
out.cstr(indents[level]);
|
|
398
|
+
out.cstr("[cause]: ");
|
|
399
|
+
if (!JS_IsObject(cause)) {
|
|
400
|
+
mik__append_primitive(ctx, cause, out);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
bool cyclic = false;
|
|
404
|
+
for (size_t j = 0; j < level; j++) {
|
|
405
|
+
if (seen[j] == JS_VALUE_GET_PTR(cause)) cyclic = true;
|
|
406
|
+
}
|
|
407
|
+
if (cyclic) {
|
|
408
|
+
out.cstr("[Circular]");
|
|
409
|
+
break;
|
|
322
410
|
}
|
|
323
|
-
|
|
411
|
+
seen[level] = JS_VALUE_GET_PTR(cause);
|
|
412
|
+
mik__append_error_head(ctx, cause, indents[level], out);
|
|
324
413
|
}
|
|
325
|
-
JS_FreeValue(ctx,
|
|
414
|
+
JS_FreeValue(ctx, cur);
|
|
326
415
|
} else {
|
|
327
416
|
/* Describe primitive rejections by type without going through
|
|
328
417
|
* mik_inspect (which allocates a std::string). */
|
|
329
|
-
if (
|
|
330
|
-
append_cstr("null");
|
|
331
|
-
} else if (JS_IsUndefined(exc)) {
|
|
332
|
-
append_cstr("undefined");
|
|
333
|
-
} else if (JS_IsBool(exc)) {
|
|
334
|
-
append_cstr(JS_ToBool(ctx, exc) ? "true" : "false");
|
|
335
|
-
} else if (JS_IsNumber(exc)) {
|
|
336
|
-
double d = 0;
|
|
337
|
-
JS_ToFloat64(ctx, &d, exc);
|
|
338
|
-
char num[32];
|
|
339
|
-
int n = snprintf(num, sizeof(num), "%g", d);
|
|
340
|
-
if (n > 0) append(num, (size_t)n);
|
|
341
|
-
} else if (JS_IsString(exc)) {
|
|
418
|
+
if (JS_IsString(exc)) {
|
|
342
419
|
const char* s = JS_ToCString(ctx, exc);
|
|
343
420
|
if (s) {
|
|
344
|
-
|
|
421
|
+
out.cstr(s);
|
|
345
422
|
JS_FreeCString(ctx, s);
|
|
346
423
|
}
|
|
347
424
|
} else {
|
|
348
|
-
|
|
425
|
+
mik__append_primitive(ctx, exc, out);
|
|
349
426
|
}
|
|
350
427
|
|
|
351
428
|
/* Primitive rejections lose the throw-site stack (the async
|
|
@@ -359,17 +436,17 @@ bool mik__report_uncaught(JSContext* ctx, JSValue exc, bool in_promise) {
|
|
|
359
436
|
int n = snprintf(note, sizeof(note),
|
|
360
437
|
"\n (primitive rejection, systemFree=%zuB at report time)",
|
|
361
438
|
plat->get_free_system_mem());
|
|
362
|
-
if (n > 0) append(note, (size_t)n);
|
|
439
|
+
if (n > 0) out.append(note, (size_t)n);
|
|
363
440
|
}
|
|
364
441
|
}
|
|
365
442
|
|
|
366
443
|
if (mik__repl_is_protocol_mode()) {
|
|
367
|
-
mik__repl_proto_send_output(MIK_MSG_ERROR, buf, pos);
|
|
444
|
+
mik__repl_proto_send_output(MIK_MSG_ERROR, buf, out.pos);
|
|
368
445
|
return true;
|
|
369
446
|
}
|
|
370
447
|
|
|
371
|
-
|
|
372
|
-
MIK_GetPlatform()->stderr_write(buf, pos);
|
|
448
|
+
out.cstr("\r\n");
|
|
449
|
+
MIK_GetPlatform()->stderr_write(buf, out.pos);
|
|
373
450
|
return true;
|
|
374
451
|
}
|
|
375
452
|
|
package/src/mik_inspect.cpp
CHANGED
|
@@ -18,6 +18,8 @@ struct InspectOpts {
|
|
|
18
18
|
bool colors;
|
|
19
19
|
bool show_hidden;
|
|
20
20
|
std::vector<void*> seen;
|
|
21
|
+
/* Nesting of [cause] rendering, capped like mik__report_uncaught */
|
|
22
|
+
int cause_level = 0;
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
/* ── Helpers ─────────────────────────────────────────────────────── */
|
|
@@ -85,6 +87,7 @@ static std::string quote_key(const char* key) {
|
|
|
85
87
|
/* ── Forward declaration ─────────────────────────────────────────── */
|
|
86
88
|
|
|
87
89
|
static std::string inspect_value(JSContext* ctx, JSValue value, InspectOpts& opts, int depth);
|
|
90
|
+
static std::string indent_continuation(const std::string& s);
|
|
88
91
|
|
|
89
92
|
/* ── Custom inspect callback state (single-threaded JS) ──────────── */
|
|
90
93
|
|
|
@@ -263,7 +266,7 @@ static std::string inspect_array_items(JSContext* ctx, JSValue arr, uint32_t len
|
|
|
263
266
|
if (JS_IsException(item)) {
|
|
264
267
|
out += getter_threw(ctx, opts);
|
|
265
268
|
} else {
|
|
266
|
-
out += inspect_value(ctx, item, opts, depth);
|
|
269
|
+
out += indent_continuation(inspect_value(ctx, item, opts, depth));
|
|
267
270
|
}
|
|
268
271
|
JS_FreeValue(ctx, item);
|
|
269
272
|
}
|
|
@@ -318,7 +321,7 @@ static std::string inspect_properties(JSContext* ctx, JSValue obj, InspectOpts&
|
|
|
318
321
|
if (JS_IsException(val)) {
|
|
319
322
|
out += getter_threw(ctx, opts);
|
|
320
323
|
} else {
|
|
321
|
-
out += inspect_value(ctx, val, opts, depth);
|
|
324
|
+
out += indent_continuation(inspect_value(ctx, val, opts, depth));
|
|
322
325
|
}
|
|
323
326
|
JS_FreeValue(ctx, val);
|
|
324
327
|
}
|
|
@@ -474,6 +477,51 @@ static std::string inspect_regexp(JSContext* ctx, JSValue value, InspectOpts& op
|
|
|
474
477
|
return stylize("/(?:)/", MIK_TOKEN_REGEXP, opts);
|
|
475
478
|
}
|
|
476
479
|
|
|
480
|
+
/* Indent every line after the first by two spaces so a multi-line value
|
|
481
|
+
* (an error's stack frames, its own cause) nests visibly under its parent. */
|
|
482
|
+
static std::string indent_continuation(const std::string& s) {
|
|
483
|
+
std::string out;
|
|
484
|
+
out.reserve(s.size() + 8);
|
|
485
|
+
for (char c : s) {
|
|
486
|
+
out += c;
|
|
487
|
+
if (c == '\n') out += " ";
|
|
488
|
+
}
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/* "\n [cause]: <value>" for an own cause, or "". Shared by Errors and
|
|
493
|
+
* plain objects, since Result errors carry cause as an ordinary field.
|
|
494
|
+
* The chain is what a log line exists for, so it renders past the depth
|
|
495
|
+
* limit; the caller keeps the parent on the seen list to bound cycles. */
|
|
496
|
+
static std::string inspect_cause(JSContext* ctx, JSValue value, InspectOpts& opts, int depth) {
|
|
497
|
+
JSValue cause = JS_GetPropertyStr(ctx, value, "cause");
|
|
498
|
+
if (JS_IsException(cause)) {
|
|
499
|
+
drain_exception(ctx);
|
|
500
|
+
return "";
|
|
501
|
+
}
|
|
502
|
+
/* Same cap as mik__report_uncaught; bounds recursion on the JS task stack */
|
|
503
|
+
static const int MAX_CAUSE_LEVEL = 4;
|
|
504
|
+
std::string out;
|
|
505
|
+
if (!JS_IsUndefined(cause)) {
|
|
506
|
+
out = "\n [cause]: ";
|
|
507
|
+
if (opts.cause_level >= MAX_CAUSE_LEVEL) {
|
|
508
|
+
out += TRUNCATOR;
|
|
509
|
+
} else {
|
|
510
|
+
opts.cause_level++;
|
|
511
|
+
out += indent_continuation(inspect_value(ctx, cause, opts, depth < 1 ? 1 : depth));
|
|
512
|
+
opts.cause_level--;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
JS_FreeValue(ctx, cause);
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* Errors render as
|
|
520
|
+
* Name: message { extra: props }
|
|
521
|
+
* at frame (file:line:col)
|
|
522
|
+
* [cause]: <inspected cause>
|
|
523
|
+
* The header keeps name, message and every own enumerable field (code,
|
|
524
|
+
* errno, ...) on one line; the stack and the cause chain follow. */
|
|
477
525
|
static std::string inspect_error(JSContext* ctx, JSValue value, InspectOpts& opts, int depth) {
|
|
478
526
|
/* Get name and message */
|
|
479
527
|
JSValue name_val = JS_GetPropertyStr(ctx, value, "name");
|
|
@@ -498,16 +546,40 @@ static std::string inspect_error(JSContext* ctx, JSValue value, InspectOpts& opt
|
|
|
498
546
|
JS_FreeValue(ctx, name_val);
|
|
499
547
|
JS_FreeValue(ctx, msg_val);
|
|
500
548
|
|
|
501
|
-
|
|
549
|
+
if (is_seen(opts, value)) return result + " [Circular]";
|
|
550
|
+
opts.seen.push_back(JS_VALUE_GET_PTR(value));
|
|
551
|
+
|
|
552
|
+
/* Inspect extra properties (skip standard error keys; cause is rendered
|
|
553
|
+
* below so it reads the same whether QuickJS or a panic defined it) */
|
|
502
554
|
static const char* const error_keys[] = {"stack", "line", "column", "name",
|
|
503
555
|
"message", "fileName", "lineNumber", "columnNumber",
|
|
504
|
-
"number", "description"};
|
|
556
|
+
"number", "description", "cause"};
|
|
505
557
|
std::string props =
|
|
506
558
|
inspect_properties(ctx, value, opts, depth, error_keys, countof(error_keys));
|
|
507
559
|
if (!props.empty()) {
|
|
508
560
|
result += " { " + props + " }";
|
|
509
561
|
}
|
|
510
562
|
|
|
563
|
+
/* QuickJS stacks are frame lines only (" at ..."), no header line */
|
|
564
|
+
JSValue stack_val = JS_GetPropertyStr(ctx, value, "stack");
|
|
565
|
+
if (JS_IsException(stack_val)) drain_exception(ctx);
|
|
566
|
+
if (JS_IsString(stack_val)) {
|
|
567
|
+
const char* stack = JS_ToCString(ctx, stack_val);
|
|
568
|
+
if (stack) {
|
|
569
|
+
std::string frames(stack);
|
|
570
|
+
JS_FreeCString(ctx, stack);
|
|
571
|
+
while (!frames.empty() && frames.back() == '\n') frames.pop_back();
|
|
572
|
+
if (!frames.empty()) {
|
|
573
|
+
result += "\n";
|
|
574
|
+
result += frames;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
JS_FreeValue(ctx, stack_val);
|
|
579
|
+
|
|
580
|
+
result += inspect_cause(ctx, value, opts, depth);
|
|
581
|
+
|
|
582
|
+
opts.seen.pop_back();
|
|
511
583
|
return result;
|
|
512
584
|
}
|
|
513
585
|
|
|
@@ -603,9 +675,9 @@ static std::string inspect_map(JSContext* ctx, JSValue value, InspectOpts& opts,
|
|
|
603
675
|
|
|
604
676
|
if (!first) items += ", ";
|
|
605
677
|
first = false;
|
|
606
|
-
items += inspect_value(ctx, key, opts, depth);
|
|
678
|
+
items += indent_continuation(inspect_value(ctx, key, opts, depth));
|
|
607
679
|
items += " => ";
|
|
608
|
-
items += inspect_value(ctx, val, opts, depth);
|
|
680
|
+
items += indent_continuation(inspect_value(ctx, val, opts, depth));
|
|
609
681
|
|
|
610
682
|
JS_FreeValue(ctx, key);
|
|
611
683
|
JS_FreeValue(ctx, val);
|
|
@@ -673,7 +745,7 @@ static std::string inspect_set(JSContext* ctx, JSValue value, InspectOpts& opts,
|
|
|
673
745
|
JSValue val = JS_GetPropertyStr(ctx, step, "value");
|
|
674
746
|
if (!first) items += ", ";
|
|
675
747
|
first = false;
|
|
676
|
-
items += inspect_value(ctx, val, opts, depth);
|
|
748
|
+
items += indent_continuation(inspect_value(ctx, val, opts, depth));
|
|
677
749
|
JS_FreeValue(ctx, val);
|
|
678
750
|
JS_FreeValue(ctx, step);
|
|
679
751
|
}
|
|
@@ -746,11 +818,16 @@ static std::string inspect_object(JSContext* ctx, JSValue value, InspectOpts& op
|
|
|
746
818
|
|
|
747
819
|
if (plen == 0) return "{}";
|
|
748
820
|
|
|
821
|
+
/* A cause field chains below the braces like an Error's, so a Result
|
|
822
|
+
* error wrapped around another keeps its whole chain readable */
|
|
823
|
+
static const char* const cause_key[] = {"cause"};
|
|
749
824
|
opts.seen.push_back(JS_VALUE_GET_PTR(value));
|
|
750
|
-
std::string props = inspect_properties(ctx, value, opts, depth);
|
|
825
|
+
std::string props = inspect_properties(ctx, value, opts, depth, cause_key, 1);
|
|
826
|
+
std::string result = props.empty() ? "{}" : "{ " + props + " }";
|
|
827
|
+
result += inspect_cause(ctx, value, opts, depth);
|
|
751
828
|
opts.seen.pop_back();
|
|
752
829
|
|
|
753
|
-
return
|
|
830
|
+
return result;
|
|
754
831
|
}
|
|
755
832
|
|
|
756
833
|
static std::string inspect_class(JSContext* ctx, JSValue value, const std::string& type_name,
|
package/src/mik_ota_client.cpp
CHANGED
|
@@ -354,10 +354,13 @@ bool MIKOtaClient::Enrollment(std::string* out_url, std::string* out_bearer) con
|
|
|
354
354
|
char url[256] = {};
|
|
355
355
|
char bearer[128] = {};
|
|
356
356
|
if (!env_ || !env_->kv_get_str) return false;
|
|
357
|
-
|
|
357
|
+
/* A read that failed reads as "not enrolled", which only makes the client
|
|
358
|
+
* skip the round. Erring the other way would send a half-read update key. */
|
|
359
|
+
if (env_->kv_get_str(env_->opaque, "ota.registry", url, sizeof(url)) != MIK_OTA_KV_OK ||
|
|
360
|
+
url[0] == '\0') {
|
|
358
361
|
return false;
|
|
359
362
|
}
|
|
360
|
-
if (
|
|
363
|
+
if (env_->kv_get_str(env_->opaque, "ota.updateKey", bearer, sizeof(bearer)) != MIK_OTA_KV_OK ||
|
|
361
364
|
bearer[0] == '\0') {
|
|
362
365
|
return false;
|
|
363
366
|
}
|
package/src/mik_ota_policy.cpp
CHANGED
|
@@ -13,6 +13,11 @@ namespace mikrojs {
|
|
|
13
13
|
/* Staging attempts for one url before a checksum is abandoned. */
|
|
14
14
|
constexpr int32_t MIK__OTA_MAX_TRIES = 3;
|
|
15
15
|
|
|
16
|
+
/* The contract is 64 lowercase hex (see decline(), which enforces the format).
|
|
17
|
+
* Only the length is checked here, because a shorter checksum round-trips
|
|
18
|
+
* fine and the registry is free to use one. */
|
|
19
|
+
constexpr size_t MIK__OTA_CHECKSUM_MAX = 64;
|
|
20
|
+
|
|
16
21
|
const char* mik__ota_outcome_to_str(MIKOtaApplyOutcome outcome) {
|
|
17
22
|
switch (outcome) {
|
|
18
23
|
case MIKOtaApplyOutcome::kStaged:
|
|
@@ -48,10 +53,23 @@ std::string url_digest(const std::string& url) {
|
|
|
48
53
|
|
|
49
54
|
} // namespace
|
|
50
55
|
|
|
56
|
+
MIKOtaKvStatus MIKOtaStore::ReadStr(const char* key, char* out, size_t max_len) const {
|
|
57
|
+
if (!env_ || !env_->kv_get_str) return MIK_OTA_KV_ABSENT;
|
|
58
|
+
MIKOtaKvStatus status = env_->kv_get_str(env_->opaque, key, out, max_len);
|
|
59
|
+
if (status == MIK_OTA_KV_ERROR) read_failed_ = true;
|
|
60
|
+
return status;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
MIKOtaKvStatus MIKOtaStore::ReadI32(const char* key, int32_t* out) const {
|
|
64
|
+
if (!env_ || !env_->kv_get_i32) return MIK_OTA_KV_ABSENT;
|
|
65
|
+
MIKOtaKvStatus status = env_->kv_get_i32(env_->opaque, key, out);
|
|
66
|
+
if (status == MIK_OTA_KV_ERROR) read_failed_ = true;
|
|
67
|
+
return status;
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
bool MIKOtaStore::UrlMatches(const std::string& url) const {
|
|
52
|
-
if (!env_ || !env_->kv_get_str) return false;
|
|
53
71
|
char buf[17] = {};
|
|
54
|
-
if (
|
|
72
|
+
if (ReadStr("ota.url", buf, sizeof(buf)) != MIK_OTA_KV_OK) return false;
|
|
55
73
|
return url_digest(url) == buf;
|
|
56
74
|
}
|
|
57
75
|
|
|
@@ -61,9 +79,8 @@ void MIKOtaStore::SetUrl(const std::string& url) {
|
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
std::string MIKOtaStore::GetAttempt() const {
|
|
64
|
-
if (!env_ || !env_->kv_get_str) return "";
|
|
65
82
|
char buf[128] = {};
|
|
66
|
-
if (
|
|
83
|
+
if (ReadStr("ota.att", buf, sizeof(buf)) == MIK_OTA_KV_OK) {
|
|
67
84
|
return buf;
|
|
68
85
|
}
|
|
69
86
|
return "";
|
|
@@ -75,12 +92,14 @@ void MIKOtaStore::SetAttempt(const std::string& checksum) {
|
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
int32_t MIKOtaStore::GetTries() const {
|
|
78
|
-
if (!env_ || !env_->kv_get_i32) return 0;
|
|
79
95
|
int32_t val = 0;
|
|
80
|
-
if (
|
|
96
|
+
if (ReadI32("ota.tries", &val) == MIK_OTA_KV_OK) {
|
|
81
97
|
return val;
|
|
82
98
|
}
|
|
83
|
-
|
|
99
|
+
/* An unreadable budget counts as spent, which is what stops the attempt in
|
|
100
|
+
* apply_begin. Defaulting to "fully available" is the one mistake here that
|
|
101
|
+
* ends in a build reinstalling itself forever. */
|
|
102
|
+
return read_failed_ ? MIK__OTA_MAX_TRIES : 0;
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
void MIKOtaStore::SetTries(int32_t n) {
|
|
@@ -89,9 +108,8 @@ void MIKOtaStore::SetTries(int32_t n) {
|
|
|
89
108
|
}
|
|
90
109
|
|
|
91
110
|
std::string MIKOtaStore::GetBad() const {
|
|
92
|
-
if (!env_ || !env_->kv_get_str) return "";
|
|
93
111
|
char buf[128] = {};
|
|
94
|
-
if (
|
|
112
|
+
if (ReadStr("ota.bad", buf, sizeof(buf)) == MIK_OTA_KV_OK) {
|
|
95
113
|
return buf;
|
|
96
114
|
}
|
|
97
115
|
return "";
|
|
@@ -103,9 +121,8 @@ void MIKOtaStore::SetBad(const std::string& checksum) {
|
|
|
103
121
|
}
|
|
104
122
|
|
|
105
123
|
bool MIKOtaStore::GetInFlight() const {
|
|
106
|
-
if (!env_ || !env_->kv_get_i32) return false;
|
|
107
124
|
int32_t val = 0;
|
|
108
|
-
if (
|
|
125
|
+
if (ReadI32("ota.inflight", &val) == MIK_OTA_KV_OK) {
|
|
109
126
|
return val == 1;
|
|
110
127
|
}
|
|
111
128
|
return false;
|
|
@@ -117,16 +134,15 @@ void MIKOtaStore::SetInFlight(bool in_flight) {
|
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
bool MIKOtaStore::GetDecline(MIKOtaDeclineRecord* out) const {
|
|
120
|
-
if (
|
|
121
|
-
if (!env_->kv_get_str(env_->opaque, "ota.declined", out->checksum, sizeof(out->checksum)) ||
|
|
137
|
+
if (ReadStr("ota.declined", out->checksum, sizeof(out->checksum)) != MIK_OTA_KV_OK ||
|
|
122
138
|
out->checksum[0] == '\0') {
|
|
123
139
|
return false;
|
|
124
140
|
}
|
|
125
|
-
if (
|
|
141
|
+
if (ReadStr("ota.declReason", out->reason, sizeof(out->reason)) != MIK_OTA_KV_OK ||
|
|
126
142
|
out->reason[0] == '\0') {
|
|
127
143
|
return false;
|
|
128
144
|
}
|
|
129
|
-
if (
|
|
145
|
+
if (ReadStr("ota.declDetail", out->detail, sizeof(out->detail)) != MIK_OTA_KV_OK) {
|
|
130
146
|
out->detail[0] = '\0';
|
|
131
147
|
}
|
|
132
148
|
return true;
|
|
@@ -181,6 +197,18 @@ bool mik__ota_parse_offer(const char* url, const char* checksum, int64_t size, b
|
|
|
181
197
|
return false;
|
|
182
198
|
}
|
|
183
199
|
|
|
200
|
+
/* ota.att and ota.bad read back through a 128-byte buffer, and a stored
|
|
201
|
+
* value too long to decode reports a failed read, not an absent one. That
|
|
202
|
+
* stops every later offer, including a good one, and nothing rewrites the
|
|
203
|
+
* key. Bound it here, at the one place untrusted registry fields become an
|
|
204
|
+
* offer, rather than letting an unreadable value into the store. */
|
|
205
|
+
if (strlen(checksum) > MIK__OTA_CHECKSUM_MAX) {
|
|
206
|
+
if (out_warn_reason) {
|
|
207
|
+
*out_warn_reason = "checksum too long";
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
184
212
|
if (size <= 0) {
|
|
185
213
|
if (out_warn_reason) {
|
|
186
214
|
*out_warn_reason = "invalid size";
|
|
@@ -202,7 +230,10 @@ MIKOtaReconcileOutcome mik__ota_policy_reconcile(const MIKOtaEnv* env) {
|
|
|
202
230
|
MIKOtaStore store(env);
|
|
203
231
|
if (store.GetInFlight()) {
|
|
204
232
|
store.SetInFlight(false);
|
|
205
|
-
} else {
|
|
233
|
+
} else if (!store.ReadFailed()) {
|
|
234
|
+
/* Only a boot we can prove was clean hands the budget back. An
|
|
235
|
+
* unreadable flag reads as "not in flight", and the attempt it would
|
|
236
|
+
* forget is the one that just took the device down. */
|
|
206
237
|
store.SetTries(0);
|
|
207
238
|
}
|
|
208
239
|
MIKOtaReconcileOutcome outcome = {};
|
|
@@ -345,7 +376,23 @@ bool mik__ota_policy_apply_begin(const MIKOtaEnv* env, const MIKOtaOffer& offer,
|
|
|
345
376
|
|
|
346
377
|
// (d) device has given up on this build
|
|
347
378
|
MIKOtaStore store(env);
|
|
348
|
-
|
|
379
|
+
std::string bad = store.GetBad();
|
|
380
|
+
bool url_matches = store.UrlMatches(offer.url);
|
|
381
|
+
std::string attempt = store.GetAttempt();
|
|
382
|
+
|
|
383
|
+
// Read first, then decide: every value above reads as absent when the store
|
|
384
|
+
// could not answer, and each of those defaults grants the attempt
|
|
385
|
+
// something: no abandoned build, a url that never matched (a fresh
|
|
386
|
+
// budget). A store this device cannot read is one it has no business
|
|
387
|
+
// re-keying, and the heap pressure behind the failure is no state to start
|
|
388
|
+
// a download in anyway. Nothing is written here, so the next boot decides
|
|
389
|
+
// on the real values.
|
|
390
|
+
if (store.ReadFailed()) {
|
|
391
|
+
if (out_outcome) *out_outcome = MIKOtaApplyOutcome::kExhausted;
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!offer.checksum.empty() && offer.checksum == bad) {
|
|
349
396
|
if (out_outcome) *out_outcome = MIKOtaApplyOutcome::kAbandoned;
|
|
350
397
|
return true;
|
|
351
398
|
}
|
|
@@ -354,7 +401,7 @@ bool mik__ota_policy_apply_begin(const MIKOtaEnv* env, const MIKOtaOffer& offer,
|
|
|
354
401
|
// attempts against this exact build at this exact url; the checksum is not
|
|
355
402
|
// abandoned, because everything counted here is transient by construction
|
|
356
403
|
// (a corrupt build is abandoned in apply_end instead).
|
|
357
|
-
if (!
|
|
404
|
+
if (!url_matches || offer.checksum != attempt) {
|
|
358
405
|
store.SetUrl(offer.url);
|
|
359
406
|
store.SetAttempt(offer.checksum);
|
|
360
407
|
store.SetTries(0);
|
package/src/mikrojs.cpp
CHANGED
|
@@ -1211,20 +1211,33 @@ int MIK_RunEntryErr(MIKRuntime* mik_rt, const char* entry, char* err_buf, size_t
|
|
|
1211
1211
|
rejected = true;
|
|
1212
1212
|
}
|
|
1213
1213
|
}
|
|
1214
|
-
if (failed
|
|
1215
|
-
/* Capture the failure's string form for the caller. Without an
|
|
1216
|
-
* err_buf the pending exception is intentionally left on ctx,
|
|
1217
|
-
* matching MIK_RunEntry's historical behavior. */
|
|
1214
|
+
if (failed) {
|
|
1218
1215
|
JSValue exc = rejected ? JS_PromiseResult(ctx, result) : JS_GetException(ctx);
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1216
|
+
if (err_buf && err_buf_size > 0) {
|
|
1217
|
+
/* Capture the failure's string form for the caller. */
|
|
1218
|
+
const char* msg = JS_ToCString(ctx, exc);
|
|
1219
|
+
if (msg) {
|
|
1220
|
+
snprintf(err_buf, err_buf_size, "%s", msg);
|
|
1221
|
+
JS_FreeCString(ctx, msg);
|
|
1222
|
+
} else {
|
|
1223
|
+
/* Stringifying the exception itself threw (e.g. OOM) — drop
|
|
1224
|
+
* the secondary exception so it can't leak into later evals. */
|
|
1225
|
+
JSValue stray = JS_GetException(ctx);
|
|
1226
|
+
JS_FreeValue(ctx, stray);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
/* A synchronous throw (a missing import, a syntax error) exists only
|
|
1230
|
+
* as the pending exception consumed above, so report it here the way
|
|
1231
|
+
* MIK_Loop reports one it finds pending: handler, dump, stop. Left
|
|
1232
|
+
* unreported, the device idles in silence. A rejected eval promise is
|
|
1233
|
+
* still queued for the loop's rejection flush, which reports it the
|
|
1234
|
+
* same way. */
|
|
1235
|
+
if (!rejected) {
|
|
1236
|
+
if (mik_rt->error_handler_fn) {
|
|
1237
|
+
mik_rt->error_handler_fn(ctx, exc, mik_rt->error_handler_opaque);
|
|
1238
|
+
}
|
|
1239
|
+
mik_dump_error1(ctx, exc);
|
|
1240
|
+
MIK_Stop(mik_rt);
|
|
1228
1241
|
}
|
|
1229
1242
|
JS_FreeValue(ctx, exc);
|
|
1230
1243
|
}
|
package/src/modules.cpp
CHANGED
|
@@ -234,8 +234,9 @@ static JSModuleDef* mik_module_loader_inner(JSContext* ctx, const char* module_n
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
/* Native module not found — this is a device-only module that isn't
|
|
237
|
-
* available in the current build (e.g. running on desktop).
|
|
238
|
-
|
|
237
|
+
* available in the current build (e.g. running on desktop). Same
|
|
238
|
+
* wording as every other unresolvable specifier (and browsers). */
|
|
239
|
+
JS_ThrowTypeError(ctx, "Failed to resolve module specifier '%s'", module_name);
|
|
239
240
|
return nullptr;
|
|
240
241
|
}
|
|
241
242
|
|
|
@@ -265,8 +266,7 @@ static JSModuleDef* mik_module_loader_inner(JSContext* ctx, const char* module_n
|
|
|
265
266
|
* returns JS_UNINITIALIZED from an empty exception slot, and
|
|
266
267
|
* re-throwing that surfaced as a bare "[uninitialized]" error. */
|
|
267
268
|
if (!JS_HasException(ctx)) {
|
|
268
|
-
|
|
269
|
-
module_name);
|
|
269
|
+
JS_ThrowTypeError(ctx, "Failed to resolve module specifier '%s'", module_name);
|
|
270
270
|
}
|
|
271
271
|
return NULL;
|
|
272
272
|
}
|
|
@@ -996,7 +996,7 @@ JSValue mik__load_module_ns(JSContext* ctx, const char* importer, const char* sp
|
|
|
996
996
|
JSValue p = JS_LoadModule(ctx, importer, specifier);
|
|
997
997
|
if (JS_IsException(p)) {
|
|
998
998
|
/* Propagate the loader's real error (e.g. a syntax error in a
|
|
999
|
-
* virtual stub), not a generic
|
|
999
|
+
* virtual stub), not a generic resolution failure. */
|
|
1000
1000
|
JS_FreeAtom(ctx, name);
|
|
1001
1001
|
return p;
|
|
1002
1002
|
}
|
|
@@ -1016,7 +1016,7 @@ JSValue mik__load_module_ns(JSContext* ctx, const char* importer, const char* sp
|
|
|
1016
1016
|
}
|
|
1017
1017
|
JS_FreeAtom(ctx, name);
|
|
1018
1018
|
if (!m) {
|
|
1019
|
-
return
|
|
1019
|
+
return JS_ThrowTypeError(ctx, "Failed to resolve module specifier '%s'", specifier);
|
|
1020
1020
|
}
|
|
1021
1021
|
/* 5 == EVALUATED; quickjs.h documents the numbering but exports no enum. */
|
|
1022
1022
|
if (require_evaluated && JS_GetModuleStatus(ctx, m) != 5) {
|