223 lines
7.7 KiB
JavaScript
223 lines
7.7 KiB
JavaScript
// 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);
|
|
}
|
|
};
|
|
};
|