mirror of
https://github.com/HeyPuter/puter.git
synced 2025-02-02 23:28:39 +08:00
dev: add emulator image build
This commit is contained in:
parent
95112a9de6
commit
2c0b8428c5
412
src/emulator/basic.html
Normal file
412
src/emulator/basic.html
Normal file
@ -0,0 +1,412 @@
|
||||
<!doctype html>
|
||||
<title>Basic Emulator</title><!-- not BASIC! -->
|
||||
<style>
|
||||
div {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
BODY {
|
||||
background-color: #111;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="../build/libv86.js"></script>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
// Libs
|
||||
// SO: 40031688
|
||||
function buf2hex(buffer) { // buffer is an ArrayBuffer
|
||||
return [...new Uint8Array(buffer)]
|
||||
.map(x => x.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
class ATStream {
|
||||
constructor ({ delegate, acc, transform, observe }) {
|
||||
this.delegate = delegate;
|
||||
if ( acc ) this.acc = acc;
|
||||
if ( transform ) this.transform = transform;
|
||||
if ( observe ) this.observe = observe;
|
||||
this.state = {};
|
||||
this.carry = [];
|
||||
}
|
||||
[Symbol.asyncIterator]() { return this; }
|
||||
async next_value_ () {
|
||||
if ( this.carry.length > 0 ) {
|
||||
console.log('got from carry!', this.carry);
|
||||
return {
|
||||
value: this.carry.shift(),
|
||||
done: false,
|
||||
};
|
||||
}
|
||||
return await this.delegate.next();
|
||||
}
|
||||
async acc ({ value }) {
|
||||
return value;
|
||||
}
|
||||
async next_ () {
|
||||
for (;;) {
|
||||
const ret = await this.next_value_();
|
||||
if ( ret.done ) return ret;
|
||||
const v = await this.acc({
|
||||
state: this.state,
|
||||
value: ret.value,
|
||||
carry: v => this.carry.push(v),
|
||||
});
|
||||
if ( this.carry.length >= 0 && v === undefined ) {
|
||||
throw new Error(`no value, but carry value exists`);
|
||||
}
|
||||
if ( v === undefined ) continue;
|
||||
// We have a value, clear the state!
|
||||
this.state = {};
|
||||
if ( this.transform ) {
|
||||
const new_value = await this.transform(
|
||||
{ value: ret.value });
|
||||
return { ...ret, value: new_value };
|
||||
}
|
||||
return { ...ret, value: v };
|
||||
}
|
||||
}
|
||||
async next () {
|
||||
const ret = await this.next_();
|
||||
if ( this.observe && !ret.done ) {
|
||||
this.observe(ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
async enqueue_ (v) {
|
||||
this.queue.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
const NewCallbackByteStream = () => {
|
||||
let listener;
|
||||
let queue = [];
|
||||
const NOOP = () => {};
|
||||
let signal = NOOP;
|
||||
(async () => {
|
||||
for (;;) {
|
||||
const v = await new Promise((rslv, rjct) => {
|
||||
listener = rslv;
|
||||
});
|
||||
queue.push(v);
|
||||
signal();
|
||||
}
|
||||
})();
|
||||
const stream = {
|
||||
[Symbol.asyncIterator](){
|
||||
return this;
|
||||
},
|
||||
async next () {
|
||||
if ( queue.length > 0 ) {
|
||||
return {
|
||||
value: queue.shift(),
|
||||
done: false,
|
||||
};
|
||||
}
|
||||
await new Promise(rslv => {
|
||||
signal = rslv;
|
||||
});
|
||||
signal = NOOP;
|
||||
const v = queue.shift();
|
||||
return { value: v, done: false };
|
||||
}
|
||||
};
|
||||
stream.listener = data => {
|
||||
listener(data);
|
||||
};
|
||||
return stream;
|
||||
}
|
||||
|
||||
// Tiny inline little-endian integer library
|
||||
const get_int = (n_bytes, array8, signed=false) => {
|
||||
return (v => signed ? v : v >>> 0)(
|
||||
array8.slice(0,n_bytes).reduce((v,e,i)=>v|=e<<8*i,0));
|
||||
}
|
||||
const to_int = (n_bytes, num) => {
|
||||
return (new Uint8Array()).map((_,i)=>(num>>8*i)&0xFF);
|
||||
}
|
||||
|
||||
const NewVirtioFrameStream = byteStream => {
|
||||
return new ATStream({
|
||||
delegate: byteStream,
|
||||
async acc ({ value, carry }) {
|
||||
if ( ! this.state.buffer ) {
|
||||
const size = get_int(4, value);
|
||||
// 512MiB limit in case of attempted abuse or a bug
|
||||
// (assuming this won't happen under normal conditions)
|
||||
if ( size > 512*(1024**2) ) {
|
||||
throw new Error(`Way too much data! (${size} bytes)`);
|
||||
}
|
||||
value = value.slice(4);
|
||||
this.state.buffer = new Uint8Array(size);
|
||||
this.state.index = 0;
|
||||
}
|
||||
|
||||
const needed = this.state.buffer.length - this.state.index;
|
||||
if ( value.length > needed ) {
|
||||
const remaining = value.slice(needed);
|
||||
console.log('we got more bytes than we needed',
|
||||
needed,
|
||||
remaining,
|
||||
value.length,
|
||||
this.state.buffer.length,
|
||||
this.state.index,
|
||||
);
|
||||
carry(remaining);
|
||||
}
|
||||
|
||||
const amount = Math.min(value.length, needed);
|
||||
const added = value.slice(0, amount);
|
||||
this.state.buffer.set(added, this.state.index);
|
||||
this.state.index += amount;
|
||||
|
||||
if ( this.state.index > this.state.buffer.length ) {
|
||||
throw new Error('WUT');
|
||||
}
|
||||
if ( this.state.index == this.state.buffer.length ) {
|
||||
return this.state.buffer;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const wisp_types = [
|
||||
{
|
||||
id: 3,
|
||||
label: 'CONTINUE',
|
||||
describe: ({ payload }) => {
|
||||
return `buffer: ${get_int(4, payload)}B`;
|
||||
},
|
||||
getAttributes ({ payload }) {
|
||||
return {
|
||||
buffer_size: get_int(4, payload),
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: 'INFO',
|
||||
describe: ({ payload }) => {
|
||||
return `v${payload[0]}.${payload[1]} ` +
|
||||
buf2hex(payload.slice(2));
|
||||
},
|
||||
getAttributes ({ payload }) {
|
||||
return {
|
||||
version_major: payload[0],
|
||||
version_minor: payload[1],
|
||||
extensions: payload.slice(2),
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
class WispPacket {
|
||||
static SEND = Symbol('SEND');
|
||||
static RECV = Symbol('RECV');
|
||||
constructor ({ data, direction, extra }) {
|
||||
this.direction = direction;
|
||||
this.data_ = data;
|
||||
this.extra = extra ?? {};
|
||||
this.types_ = {
|
||||
1: { label: 'CONNECT' },
|
||||
2: { label: 'DATA' },
|
||||
4: { label: 'CLOSE' },
|
||||
};
|
||||
for ( const item of wisp_types ) {
|
||||
this.types_[item.id] = item;
|
||||
}
|
||||
}
|
||||
get type () {
|
||||
const i_ = this.data_[0];
|
||||
return this.types_[i_];
|
||||
}
|
||||
get attributes () {
|
||||
if ( ! this.type.getAttributes ) return {};
|
||||
const attrs = {};
|
||||
Object.assign(attrs, this.type.getAttributes({
|
||||
payload: this.data_.slice(5),
|
||||
}));
|
||||
Object.assign(attrs, this.extra);
|
||||
return attrs;
|
||||
}
|
||||
toVirtioFrame () {
|
||||
const arry = new Uint8Array(this.data_.length + 4);
|
||||
arry.set(to_int(4, this.data_.length), 0);
|
||||
arry.set(this.data_, 4);
|
||||
return arry;
|
||||
}
|
||||
describe () {
|
||||
return this.type.label + '(' +
|
||||
(this.type.describe?.({
|
||||
payload: this.data_.slice(5),
|
||||
}) ?? '?') + ')';
|
||||
}
|
||||
log () {
|
||||
const arrow =
|
||||
this.direction === this.constructor.SEND ? '->' :
|
||||
this.direction === this.constructor.RECV ? '<-' :
|
||||
'<>' ;
|
||||
console.groupCollapsed(`WISP ${arrow} ${this.describe()}`);
|
||||
const attrs = this.attributes;
|
||||
for ( const k in attrs ) {
|
||||
console.log(k, attrs[k]);
|
||||
}
|
||||
console.groupEnd();
|
||||
}
|
||||
reflect () {
|
||||
const reflected = new WispPacket({
|
||||
data: this.data_,
|
||||
direction:
|
||||
this.direction === this.constructor.SEND ?
|
||||
this.constructor.RECV :
|
||||
this.direction === this.constructor.RECV ?
|
||||
this.constructor.SEND :
|
||||
undefined,
|
||||
extra: {
|
||||
reflectedFrom: this,
|
||||
}
|
||||
});
|
||||
return reflected;
|
||||
}
|
||||
}
|
||||
|
||||
for ( const item of wisp_types ) {
|
||||
WispPacket[item.label] = item;
|
||||
}
|
||||
|
||||
const NewWispPacketStream = frameStream => {
|
||||
return new ATStream({
|
||||
delegate: frameStream,
|
||||
transform ({ value }) {
|
||||
return new WispPacket({
|
||||
data: value,
|
||||
direction: WispPacket.RECV,
|
||||
});
|
||||
},
|
||||
observe ({ value }) {
|
||||
value.log();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class WispClient {
|
||||
constructor ({
|
||||
packetStream,
|
||||
sendFn,
|
||||
}) {
|
||||
this.packetStream = packetStream;
|
||||
this.sendFn = sendFn;
|
||||
}
|
||||
send (packet) {
|
||||
packet.log();
|
||||
this.sendFn(packet);
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = async function()
|
||||
{
|
||||
const resp = await fetch(
|
||||
'./image/build/x86images/rootfs.bin'
|
||||
);
|
||||
const arrayBuffer = await resp.arrayBuffer();
|
||||
var emulator = window.emulator = new V86({
|
||||
wasm_path: "../build/v86.wasm",
|
||||
memory_size: 512 * 1024 * 1024,
|
||||
vga_memory_size: 2 * 1024 * 1024,
|
||||
screen_container: document.getElementById("screen_container"),
|
||||
bios: {
|
||||
url: "../bios/seabios.bin",
|
||||
},
|
||||
vga_bios: {
|
||||
url: "../bios/vgabios.bin",
|
||||
},
|
||||
|
||||
initrd: {
|
||||
url: './image/build/x86images/boot/initramfs-lts',
|
||||
},
|
||||
bzimage: {
|
||||
url: './image/build/x86images/boot/vmlinuz-lts',
|
||||
async: false
|
||||
},
|
||||
cmdline: 'rw root=/dev/sda init=/sbin/init rootfstype=ext4',
|
||||
// cmdline: 'rw root=/dev/sda init=/bin/bash rootfstype=ext4',
|
||||
// cmdline: "rw init=/sbin/init root=/dev/sda rootfstype=ext4",
|
||||
// cmdline: "rw init=/sbin/init root=/dev/sda rootfstype=ext4 random.trust_cpu=on 8250.nr_uarts=10 spectre_v2=off pti=off mitigations=off",
|
||||
|
||||
// cdrom: {
|
||||
// // url: "../images/al32-2024.07.10.iso",
|
||||
// url: "./image/build/x86images/rootfs.bin",
|
||||
// },
|
||||
hda: {
|
||||
buffer: arrayBuffer,
|
||||
// url: './image/build/x86images/rootfs.bin',
|
||||
async: true,
|
||||
// size: 1073741824,
|
||||
// size: 805306368,
|
||||
},
|
||||
// bzimage_initrd_from_filesystem: true,
|
||||
autostart: true,
|
||||
|
||||
network_relay_url: "wisp://127.0.0.1:3000",
|
||||
virtio_console: true,
|
||||
});
|
||||
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
const byteStream = NewCallbackByteStream();
|
||||
emulator.add_listener('virtio-console0-output-bytes',
|
||||
byteStream.listener);
|
||||
const virtioStream = NewVirtioFrameStream(byteStream);
|
||||
const wispStream = NewWispPacketStream(virtioStream);
|
||||
|
||||
class PTYManager {
|
||||
constructor ({ client }) {
|
||||
this.client = client;
|
||||
}
|
||||
init () {
|
||||
this.run_();
|
||||
}
|
||||
async run_ () {
|
||||
const handlers_ = {
|
||||
[WispPacket.INFO.id]: ({ packet }) => {
|
||||
// console.log('guess we doing info packets now', packet);
|
||||
this.client.send(packet.reflect());
|
||||
}
|
||||
};
|
||||
for await ( const packet of this.client.packetStream ) {
|
||||
// console.log('what we got here?',
|
||||
// packet.type,
|
||||
// packet,
|
||||
// );
|
||||
handlers_[packet.type.id]?.({ packet });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ptyMgr = new PTYManager({
|
||||
client: new WispClient({
|
||||
packetStream: wispStream,
|
||||
sendFn: packet => {
|
||||
emulator.bus.send(
|
||||
"virtio-console0-input-bytes",
|
||||
packet.toVirtioFrame(),
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
ptyMgr.init();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- A minimal structure for the ScreenAdapter defined in browser/screen.js -->
|
||||
<div id="screen_container">
|
||||
<div style="white-space: pre; font: 14px monospace; line-height: 14px"></div>
|
||||
<canvas style="display: none"></canvas>
|
||||
</div>
|
57
src/emulator/image/Dockerfile
Normal file
57
src/emulator/image/Dockerfile
Normal file
@ -0,0 +1,57 @@
|
||||
FROM i386/alpine:edge
|
||||
|
||||
RUN apk add --update \
|
||||
alpine-base bash ncurses shadow curl \
|
||||
linux-lts linux-firmware-none linux-headers \
|
||||
gcc make gcompat musl-dev libx11-dev xinit \
|
||||
bind-tools \
|
||||
util-linux \
|
||||
htop vim nano \
|
||||
&& \
|
||||
setup-xorg-base xhost xterm xcalc xdotool xkill || true && \
|
||||
setup-devd udev || true && \
|
||||
touch /root/.Xdefaults && \
|
||||
rm /etc/motd /etc/issue && \
|
||||
passwd -d root && \
|
||||
chsh -s /bin/bash
|
||||
|
||||
RUN apk add neofetch
|
||||
|
||||
COPY basic-boot /etc/init.d/
|
||||
RUN chmod +x /etc/init.d/basic-boot
|
||||
|
||||
COPY assets/twisp /bin/twisp
|
||||
RUN chmod u+x /bin/twisp
|
||||
COPY twisp-service /etc/init.d/
|
||||
RUN chmod +x /etc/init.d/twisp-service
|
||||
RUN rc-update add twisp-service default
|
||||
|
||||
COPY debug-service /etc/init.d/
|
||||
RUN chmod +x /etc/init.d/debug-service
|
||||
RUN rc-update add debug-service default
|
||||
|
||||
COPY initd/network-service /etc/init.d/
|
||||
RUN chmod +x /etc/init.d/network-service
|
||||
RUN rc-update add network-service default
|
||||
|
||||
# setup init system
|
||||
# COPY rc.conf /etc/rc.conf
|
||||
RUN rc-update add dmesg sysinit
|
||||
RUN rc-update add basic-boot sysinit
|
||||
|
||||
RUN rc-update add root boot
|
||||
RUN rc-update add localmount boot
|
||||
RUN rc-update add modules boot
|
||||
RUN rc-update add sysctl boot
|
||||
RUN rc-update add bootmisc boot
|
||||
RUN rc-update add syslog boot
|
||||
|
||||
RUN rc-update add mount-ro shutdown
|
||||
RUN rc-update add killprocs shutdown
|
||||
RUN rc-update add savecache shutdown
|
||||
|
||||
COPY rootfs/ /
|
||||
|
||||
RUN setup-hostname puter-alpine
|
||||
|
||||
RUN bash
|
14
src/emulator/image/basic-boot
Normal file
14
src/emulator/image/basic-boot
Normal file
@ -0,0 +1,14 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="Run Essential Boot Scripts"
|
||||
|
||||
start() {
|
||||
ebegin "Running Essential Boot Scripts"
|
||||
mount / -o remount,rw
|
||||
eend $?
|
||||
}
|
||||
|
||||
stop() {
|
||||
ebegin "Stopping Essential Boot Scripts"
|
||||
eend $?
|
||||
}
|
50
src/emulator/image/build.sh
Executable file
50
src/emulator/image/build.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -veu
|
||||
|
||||
if [ -w /var/run/docker.sock ]
|
||||
then
|
||||
echo true
|
||||
else
|
||||
echo "You aren't in the docker group, please run usermod -a -G docker $USER && newgrp docker"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
|
||||
IMAGES="$(dirname "$0")"/build/x86images
|
||||
OUT_ROOTFS_TAR="$IMAGES"/rootfs.tar
|
||||
OUT_ROOTFS_BIN="$IMAGES"/rootfs.bin
|
||||
OUT_ROOTFS_MNT="$IMAGES"/rootfs.mntpoint
|
||||
CONTAINER_NAME=alpine-full
|
||||
IMAGE_NAME=i386/alpine-full
|
||||
|
||||
rm -rf $OUT_ROOTFS_BIN || :
|
||||
|
||||
mkdir -p "$IMAGES"
|
||||
docker build . --platform linux/386 --rm --tag "$IMAGE_NAME"
|
||||
docker rm "$CONTAINER_NAME" || true
|
||||
docker create --platform linux/386 -t -i --name "$CONTAINER_NAME" "$IMAGE_NAME" bash
|
||||
|
||||
docker export "$CONTAINER_NAME" > "$OUT_ROOTFS_TAR"
|
||||
dd if=/dev/zero "of=$OUT_ROOTFS_BIN" bs=512M count=2
|
||||
|
||||
loop=$(sudo losetup -f)
|
||||
sudo losetup -P "$loop" "$OUT_ROOTFS_BIN"
|
||||
sudo mkfs.ext4 "$loop"
|
||||
mkdir -p "$OUT_ROOTFS_MNT"
|
||||
sudo mount "$loop" "$OUT_ROOTFS_MNT"
|
||||
|
||||
sudo tar -xf "$OUT_ROOTFS_TAR" -C "$OUT_ROOTFS_MNT"
|
||||
sudo cp -r "$OUT_ROOTFS_MNT/boot" "$IMAGES/boot"
|
||||
|
||||
sudo umount "$loop"
|
||||
sudo losetup -d "$loop"
|
||||
rm "$OUT_ROOTFS_TAR"
|
||||
rm -rf "$OUT_ROOTFS_MNT"
|
||||
|
||||
echo "done! created"
|
||||
sudo chown -R $USER:$USER $IMAGES/boot
|
||||
cd "$IMAGES"
|
||||
mkdir -p rootfs
|
||||
split -b50M rootfs.bin rootfs/
|
||||
cd ../
|
||||
find x86images/rootfs/* | jq -Rnc "[inputs]"
|
3
src/emulator/image/clean.sh
Executable file
3
src/emulator/image/clean.sh
Executable file
@ -0,0 +1,3 @@
|
||||
sudo umount build/x86images/rootfs.mntpoint
|
||||
sudo rm -rf ./build
|
||||
|
19
src/emulator/image/debug-service
Normal file
19
src/emulator/image/debug-service
Normal file
@ -0,0 +1,19 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="Run debug init"
|
||||
|
||||
depend() {
|
||||
after twisp-service
|
||||
}
|
||||
|
||||
start() {
|
||||
ebegin "Running Debug Init"
|
||||
echo " 🛠 bash will be on tty2"
|
||||
setsid bash < /dev/tty2 > /dev/tty2 2>&1 &
|
||||
eend $?
|
||||
}
|
||||
|
||||
stop() {
|
||||
ebegin "Stopping Debug Init"
|
||||
eend $?
|
||||
}
|
17
src/emulator/image/initd/network-service
Normal file
17
src/emulator/image/initd/network-service
Normal file
@ -0,0 +1,17 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="Run network setup"
|
||||
|
||||
start() {
|
||||
ebegin "Running network setup"
|
||||
modprobe ne2k-pci
|
||||
ifupdown ifup eth0
|
||||
ip link set lo up
|
||||
echo "nameserver 192.168.86.1" > /etc/resolv.conf
|
||||
eend $?
|
||||
}
|
||||
|
||||
stop() {
|
||||
ebegin "Stopping network setup"
|
||||
eend $?
|
||||
}
|
7
src/emulator/image/qemu.sh
Executable file
7
src/emulator/image/qemu.sh
Executable file
@ -0,0 +1,7 @@
|
||||
qemu-system-i386 \
|
||||
-kernel ./build/x86images/boot/vmlinuz-lts \
|
||||
-initrd ./build/x86images/boot/initramfs-lts \
|
||||
-append "rw root=/dev/sda console=ttyS0 init=/sbin/init rootfstype=ext4" \
|
||||
-hda ./build/x86images/rootfs.bin \
|
||||
-m 1024M \
|
||||
-nographic
|
1
src/emulator/image/rootfs/etc/hostname
Normal file
1
src/emulator/image/rootfs/etc/hostname
Normal file
@ -0,0 +1 @@
|
||||
puter-alpine
|
4
src/emulator/image/rootfs/etc/network/interfaces
Normal file
4
src/emulator/image/rootfs/etc/network/interfaces
Normal file
@ -0,0 +1,4 @@
|
||||
iface eth0 inet static
|
||||
address 192.168.86.100
|
||||
netmask 255.255.255.0
|
||||
gateway 192.168.86.1
|
1
src/emulator/image/rootfs/etc/resolv.conf
Normal file
1
src/emulator/image/rootfs/etc/resolv.conf
Normal file
@ -0,0 +1 @@
|
||||
nameserver 192.168.86.1
|
25
src/emulator/image/twisp-service
Normal file
25
src/emulator/image/twisp-service
Normal file
@ -0,0 +1,25 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="twisp daemon"
|
||||
command="/bin/twisp"
|
||||
command_args="--pty /dev/hvc0"
|
||||
pidfile="/var/run/twisp.pid"
|
||||
command_background="yes"
|
||||
start_stop_daemon_args="--background --make-pidfile"
|
||||
|
||||
depend() {
|
||||
need localmount
|
||||
after bootmisc
|
||||
}
|
||||
|
||||
start() {
|
||||
ebegin "Starting ${description}"
|
||||
start-stop-daemon --start --pidfile "${pidfile}" --background --exec ${command} -- ${command_args}
|
||||
eend $?
|
||||
}
|
||||
|
||||
stop() {
|
||||
ebegin "Stopping ${description}"
|
||||
start-stop-daemon --stop --pidfile "${pidfile}"
|
||||
eend $?
|
||||
}
|
Loading…
Reference in New Issue
Block a user