Plan 9.r post 01/B46A - 9front, qemu, lua

Tags: @9rhiz @English

DISCLAIMER: If you want to know what I discover inside the box keep on reading. If you want to know how to use the box properly please go to 9front.org. For plan9 experts reading this: I promise to do everything by the book one day and as much as I can as a go, but that is not my priority.
This is about my learning process: Everything I post worked at the time, but has probably been replace by something better or more correct by now.

A quick disclaimer. It is basically guaranteed that I am doing it wrong. Plan9 is different from the POSIX stuff I’ve been doing all my live. My assumptions will be wrong. So this is a way of doing it, definitely not the canonical one.

Goal

My goal is to keep all my shortcuts, custom keyboard layout and my modal text-editor until I have setup 9front and I am ready to switch to rio and acme for good. My brain runs to a very high degree on muscle memory and habits. If I were to switch back and forth between acme and my model text-editor, my brain would go completely bananas. Read more about my motivation in the last post.

Prerequisites

We download 9front-*.amd64.qcow2.gz from https://9front.org/iso. I first wanted to properly install 9front, but everything is already on a writable file-system in the qcow image. By scripting qemu I can easily replace or update the image. I propose that you script it yourself.

Qemu

So we start 9front like this:

» export N9F_IMG=9front-11554.amd64.qcow2
» export N9F_EXP_9FS=hostfwd=tcp:127.0.0.1:1564-:564
» export N9F_EXP_RC=hostfwd=tcp:127.0.0.1:4242-:4242
» qemu-system-x86_64 -enable-kvm \
-smp $(getconf _NPROCESSORS_ONLN) \
-cpu host -m 1024 \
-device virtio-net-pci,mac=00:20:91:37:33:77,netdev=net0 \
-netdev id=net0,user,$N9F_EXP_9FS,$N9F_EXP_RC \
-device virtio-scsi-pci,id=scsi \
-drive if=none,id=vd0,file=$N9F_IMG \
-device scsi-hd,drive=vd0 \
-display none \
-monitor none \
-serial stdio

The following line defines a virtio network device with the id net0. I have no idea why we are setting a mac-address. Everyone does it and a bit of cargo-cult was easier than to experiment with the mac-addresses, so here we go:

-device virtio-net-pci,mac=00:20:91:37:33:77,netdev=net0

Then we use

-netdev id=net0,user,$N9F_EXP_9FS,$N9F_EXP_RC

to define a user-space networking stack on top of that device. We forward the ports 564, which is 9fs. We also forward port 4242, which is my default port for anything; in this case we will forward rc the shell of plan9.

» export N9F_EXP_9FS=hostfwd=tcp:127.0.0.1:1564-:564
» export N9F_EXP_RC=hostfwd=tcp:127.0.0.1:4242-:4242

I skip over setting the storage-device. Next we disable the display aka graphics card emulation.

-display none

We disable the monitor redirection as we don’t need it.

-monitor none

But we want the serial console to be on -serial stdio. By default qemu would attach to the current pty, which means the file-descriptors of stdin/stdout would be in all kinds of strange modes that are good for someone typing but not good for scripting. Checkout man termios and there raw mode or cfmakeraw(). I used that for my typewriter-emulation but I have already forgotten what it all means.

9front

When we start 9front will ask us which device it should use as root partition.

bootargs is (tcp, tls, il, local!device)[local!/dev/sd00/fs]

We very kindly respond with enter, because it detected the root partition correctly.

user[glenda]:

We select user glenda who is presumably in all the right groups.

term% ip/ipconfig
term% cat /net/ndb
ip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2
    sys=cirno
    dns=10.0.2.3

9front will present the rc shell to us and we call ip/config, which will run dhcp and request an IP from qemu’s user-space networking. We can cat cat /net/ndb to check if it worked.

Using it from Linux

term% aux/listen1 -t tcp!*!564 exportfs -r /sys/src &
term% echo 'rc -i >[2=1]' > /tmp/term
listen started
term% aux/listen1 -t tcp!*!4242 rc /tmp/term &
listen started
term%

