84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
|
|
/**
|
||
|
|
* instantEngine — runtime seam for an in-browser engine that can replace a
|
||
|
|
* QEMU-Linux boot for the boards that don't need a real OS.
|
||
|
|
*
|
||
|
|
* QEMU-Linux boards (Raspberry Pi family + overlay piFamily kinds) cost a
|
||
|
|
* backend process and ~90 s of boot per run. Most projects are just a
|
||
|
|
* Python script driving GPIO and a screen — those can run in the browser
|
||
|
|
* in a couple of seconds. An overlay registers an engine here; the store
|
||
|
|
* asks it, per board, whether it can take this run:
|
||
|
|
*
|
||
|
|
* Run pressed
|
||
|
|
* -> engine.decide(boardId) -> 'instant' : engine.run(boardId)
|
||
|
|
* 'linux' : boot the QEMU guest
|
||
|
|
*
|
||
|
|
* The decision is data (`EngineDecision`), not a boolean, so the UI can
|
||
|
|
* explain WHY a board needs Linux ("script.py:12 uses subprocess") instead
|
||
|
|
* of silently taking 90 s. Nothing is registered in the OSS build, so the
|
||
|
|
* QEMU path stays exactly as it was.
|
||
|
|
*/
|
||
|
|
|
||
|
|
export interface EngineDecision {
|
||
|
|
/** Which engine should take this run. */
|
||
|
|
engine: 'instant' | 'linux';
|
||
|
|
/** Short, user-facing reason. Empty when 'instant' with nothing to say. */
|
||
|
|
reason: string;
|
||
|
|
/** Optional `file:line` the reason refers to, for the tooltip. */
|
||
|
|
where?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface InstantEngine {
|
||
|
|
/** Decide which engine runs this board's current files. */
|
||
|
|
decide(boardId: string): EngineDecision;
|
||
|
|
/** Run the board's files in the browser. Resolves when the script ends. */
|
||
|
|
run(boardId: string): Promise<void>;
|
||
|
|
/** Cancel a running script (the runtime may stay warm). */
|
||
|
|
stop(boardId: string): void;
|
||
|
|
}
|
||
|
|
|
||
|
|
let engine: InstantEngine | null = null;
|
||
|
|
const listeners = new Set<() => void>();
|
||
|
|
|
||
|
|
export function registerInstantEngine(e: InstantEngine | null): void {
|
||
|
|
engine = e;
|
||
|
|
for (const l of listeners) l();
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getInstantEngine(): InstantEngine | null {
|
||
|
|
return engine;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Subscribe to registration (the overlay loads asynchronously). */
|
||
|
|
export function subscribeInstantEngine(cb: () => void): () => void {
|
||
|
|
listeners.add(cb);
|
||
|
|
return () => listeners.delete(cb);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Which engine should run this board, honouring a user override.
|
||
|
|
*
|
||
|
|
* `pinned` comes from the board instance (the user clicked the mode chip
|
||
|
|
* or turned on the Linux terminal); when set it wins over the detector,
|
||
|
|
* because predictability beats cleverness for a project you already know.
|
||
|
|
*/
|
||
|
|
export function decideEngine(
|
||
|
|
boardId: string,
|
||
|
|
pinned?: 'instant' | 'linux',
|
||
|
|
): EngineDecision {
|
||
|
|
if (pinned === 'linux') {
|
||
|
|
return { engine: 'linux', reason: 'Linux mode is on for this project' };
|
||
|
|
}
|
||
|
|
if (!engine) {
|
||
|
|
return { engine: 'linux', reason: 'the instant engine is not available' };
|
||
|
|
}
|
||
|
|
if (pinned === 'instant') {
|
||
|
|
return { engine: 'instant', reason: 'instant mode is pinned for this project' };
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
return engine.decide(boardId);
|
||
|
|
} catch {
|
||
|
|
// A detector bug must never block a run: fall back to the real machine.
|
||
|
|
return { engine: 'linux', reason: 'could not analyse the project' };
|
||
|
|
}
|
||
|
|
}
|