@mikrojs/native 0.11.0 → 0.12.0-next.7.g0a0155c
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/package.json +3 -2
- 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/http/server-impl.ts +308 -0
- package/runtime/http/server.ts +30 -0
- package/runtime/internal.d.ts +32 -0
- package/src/mikrojs.cpp +16 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikrojs/native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0-next.7.g0a0155c",
|
|
4
4
|
"description": "Mikro.js C++ runtime library and Node.js native addon",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esp32",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"./runtime/env/types": "./runtime/env/types.ts",
|
|
51
51
|
"./runtime/format/types": "./runtime/format/types.ts",
|
|
52
52
|
"./runtime/http/helpers": "./runtime/http/helpers.ts",
|
|
53
|
+
"./runtime/http/server-impl": "./runtime/http/server-impl.ts",
|
|
53
54
|
"./runtime/fs/types": "./runtime/fs/types.ts",
|
|
54
55
|
"./runtime/i2c/types": "./runtime/i2c/types.ts",
|
|
55
56
|
"./runtime/inspect/types": "./runtime/inspect/types.ts",
|
|
@@ -79,7 +80,7 @@
|
|
|
79
80
|
"cmake-js": "^8.0.0",
|
|
80
81
|
"node-addon-api": "^8.7.0",
|
|
81
82
|
"node-gyp-build": "^4.8.4",
|
|
82
|
-
"@mikrojs/quickjs": "0.
|
|
83
|
+
"@mikrojs/quickjs": "0.12.0-next.7.g0a0155c"
|
|
83
84
|
},
|
|
84
85
|
"devDependencies": {
|
|
85
86
|
"@swc/core": "^1.15.30",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// Internal: native:http_server-backed server implementation. Bundled into
|
|
2
|
+
// http/server.js; not exposed as its own subpath. Exists as a seam so host
|
|
3
|
+
// tests can inject a fake native module (mirrors http/native.ts for the client).
|
|
4
|
+
|
|
5
|
+
import {makeResponse, RequestError} from 'mikro/http/helpers'
|
|
6
|
+
import {err, ok} from 'mikro/result'
|
|
7
|
+
|
|
8
|
+
import type {Result} from '../result/types.js'
|
|
9
|
+
|
|
10
|
+
/* Error returned by listen(). The native layer produces these via
|
|
11
|
+
* mik__result_err_*, so the variant names must match mik_http_server.cpp. */
|
|
12
|
+
export const ServerError = {
|
|
13
|
+
/** A server is already listening; close() before listening again. */
|
|
14
|
+
AlreadyListening: () => ({name: 'AlreadyListening'}) as const,
|
|
15
|
+
OutOfMemory: (message: string) => ({name: 'OutOfMemory', message}) as const,
|
|
16
|
+
/** httpd_start failed (e.g. port unavailable). Message carries the esp_err. */
|
|
17
|
+
StartFailed: (message: string) => ({name: 'StartFailed', message}) as const,
|
|
18
|
+
}
|
|
19
|
+
export type ServerError = ReturnType<(typeof ServerError)[keyof typeof ServerError]>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Lazy, non-enumerable request header accessor. esp_http_server cannot list a
|
|
23
|
+
* request's headers, so there is no full array — these fetch a header by name.
|
|
24
|
+
* `getAll` returns `[value]` or `[]` (duplicate request headers collapse to one
|
|
25
|
+
* on this platform).
|
|
26
|
+
*/
|
|
27
|
+
export interface RequestHeaders {
|
|
28
|
+
get(name: string): string | undefined
|
|
29
|
+
getAll(name: string): string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ServerRequest {
|
|
33
|
+
method: string
|
|
34
|
+
/** Path plus query string, e.g. "/hello?x=1". */
|
|
35
|
+
url: string
|
|
36
|
+
headers: RequestHeaders
|
|
37
|
+
/**
|
|
38
|
+
* Request body as a single-shot async iterable of byte chunks (v1 buffers the
|
|
39
|
+
* whole body up to `maxBodySize` before the handler runs). After any of
|
|
40
|
+
* `body`/`text()`/`json()`/`bytes()` starts draining, the next consumer call
|
|
41
|
+
* throws `BodyConsumedError`.
|
|
42
|
+
*/
|
|
43
|
+
body: AsyncIterable<Result<Uint8Array, RequestError>>
|
|
44
|
+
text(): Promise<Result<string, RequestError>>
|
|
45
|
+
json(): Promise<Result<unknown, RequestError>>
|
|
46
|
+
bytes(): Promise<Result<Uint8Array, RequestError>>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ServerResponse {
|
|
50
|
+
status: number
|
|
51
|
+
headers?: [string, string][] | Record<string, string>
|
|
52
|
+
body?: string | Uint8Array | AsyncIterable<Uint8Array | string>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type Handler = (req: ServerRequest) => ServerResponse | Promise<ServerResponse>
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Called when a handler panics (throws or rejects) — an unexpected failure, not
|
|
59
|
+
* the place for normal error handling. Receives the thrown value and the
|
|
60
|
+
* request. Return a `ServerResponse` to send it; return nothing to fall back to
|
|
61
|
+
* a plain 500. If the hook itself throws, the 500 fallback is used.
|
|
62
|
+
*/
|
|
63
|
+
export type PanicHandler = (
|
|
64
|
+
error: unknown,
|
|
65
|
+
req: ServerRequest,
|
|
66
|
+
) => ServerResponse | undefined | void | Promise<ServerResponse | undefined | void>
|
|
67
|
+
|
|
68
|
+
export interface ServerOptions {
|
|
69
|
+
/** Cap on the buffered request body, in bytes (default 16 KiB). */
|
|
70
|
+
maxBodySize?: number
|
|
71
|
+
/** Hook invoked when a handler panics (throws). See {@link PanicHandler}. */
|
|
72
|
+
onPanic?: PanicHandler
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface Server {
|
|
76
|
+
/** Bind and start serving. Synchronous; returns Err on bind failure. */
|
|
77
|
+
listen(opts: {port: number}): Result<void, ServerError>
|
|
78
|
+
/** Stop serving and release the port. Safe to call when not listening. */
|
|
79
|
+
close(): void
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type CreateServer = (handler: Handler, opts?: ServerOptions) => Server
|
|
83
|
+
|
|
84
|
+
interface NativeRequestDescriptor {
|
|
85
|
+
id: number
|
|
86
|
+
method: string
|
|
87
|
+
url: string
|
|
88
|
+
body: Uint8Array
|
|
89
|
+
bodyTooLarge: boolean
|
|
90
|
+
contentLength: number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface NativeHttpServerModule {
|
|
94
|
+
start: (port: number, maxBodySize?: number) => Result<void, ServerError>
|
|
95
|
+
stop: () => void
|
|
96
|
+
nextRequest: () => Promise<NativeRequestDescriptor | {closed: true}>
|
|
97
|
+
/** One-shot response: status + headers + full body, sent with Content-Length. */
|
|
98
|
+
respond: (id: number, status: number, headers: [string, string][], body?: Uint8Array) => void
|
|
99
|
+
/** Begin a chunked (streamed) response. Resolves once status + headers are sent. */
|
|
100
|
+
respondStart: (id: number, status: number, headers: [string, string][]) => Promise<void>
|
|
101
|
+
/** Send one body chunk. Resolves to false if the client has gone (stop sending). */
|
|
102
|
+
respondChunk: (id: number, data: Uint8Array) => Promise<boolean>
|
|
103
|
+
/** Finish a chunked response. */
|
|
104
|
+
respondEnd: (id: number) => void
|
|
105
|
+
getHeader: (id: number, name: string) => string | undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const encoder = new TextEncoder()
|
|
109
|
+
const DEFAULT_MAX_BODY = 16384
|
|
110
|
+
|
|
111
|
+
/* ── Request / response plumbing ───────────────────────────────────── */
|
|
112
|
+
|
|
113
|
+
// CR, LF, and NUL in a header name or value could split the response or
|
|
114
|
+
// truncate it. esp_http_server stores header pointers verbatim and does not
|
|
115
|
+
// sanitize, so an app that reflects untrusted input into a header (e.g. a
|
|
116
|
+
// redirect Location) must not be able to inject headers through it.
|
|
117
|
+
const UNSAFE_HEADER_CHAR = /[\r\n\0]/
|
|
118
|
+
|
|
119
|
+
function normalizeHeaders(
|
|
120
|
+
input: [string, string][] | Record<string, string> | undefined,
|
|
121
|
+
): [string, string][] {
|
|
122
|
+
if (!input) return []
|
|
123
|
+
const tuples: [string, string][] = Array.isArray(input)
|
|
124
|
+
? input
|
|
125
|
+
: Object.keys(input).map((k) => [k, input[k]!])
|
|
126
|
+
// Drop offending headers rather than throw: a throw here would leave the
|
|
127
|
+
// httpd worker un-responded and hang the request.
|
|
128
|
+
return tuples.filter(([k, v]) => !UNSAFE_HEADER_CHAR.test(k) && !UNSAFE_HEADER_CHAR.test(v))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function bodyIterable(
|
|
132
|
+
desc: NativeRequestDescriptor,
|
|
133
|
+
cap: number,
|
|
134
|
+
): AsyncIterable<Result<Uint8Array, RequestError>> {
|
|
135
|
+
return {
|
|
136
|
+
[Symbol.asyncIterator]() {
|
|
137
|
+
let sent = false
|
|
138
|
+
return {
|
|
139
|
+
async next(): Promise<IteratorResult<Result<Uint8Array, RequestError>>> {
|
|
140
|
+
if (sent) return {done: true, value: undefined}
|
|
141
|
+
sent = true
|
|
142
|
+
if (desc.bodyTooLarge) {
|
|
143
|
+
return {done: false, value: err(RequestError.BodyTooLarge(desc.contentLength, cap))}
|
|
144
|
+
}
|
|
145
|
+
if (desc.body.length === 0) return {done: true, value: undefined}
|
|
146
|
+
return {done: false, value: ok(desc.body)}
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function makeServerRequest(
|
|
154
|
+
native: NativeHttpServerModule,
|
|
155
|
+
desc: NativeRequestDescriptor,
|
|
156
|
+
cap: number,
|
|
157
|
+
): ServerRequest {
|
|
158
|
+
// Borrow the client's single-shot body draining (text/json/bytes share one
|
|
159
|
+
// consumed flag with body). Headers are NOT borrowed: esp_http_server can't
|
|
160
|
+
// enumerate them, so they're lazy by-name lookups via native.getHeader.
|
|
161
|
+
const drained = makeResponse({
|
|
162
|
+
status: 0,
|
|
163
|
+
statusText: '',
|
|
164
|
+
url: desc.url,
|
|
165
|
+
redirected: false,
|
|
166
|
+
headers: [],
|
|
167
|
+
body: bodyIterable(desc, cap),
|
|
168
|
+
})
|
|
169
|
+
return {
|
|
170
|
+
method: desc.method,
|
|
171
|
+
url: desc.url,
|
|
172
|
+
headers: {
|
|
173
|
+
get: (name) => native.getHeader(desc.id, name),
|
|
174
|
+
getAll: (name) => {
|
|
175
|
+
const v = native.getHeader(desc.id, name)
|
|
176
|
+
return v === undefined ? [] : [v]
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
body: drained.body,
|
|
180
|
+
text: drained.text,
|
|
181
|
+
json: drained.json,
|
|
182
|
+
bytes: drained.bytes,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function streamResponse(
|
|
187
|
+
native: NativeHttpServerModule,
|
|
188
|
+
id: number,
|
|
189
|
+
status: number,
|
|
190
|
+
headers: [string, string][],
|
|
191
|
+
body: AsyncIterable<Uint8Array | string>,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
let started = false
|
|
194
|
+
try {
|
|
195
|
+
await native.respondStart(id, status, headers)
|
|
196
|
+
started = true
|
|
197
|
+
for await (const chunk of body) {
|
|
198
|
+
const bytes = typeof chunk === 'string' ? encoder.encode(chunk) : chunk
|
|
199
|
+
if (bytes.length === 0) continue
|
|
200
|
+
// respondChunk resolves once the chunk is flushed (backpressure); false
|
|
201
|
+
// means the client disconnected, so stop pulling from the generator.
|
|
202
|
+
if (!(await native.respondChunk(id, bytes))) break
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// The generator threw after status + headers were already sent, so the
|
|
206
|
+
// status can't change. End the response below to truncate it cleanly; the
|
|
207
|
+
// server keeps running.
|
|
208
|
+
} finally {
|
|
209
|
+
// Only end a stream we actually started, so the worker is always released
|
|
210
|
+
// exactly once.
|
|
211
|
+
if (started) native.respondEnd(id)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function sendResponse(
|
|
216
|
+
native: NativeHttpServerModule,
|
|
217
|
+
id: number,
|
|
218
|
+
response: ServerResponse,
|
|
219
|
+
): Promise<void> {
|
|
220
|
+
const headers = normalizeHeaders(response.headers)
|
|
221
|
+
const body = response.body
|
|
222
|
+
if (body === undefined) {
|
|
223
|
+
native.respond(id, response.status, headers, undefined)
|
|
224
|
+
} else if (typeof body === 'string') {
|
|
225
|
+
native.respond(id, response.status, headers, encoder.encode(body))
|
|
226
|
+
} else if (body instanceof Uint8Array) {
|
|
227
|
+
native.respond(id, response.status, headers, body)
|
|
228
|
+
} else {
|
|
229
|
+
await streamResponse(native, id, response.status, headers, body)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const FALLBACK_500: ServerResponse = {status: 500, body: 'Internal Server Error'}
|
|
234
|
+
|
|
235
|
+
// A handler that panics (throws) is isolated to its one request so the server
|
|
236
|
+
// keeps serving: run the optional onPanic hook for a custom response (and
|
|
237
|
+
// server-side visibility), else send a plain 500. The httpd worker is blocked
|
|
238
|
+
// until we respond, so every branch must produce exactly one response.
|
|
239
|
+
async function handlePanic(
|
|
240
|
+
onPanic: PanicHandler | undefined,
|
|
241
|
+
error: unknown,
|
|
242
|
+
req: ServerRequest,
|
|
243
|
+
): Promise<ServerResponse> {
|
|
244
|
+
if (!onPanic) return FALLBACK_500
|
|
245
|
+
try {
|
|
246
|
+
return (await onPanic(error, req)) ?? FALLBACK_500
|
|
247
|
+
} catch {
|
|
248
|
+
// The hook itself threw; don't let its bug hang the worker.
|
|
249
|
+
return FALLBACK_500
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function serveLoop(
|
|
254
|
+
native: NativeHttpServerModule,
|
|
255
|
+
handler: Handler,
|
|
256
|
+
isRunning: () => boolean,
|
|
257
|
+
cap: number,
|
|
258
|
+
onPanic: PanicHandler | undefined,
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
while (isRunning()) {
|
|
261
|
+
let desc: NativeRequestDescriptor | {closed: true}
|
|
262
|
+
try {
|
|
263
|
+
desc = await native.nextRequest()
|
|
264
|
+
} catch {
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
if ('closed' in desc) break
|
|
268
|
+
|
|
269
|
+
const req = makeServerRequest(native, desc, cap)
|
|
270
|
+
// The whole per-request cycle (handler + send) is isolated so neither a
|
|
271
|
+
// throwing handler nor a failed send can break the serve loop and stall
|
|
272
|
+
// the server.
|
|
273
|
+
try {
|
|
274
|
+
let response: ServerResponse
|
|
275
|
+
try {
|
|
276
|
+
response = await handler(req)
|
|
277
|
+
} catch (e) {
|
|
278
|
+
response = await handlePanic(onPanic, e, req)
|
|
279
|
+
}
|
|
280
|
+
await sendResponse(native, desc.id, response)
|
|
281
|
+
} catch {
|
|
282
|
+
// Sending the response failed; the request is already past the point
|
|
283
|
+
// where we can change it. Keep serving the next request.
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function createServerFromNative(native: NativeHttpServerModule): CreateServer {
|
|
289
|
+
return (handler, opts = {}) => {
|
|
290
|
+
const cap = opts.maxBodySize ?? DEFAULT_MAX_BODY
|
|
291
|
+
let running = false
|
|
292
|
+
return {
|
|
293
|
+
listen({port}) {
|
|
294
|
+
if (running) return err(ServerError.AlreadyListening())
|
|
295
|
+
const started = native.start(port, opts.maxBodySize)
|
|
296
|
+
if (!started.ok) return started
|
|
297
|
+
running = true
|
|
298
|
+
void serveLoop(native, handler, () => running, cap, opts.onPanic)
|
|
299
|
+
return ok()
|
|
300
|
+
},
|
|
301
|
+
close() {
|
|
302
|
+
if (!running) return
|
|
303
|
+
running = false
|
|
304
|
+
native.stop()
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as native from 'native:http_server'
|
|
2
|
+
|
|
3
|
+
import {createServerFromNative} from './server-impl.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create an HTTP server backed by the ESP32 `esp_http_server`. Lazy: nothing
|
|
7
|
+
* binds until `listen()`. The handler runs per request on the event loop and
|
|
8
|
+
* returns `{status, headers?, body?}`; `body` may be a string, `Uint8Array`, or
|
|
9
|
+
* an async iterable (e.g. an async generator) for a streamed response.
|
|
10
|
+
*
|
|
11
|
+
* const server = createServer((req) => {
|
|
12
|
+
* if (req.url === '/') return {status: 200, body: 'Hello'}
|
|
13
|
+
* return {status: 404, body: 'not found'}
|
|
14
|
+
* })
|
|
15
|
+
* const r = server.listen({port: 80})
|
|
16
|
+
* if (!r.ok) return // ServerError
|
|
17
|
+
*/
|
|
18
|
+
export const createServer = createServerFromNative(native)
|
|
19
|
+
|
|
20
|
+
export type {
|
|
21
|
+
CreateServer,
|
|
22
|
+
Handler,
|
|
23
|
+
PanicHandler,
|
|
24
|
+
RequestHeaders,
|
|
25
|
+
Server,
|
|
26
|
+
ServerOptions,
|
|
27
|
+
ServerRequest,
|
|
28
|
+
ServerResponse,
|
|
29
|
+
} from './server-impl.js'
|
|
30
|
+
export {ServerError} from './server-impl.js'
|
package/runtime/internal.d.ts
CHANGED
|
@@ -163,6 +163,38 @@ declare module 'native:http' {
|
|
|
163
163
|
export function cancel(id: number): void
|
|
164
164
|
export function pendingCount(): number
|
|
165
165
|
}
|
|
166
|
+
|
|
167
|
+
declare module 'native:http_server' {
|
|
168
|
+
import type {Result} from 'mikro/result'
|
|
169
|
+
|
|
170
|
+
import type {ServerError} from './http/server-impl.js'
|
|
171
|
+
|
|
172
|
+
export type NativeRequestDescriptor = {
|
|
173
|
+
id: number
|
|
174
|
+
method: string
|
|
175
|
+
url: string
|
|
176
|
+
body: Uint8Array
|
|
177
|
+
bodyTooLarge: boolean
|
|
178
|
+
contentLength: number
|
|
179
|
+
}
|
|
180
|
+
export function start(port: number, maxBodySize?: number): Result<void, ServerError>
|
|
181
|
+
export function stop(): void
|
|
182
|
+
export function nextRequest(): Promise<NativeRequestDescriptor | {closed: true}>
|
|
183
|
+
export function respond(
|
|
184
|
+
id: number,
|
|
185
|
+
status: number,
|
|
186
|
+
headers: [string, string][],
|
|
187
|
+
body?: Uint8Array,
|
|
188
|
+
): void
|
|
189
|
+
export function respondStart(
|
|
190
|
+
id: number,
|
|
191
|
+
status: number,
|
|
192
|
+
headers: [string, string][],
|
|
193
|
+
): Promise<void>
|
|
194
|
+
export function respondChunk(id: number, data: Uint8Array): Promise<boolean>
|
|
195
|
+
export function respondEnd(id: number): void
|
|
196
|
+
export function getHeader(id: number, name: string): string | undefined
|
|
197
|
+
}
|
|
166
198
|
declare module 'native:i2c' {
|
|
167
199
|
import type {I2cError} from '@mikrojs/native/runtime/i2c/types'
|
|
168
200
|
import type {Result} from 'mikro/result'
|
package/src/mikrojs.cpp
CHANGED
|
@@ -150,13 +150,13 @@ static void mik__promise_rejection_tracker(JSContext* ctx, JSValue promise, JSVa
|
|
|
150
150
|
* Don't JS_Throw here — we're inside a QuickJS callback
|
|
151
151
|
* during promise resolution; throwing would corrupt engine
|
|
152
152
|
* state and cause mik__execute_jobs to dump the error a
|
|
153
|
-
* second time. MIK_Stop
|
|
154
|
-
*
|
|
155
|
-
*
|
|
153
|
+
* second time. MIK_Stop gates on whether we're inside an
|
|
154
|
+
* interactive REPL eval (skipping both the stop request and
|
|
155
|
+
* the restart) so a typo at the prompt doesn't reboot the
|
|
156
|
+
* device. */
|
|
156
157
|
if (mik_rt->error_handler_fn) {
|
|
157
158
|
mik_rt->error_handler_fn(ctx, reason, mik_rt->error_handler_opaque);
|
|
158
159
|
}
|
|
159
|
-
mik_rt->stop_requested = true;
|
|
160
160
|
MIK_Stop(mik_rt);
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
@@ -192,7 +192,6 @@ static void mik__promise_rejection_tracker(JSContext* ctx, JSValue promise, JSVa
|
|
|
192
192
|
if (mik_rt->error_handler_fn) {
|
|
193
193
|
mik_rt->error_handler_fn(ctx, reason, mik_rt->error_handler_opaque);
|
|
194
194
|
}
|
|
195
|
-
mik_rt->stop_requested = true;
|
|
196
195
|
MIK_Stop(mik_rt);
|
|
197
196
|
}
|
|
198
197
|
}
|
|
@@ -680,17 +679,24 @@ bool MIK_IsStopRequested(MIKRuntime* mik_rt) {
|
|
|
680
679
|
|
|
681
680
|
void MIK_Stop(MIKRuntime* mik_rt) {
|
|
682
681
|
CHECK_NOT_NULL(mik_rt);
|
|
682
|
+
/* A throw inside an interactive REPL eval is a user typo, not an app
|
|
683
|
+
* crash; the eval path already reports it. Don't request a stop or
|
|
684
|
+
* reboot. (Evaluating implies the REPL is active, so this is checked
|
|
685
|
+
* before the IsReplActive guard below.) */
|
|
686
|
+
if (mik__repl_is_evaluating()) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
/* Signal the loop to halt. This is the single place stop_requested is
|
|
690
|
+
* set, so the repl-eval gate above can't be bypassed by a caller. Host
|
|
691
|
+
* embedders (Node addon) observe it via MIK_Loop's return value; the
|
|
692
|
+
* firmware test supervisor reads it via MIK_IsStopRequested. */
|
|
693
|
+
mik_rt->stop_requested = true;
|
|
683
694
|
/* Only firmware (protocol REPL attached) auto-restarts on uncaught
|
|
684
695
|
* exceptions. Host embedders (Node addon, standalone tests) own their
|
|
685
696
|
* own process lifecycle and surface errors via the error handler. */
|
|
686
697
|
if (!MIK_IsReplActive()) {
|
|
687
698
|
return;
|
|
688
699
|
}
|
|
689
|
-
/* A throw inside an interactive REPL eval is a user typo, not an app
|
|
690
|
-
* crash — don't reboot the device. */
|
|
691
|
-
if (mik__repl_is_evaluating()) {
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
700
|
/* In test mode, the supervisor wants stop_requested to bubble up so it
|
|
695
701
|
* can synthesize a failing-test event and move to the next file. Arming
|
|
696
702
|
* a panic-restart here would reboot the device mid-manifest on the
|