// SPDX-License-Identifier: GPL-2.0-only /// Create a Linux machine and run it. const linux = async (worker_url, vmlinux, boot_cmdline, initrd, log, console_write) => { /// Dict of online CPUs. const cpus = {}; /// Dict of tasks. const tasks = {}; /// Input buffer (from keyboard to tty). let input_buffer = new ArrayBuffer(0); const text_decoder = new TextDecoder("utf-8"); const text_encoder = new TextEncoder(); const lock_notify = (locks, lock, count) => { Atomics.store(locks._memory, locks[lock], 1); Atomics.notify(locks._memory, locks[lock], count || 1); }; const lock_wait = (locks, lock) => { Atomics.wait(locks._memory, locks[lock], 0); Atomics.store(locks._memory, locks[lock], 0); }; /// Callbacks from Web Workers (each one representing one task). const message_callbacks = { start_primary: (message) => { // CPU 0 has init_task which sits in static storage. After booting it becomes CPU 0's idle task. The runner will // in this special case tell us where it is so that we can register it. log("Starting cpu 0 with init_task " + message.init_task) tasks[message.init_task] = cpus[0]; }, start_secondary: (message) => { if (message.cpu <= 0) { throw new Error("Trying to start secondary cpu with ID <= 0"); } log("Starting cpu " + message.cpu + " (" + message.idle_task + ")" + " with start stack " + message.start_stack); make_cpu(message.cpu, message.idle_task, message.start_stack); }, stop_secondary: (message) => { if (message.cpu <= 0) { // If you arrive here, you probably got panic():ed with a broken stack. if (!confirm("Trying to stop secondary cpu with ID 0.\n\n" + "You probably got panic():ed with a broken stack. Continue?\n\n" + " (Say ok if you know what you are doing and want to catch the panic, otherwise cancel.)")) { throw new Error("Trying to stop secondary cpu with ID 0"); } } if (cpus[message.cpu]) { log("[Main]: Stopping CPU " + message.cpu); cpus[message.cpu].worker.terminate(); delete cpus[message.cpu]; } else { log("[Main]: Tried to stop CPU " + message.cpu + " but it was already stopped (broken system)!"); } }, create_and_run_task: (message) => { // ret_from_fork will make sure the task switch finishes. make_task(message.prev_task, message.new_task, message.name, message.user_executable); }, release_task: (message) => { // Stop the worker, which will stop script execution. This is safe as the task should be hanging on a lock waiting // to be scheduled - which never happens as dead tasks don't get ever get scheduled. tasks[message.dead_task].worker.terminate(); delete tasks[message.dead_task]; }, serialize_tasks: (message) => { // next_task was previously suspended, wake it up. // Tell the next task where we switched from, so that it can finish the task switch. tasks[message.next_task].last_task[0] = message.prev_task; // Release the above write of last_task and wake up the task. lock_notify(tasks[message.next_task].locks, "serialize"); }, console_read: (message, worker) => { const memory_u8 = new Uint8Array(memory.buffer); const buffer = new Uint8Array(input_buffer); const used = buffer.slice(0, message.count); memory_u8.set(used, message.buffer); const unused = buffer.slice(message.count); input_buffer = unused.buffer; // Tell the Worker that asked for input how many bytes (perhaps 0) were actually written. Atomics.store(message.console_read_messenger, 0, used.length); Atomics.notify(message.console_read_messenger, 0, 1); }, console_write: (message) => { console_write(message.message); }, log: (message) => { log(message.message); }, }; /// Memory shared between all CPUs. const memory = new WebAssembly.Memory({ initial: 30, // TODO: extract this automatically from vmlinux. maximum: 0x10000, // Allow the full 32-bit address space to be allocated. shared: true, }); /** * Create and run one CPU in a background thread (a Web Worker). * * This will run boot code for the CPU, and then drop to run the idle task. For CPU 0 this involves booting the entire * system, including bringing up secondary CPUs at the end, while for secondary CPUs, this just means some * book-keeping before dropping into their own idle tasks. */ const make_cpu = (cpu, idle_task, start_stack) => { const options = { runner_type: (cpu == 0) ? "primary_cpu" : "secondary_cpu", start_stack: start_stack, // undefined for CPU 0 }; if (cpu == 0) { options.boot_cmdline = boot_cmdline; options.initrd = initrd; initrd = null; // allow gc } // idle_task is undefined for cpu 0, we will know it first when start_primary notifies us. const name = "CPU " + cpu + " [boot+idle]" + (cpu != 0 ? " (" + idle_task + ")" : ""); const runner = make_vmlinux_runner(name, options); cpus[cpu] = runner; if (cpu != 0) { tasks[idle_task] = runner; // For CPU 0, start_primary does this registration for us. } }; /** * Create and run one task. This task has been switch_to():ed by the scheduler for the first time. * * In the beginning, all tasks are serialized and have to cooperate to schedule eachother, but after secondary CPUs * are brought up, they can run concurrently (and will effectively be managed by the Wasm host OS). While we are not * able to suspend them from JS, the host OS will do that. */ const make_task = (prev_task, new_task, name, user_executable) => { const options = { runner_type: "task", prev_task: prev_task, new_task: new_task, user_executable: user_executable, }; tasks[new_task] = make_vmlinux_runner(name + " (" + new_task + ")", options); }; /// Create a runner for vmlinux. It will run in a Web Worker and execute some specified code. const make_vmlinux_runner = (name, options) => { // Note: SharedWorker does not seem to allow WebAssembly Module or Memory instances posted. const worker = new Worker(worker_url, { name: name }); let locks = { serialize: 0, }; locks._memory = new Int32Array(new SharedArrayBuffer(Object.keys(locks).length * 4)); // Store for last task when wasm_serialize() returns in switch_to(). Needed for each task, both normal ones and each // CPUs idle tasks (first called init_task (PID 0), not to be confused with init (PID 1) which is a normal task). const last_task = new Uint32Array(new SharedArrayBuffer(4)); worker.onerror = (error) => { throw error; }; worker.onmessage = (message_event) => { const data = message_event.data; message_callbacks[data.method](data, worker); }; worker.onmessageerror = (error) => { throw error; }; worker.postMessage({ ...options, method: "init", vmlinux: vmlinux, memory: memory, locks: locks, last_task: last_task, runner_name: name, }); return { worker: worker, locks: locks, last_task: last_task, }; }; // Create the primary cpu, it will later on callback to us and we start secondaries. make_cpu(0); return { key_input: (data) => { const key_buffer = text_encoder.encode(data); // Possibly UTF-8 (up to 16 bits). // Append key_buffer to the end of input_buffer. const old_size = input_buffer.byteLength; input_buffer = input_buffer.transfer(old_size + key_buffer.byteLength); (new Uint8Array(input_buffer)).set(key_buffer, old_size); } }; };