aux/listen1 -t proto!bind!port command means that aux/listen1 listens on proto and binds to the IP bind on port port. When a client connects, it runs the command and forwards input and output to the client. The command aux/listen1 -t tcp!*!564 exportfs -r /sys/src & therefore listens on tcp 564 and exports the 9front source at /sys/src. We can mount this using the built in 9fs support of linux, but it is very disappointing.

» doas mount -v -t 9p -o rw,trans=tcp,port=1564,noextend 127.0.0.1 mnt
mount: 127.0.0.1 mounted on /home/alter/mnt.

Whatever we do - I tried all the options in that linux-module - we get a read-only mount. This is very probably one of the instances where I am holding it wrong. But if I mount that file-system in 9front it works perfectly. I looked at the calls linux vs 9front do and it seems linux is always slightly off compared to 9front. Either noextend or version=9p2000.u should make the linux-module talk proper plan9 9fs, but either 9front doesn’t talk proper plan9 9fs or linux doesn’t.

Plan9port to the rescue, almost

» doas apk add plan9port
» /usr/lib/plan9/bin/9pfuse 'tcp!127.0.0.1!1564' mnt

We install plan9port, make sure fuse is loaded and working; we get a read-write mount that is also slightly disappointing: While linux had a perfectly faithful read-only mount, plan9port gives us a somewhat lossy read-write mount: It doesn’t list all the files. But we can open and edit the files it doesn’t show, so for now this is good enough.

Nuts shell

Since my brain is used to some kind of unix-shell since over 30 years, it goes nuts with the GUI version of rc. I will try to get used to native rc, but for now we just fix it.

echo 'rc -i >[2=1]' > /tmp/term

This command creates an rc that only uses stdin and stdout. I love the rc syntax - >[2=1] is a very expressive syntax for saying we want to merge stderr and stdout.

aux/listen1 -t tcp!*!4242 rc /tmp/term &

Next we listen on the geek-protocol port 4242 1 and we start our instrumented rc when a client connects to it.

socat READLINE,history=$HOME/.rc_history TCP4:127.0.0.1:4242

On the host we connect to the with socat and READLINE. And just like that we ignored all the good things about plan9 and assimilated it into our borg collective.

How to script it

I use my own terrible implementation of an interface that is a bit like pythons Popen. The part where I prevent IO deadlocks in single-threaded lua, is actually quite good, but everything else isn’t. You find the reasons in the repository of my terrible lua code. I also commited the qemu-9front script to the repo: examples/9front.

function qemu:runner()
    local proc = Popen {
        "qemu-system-x86_64", "-enable-kvm", "-smp", "4",
        "-cpu", "host", "-m", "1024",
        "-device", "virtio-net-pci,mac=00:20:91:37:33:77,netdev=net0",
        "-netdev", "user,id=net0," ..
    "hostfwd=tcp:127.0.0.1:1564-:564," ..
    "hostfwd=tcp:127.0.0.1:4242-:4242",
        "-device", "virtio-scsi-pci,id=scsi",
        "-drive", "if=none,id=vd0,file=" .. img,
        "-device", "scsi-hd,drive=vd0",
        "-display", "none", "-monitor", "none",
        "-serial", "stdio",
        stdin = Popen.pipe,
        stdout = Popen.pipe
    }
    [...]
    repeat
        local new = self:check(proc.stdout)
        self:display(new)
        u.extend(self.buf, new)
        self:current()
    until self.done or proc.stdout:is_closed()
end

We accumulate the output of 9front in self.buf until we find the expected output.

function qemu:stage0()
    if u.tcat(self.buf):match("\nbootargs is") then
        self.qemin:write("\n")
        self.current = self.stage1
        self.buf = { "\n" }
    end
end

Then we write the next command, we move the state-machine to the next state (stage1) and we reset the accumulator self.buf. We do that till we have done the same we did manually.

Moral of the Story

We have learned that my brain likes nuts and bananas, as well as a wrong way of incrementally getting into 9front. Next I hope to fix some of the quirks 9front has on my hardware.


  1. I just invented that name.↩︎