{"version":3,"file":"host.cjs","names":[],"sources":["../../src/webix/host.ts"],"sourcesContent":["/**\n * WebixHost — lifecycle wrapper around a single webix Blink x86_64 Linux host.\n *\n * One WebixHost backs one Sandbox: the Blink WASM owns the CPU/MMU/syscalls and\n * an in-memory Linux filesystem (MEMFS), so all of a sandbox's commands and file\n * operations share persistent state through the lifetime of the host. There is\n * no remote network — this is a self-contained, in-browser microVM, so\n * remote ports/dev-servers/public domains are unavailable by construction.\n * The portabox build additionally exposes a framebuffer + input device (see\n * fbView/attachDisplay/pushInput) and is threaded + sockets-enabled (socket()\n * works in-process); fork() remains absent (emscripten limitation).\n */\n\nexport interface WebixHostOptions {\n  /** URL of the Blink wasm. Defaults to `/containers/blinkenlib.wasm`. */\n  wasmUrl?: string;\n  /** URL of the Blink emscripten glue JS. Defaults to `/containers/blinkenlib.js`. */\n  glueUrl?: string;\n  /**\n   * URL of the root filesystem tarball (optionally gzipped) mounted at `/`.\n   * Defaults to `/containers/alpine-minirootfs-x86_64.tar.gz`.\n   */\n  rootfsUrl?: string;\n  /** Raw rootfs tar bytes (skips the fetch); takes precedence over rootfsUrl. */\n  rootfsTarBytes?: Uint8Array;\n  /** Node-only: filesystem path to the wasm (skips fetch). */\n  wasmPath?: string;\n  /** Node-only: filesystem path to the glue JS. */\n  gluePath?: string;\n}\n\nconst DEFAULTS = {\n  // Browser default: assets served same-origin under /containers/ (correct MIME\n  // for streaming-compiled wasm + dynamic-imported glue). Serve your own copies\n  // (e.g. from node_modules/webix/containers) or override these per call.\n  wasmUrl: \"/containers/blinkenlib.wasm\",\n  glueUrl: \"/containers/blinkenlib.js\",\n  rootfsUrl: \"/containers/alpine-minirootfs-x86_64.tar.gz\",\n};\n\nconst BUSYBOX_PATH = \"/bin/busybox\";\n\nconst isBrowser =\n  typeof (globalThis as { window?: unknown }).window !== \"undefined\" &&\n  typeof fetch !== \"undefined\";\n\ntype BlinkCore = Awaited<\n  ReturnType<typeof import(\"webix/blink-core\").createBlinkCore>\n>;\n\n/** Gunzip bytes if they carry the gzip magic, else return as-is. */\nasync function maybeGunzip(bytes: Uint8Array): Promise<Uint8Array> {\n  if (bytes.length < 2 || bytes[0] !== 0x1f || bytes[1] !== 0x8b) return bytes;\n  if (typeof DecompressionStream !== \"undefined\") {\n    const ds = new DecompressionStream(\"gzip\");\n    const stream = new Response(bytes).body!.pipeThrough(ds);\n    return new Uint8Array(await new Response(stream).arrayBuffer());\n  }\n  const { gunzipSync } = await import(\"node:zlib\");\n  return new Uint8Array(gunzipSync(bytes));\n}\n\n/**\n * The Blink faketty echoes the command line as a shell prompt before the real\n * output: `\\n$ <argv joined by space>\\n<output>`. Strip exactly that known\n * prefix so runCommand stdout is the command's actual output. Matching the\n * exact joined argv (not a generic `$ ` regex) avoids clobbering output that\n * legitimately contains `$ ` lines.\n */\nfunction stripBlinkPrompt(stdout: string, argv: string[]): string {\n  const prompt = \"$ \" + argv.join(\" \") + \"\\n\";\n  if (stdout.startsWith(prompt)) return stdout.slice(prompt.length);\n  if (stdout.startsWith(\"\\n\" + prompt)) return stdout.slice(prompt.length + 1);\n  return stdout;\n}\n\nasync function fetchBytes(url: string): Promise<Uint8Array> {\n  if (isBrowser) {\n    const res = await fetch(url);\n    if (!res.ok) {\n      throw new Error(\n        `webix: failed to fetch ${url} (HTTP ${res.status}). The wasm must be served with Content-Type: application/wasm.`,\n      );\n    }\n    return new Uint8Array(await res.arrayBuffer());\n  }\n  const { readFile } = await import(\"node:fs/promises\");\n  const { fileURLToPath } = await import(\"node:url\");\n  const path = url.startsWith(\"file:\") ? fileURLToPath(url) : url;\n  return new Uint8Array(await readFile(path));\n}\n\nexport class WebixHost {\n  private core: BlinkCore | null = null;\n  private busyboxHandle: string | null = null;\n  private runQueue: Promise<unknown> = Promise.resolve();\n  private stopped = false;\n  private readonly opts: WebixHostOptions;\n\n  constructor(opts: WebixHostOptions = {}) {\n    this.opts = opts;\n  }\n\n  /** Boot the Blink host, mount the rootfs, and preload busybox. Idempotent. */\n  async boot(): Promise<void> {\n    if (this.core) return;\n\n    const wasmUrl = this.opts.wasmUrl ?? DEFAULTS.wasmUrl;\n    const glueUrl = this.opts.glueUrl ?? DEFAULTS.glueUrl;\n\n    if (isBrowser) {\n      const { createBlinkHostBrowser } = await import(\"webix/blink-browser\");\n      this.core = await createBlinkHostBrowser({ wasmUrl, glueUrl });\n    } else {\n      // The threaded (-pthread) emscripten glue references the worker-scope\n      // global `self` at module-eval time; on Node's main thread that is\n      // undefined. Shim it to globalThis so the threaded artifact loads under\n      // Node (the pthread pool spawns real worker_threads that set their own\n      // scope). Harmless on the single-thread build.\n      const g = globalThis as { self?: unknown };\n      if (typeof g.self === \"undefined\") g.self = globalThis;\n      // Node-only host (pulls node:fs via webix/blink). Marked bundler-ignored\n      // so browser bundlers (Next/Turbopack) don't try to chunk it into the\n      // client build — this branch only runs under Node.\n      const { createBlinkHost } = await import(\n        /* webpackIgnore: true */ /* @vite-ignore */ \"webix/blink\"\n      );\n      this.core = await createBlinkHost({\n        wasmPath: this.opts.wasmPath ?? wasmUrl.replace(/^\\//, \"\"),\n        gluePath: this.opts.gluePath ?? glueUrl.replace(/^\\//, \"\"),\n      });\n    }\n\n    const rootfsBytes =\n      this.opts.rootfsTarBytes ??\n      (await maybeGunzip(\n        await fetchBytes(this.opts.rootfsUrl ?? DEFAULTS.rootfsUrl),\n      ));\n    this.core.mountTarBytes(rootfsBytes);\n\n    // Preload busybox once so each runElf skips the multi-MB FS rewrite.\n    try {\n      const bb = this.core.Module.FS.readFile(BUSYBOX_PATH);\n      this.busyboxHandle = this.core.preloadFile(\"busybox\", bb);\n    } catch {\n      this.busyboxHandle = null;\n    }\n  }\n\n  private ensureCore(): BlinkCore {\n    if (this.stopped) throw new Error(\"webix: sandbox has been stopped\");\n    if (!this.core) throw new Error(\"webix: host not booted (call boot())\");\n    return this.core;\n  }\n\n  get fs(): BlinkCore[\"Module\"][\"FS\"] {\n    return this.ensureCore().Module.FS;\n  }\n\n  /** Whether `/bin/busybox` exists in the mounted rootfs. */\n  get hasBusybox(): boolean {\n    return this.busyboxHandle !== null;\n  }\n\n  /**\n   * Run an ELF (by guest-FS path or raw bytes) to completion. Calls are\n   * serialized: Blink is single-threaded and rejects overlapping runs, so we\n   * chain them through an internal queue.\n   */\n  runElf(\n    elf: { path?: string; bytes?: Uint8Array },\n    argv: string[],\n  ): Promise<{\n    exitCode: number;\n    stdout: string;\n    stderr: string;\n    signal: { sig: number; code: number } | null;\n  }> {\n    const run = this.runQueue.then(async () => {\n      const core = this.ensureCore();\n      const r = await core.runElf(elf.bytes ?? null, { argv, path: elf.path });\n      return { ...r, stdout: stripBlinkPrompt(r.stdout, argv) };\n    });\n    // Keep the queue alive regardless of individual failures.\n    this.runQueue = run.then(\n      () => undefined,\n      () => undefined,\n    );\n    return run;\n  }\n\n  /** Run a command via busybox: `busybox <argv...>` (applet dispatch). */\n  runBusybox(\n    argv: string[],\n  ): Promise<{\n    exitCode: number;\n    stdout: string;\n    stderr: string;\n    signal: { sig: number; code: number } | null;\n  }> {\n    if (!this.busyboxHandle) {\n      throw new Error(\"webix: busybox not present in rootfs\");\n    }\n    return this.runElf({ path: this.busyboxHandle }, argv);\n  }\n\n  /**\n   * Framebuffer geometry the guest published via syscall 0x5fb, or null if no\n   * guest has registered a framebuffer yet.\n   */\n  fbInfo(): {\n    vaddr: number;\n    width: number;\n    height: number;\n    stride: number;\n    generation: number;\n  } | null {\n    return (this.ensureCore() as any).fbInfo?.() ?? null;\n  }\n\n  /**\n   * Zero-copy view over the guest framebuffer (re-derived each call; safe\n   * against ALLOW_MEMORY_GROWTH detach). Null until a guest registers one.\n   */\n  fbView(): { pixels: Uint8ClampedArray; width: number; height: number; stride: number; generation: number } | null {\n    return (this.ensureCore() as any).fbView?.() ?? null;\n  }\n\n  /**\n   * Attach a canvas to the live framebuffer via webix's display module: a rAF\n   * blit loop that paints guest pixels and forwards canvas key/mouse events\n   * into the guest input device. Returns a controller with stats()/stop().\n   */\n  async attachDisplay(\n    canvas: unknown,\n    opts?: { fpsCap?: number },\n  ): Promise<{ stats: () => unknown; stop: () => void }> {\n    const core = this.ensureCore();\n    const { attachDisplay } = (await import(\"webix/display\")) as {\n      attachDisplay: (\n        host: unknown,\n        canvas: unknown,\n        opts?: { fpsCap?: number },\n      ) => { stats: () => unknown; stop: () => void };\n    };\n    return attachDisplay(core, canvas, opts);\n  }\n\n  /**\n   * Push one input event into the guest input ring (host -> guest). Event\n   * shape: { type: \"key\"|\"motion\"|\"button\", code?, button?, x?, y?, down? }.\n   * Returns false if the running wasm predates the input device.\n   */\n  pushInput(evt: {\n    type: \"key\" | \"motion\" | \"button\";\n    code?: number;\n    button?: number;\n    x?: number;\n    y?: number;\n    down?: number;\n    value?: number;\n  }): boolean {\n    return (this.ensureCore() as any).pushInput?.(evt) ?? false;\n  }\n\n  /** Capability flags of the running Blink build. */\n  get capabilities(): Record<string, unknown> {\n    return (this.ensureCore() as any).capabilities ?? {};\n  }\n\n  snapshot() {\n    return this.ensureCore().snapshot();\n  }\n\n  restore(snap: ReturnType<BlinkCore[\"snapshot\"]>) {\n    this.ensureCore().restore(snap);\n  }\n\n  stop(): void {\n    this.stopped = true;\n    this.core = null;\n    this.busyboxHandle = null;\n  }\n}\n"],"mappings":"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"}