├── .dockerignore ├── .github └── workflows │ └── push-dockerhub.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── assets ├── assets.go ├── console.html ├── duplex.min.js ├── env86.js └── index.html ├── cmd ├── env86 │ ├── assets.go │ ├── boot.go │ ├── create.go │ ├── main.go │ ├── network.go │ ├── prepare.go │ ├── pull.go │ ├── run.go │ ├── serve.go │ └── v86util.go └── guest86 │ ├── exec.go │ ├── fs.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── mount.go │ ├── tcp.go │ └── tty.go ├── config.go ├── console.go ├── fsutil ├── copy.go └── path.go ├── go.mod ├── go.sum ├── guest.go ├── http.go ├── image.go ├── misc.go ├── namespacefs ├── dirsfile.go └── fs.go ├── network └── handler.go ├── scripts ├── Dockerfile.build ├── Dockerfile.example ├── Dockerfile.kernel ├── Dockerfile.v86 └── local.conf ├── tarfs ├── file.go └── fs.go ├── tty.go └── vm.go /.dockerignore: -------------------------------------------------------------------------------- 1 | assets/*.bin 2 | assets/libv86.js 3 | assets/v86.wasm 4 | assets/guest86 5 | /env86 6 | /NOTES 7 | /local 8 | /dist -------------------------------------------------------------------------------- /.github/workflows/push-dockerhub.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Hub image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | push_to_registry: 10 | name: Push Docker image to Docker Hub 11 | runs-on: ubuntu-latest 12 | permissions: 13 | packages: write 14 | contents: read 15 | attestations: write 16 | id-token: write 17 | steps: 18 | - name: Check out the repo 19 | uses: actions/checkout@v4 20 | 21 | - name: Log in to Docker Hub 22 | uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a 23 | with: 24 | username: ${{ secrets.DOCKER_USERNAME }} 25 | password: ${{ secrets.DOCKER_PASSWORD }} 26 | 27 | - name: Build and push Docker image 28 | id: push 29 | uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 30 | with: 31 | context: . 32 | file: ./scripts/Dockerfile.build 33 | push: true 34 | tags: progrium/env86:latest 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | assets/*.bin 2 | assets/libv86.js 3 | assets/v86.wasm 4 | assets/guest86 5 | /env86 6 | /NOTES 7 | /local 8 | /dist 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | checksum: 5 | disable: true 6 | snapshot: 7 | name_template: '{{envOrDefault "VERSION" .ShortCommit}}' 8 | builds: 9 | - id: default 10 | goos: 11 | - linux 12 | - windows 13 | goarch: 14 | - amd64 15 | - arm64 16 | ldflags: "-X main.Version={{.Version}}" 17 | main: ./cmd/env86 18 | - id: mac 19 | goos: 20 | - darwin 21 | goarch: 22 | - amd64 23 | - arm64 24 | ldflags: "-X main.Version={{.Version}}" 25 | main: ./cmd/env86 26 | hooks: 27 | post: "codesign --deep --force --verify --verbose --timestamp --options runtime --sign 'Developer ID Application: Jeff Lindsay (4HSU97X8UX)' {{ .Path }}" 28 | archives: 29 | - id: default 30 | builds: 31 | - default 32 | - mac 33 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{.Os}}_{{.Arch}}' 34 | format: zip 35 | wrap_in_directory: false 36 | files: 37 | - none* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jeff Lindsay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install build assets release guest v86 kernel 2 | 3 | VERSION=0.2dev 4 | 5 | ifeq ($(OS),Windows_NT) 6 | ASSETS_DIR := .\assets 7 | ENV86 := .\env86.exe 8 | else 9 | ASSETS_DIR := ./assets 10 | ENV86 := ./env86 11 | endif 12 | 13 | all: assets build 14 | 15 | 16 | build: 17 | go build -ldflags="-X 'main.Version=${VERSION}'" -o $(ENV86) ./cmd/env86 18 | 19 | install: build 20 | mv ./env86 /usr/local/bin/env86 21 | 22 | release: 23 | VERSION=$(VERSION) goreleaser release --snapshot --clean 24 | 25 | 26 | assets: guest kernel v86 27 | 28 | guest: export GOOS=linux 29 | guest: export GOARCH=386 30 | guest: 31 | cd ./cmd/guest86 && go build -o ../../assets/guest86 . 32 | 33 | kernel: 34 | docker build --platform=linux/386 -t env86-kernel -f ./scripts/Dockerfile.kernel ./scripts 35 | docker run --rm --platform=linux/386 -v $(ASSETS_DIR):/dst env86-kernel 36 | 37 | v86: 38 | docker build -t env86-v86 -f ./scripts/Dockerfile.v86 ./scripts 39 | docker run --rm -v $(ASSETS_DIR):/dst env86-v86 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # env86 2 | 3 | Embeddable virtual machines based on the [v86](https://github.com/copy/v86) emulator. 4 | 5 | ## Features 6 | 7 | * Run, author, and debug web embeddable VMs via CLI tool 8 | * Network VMs with full virtual network stack 9 | * Prepare HTML assets for easy deploying to static hosts 10 | 11 | 12 | ## Getting Started 13 | 14 | You can build env86 using Go, but the static assets it bundles are set up with Docker. With both installed, 15 | and a cloned repo, you can run: 16 | 17 | ```sh 18 | make all 19 | ``` 20 | 21 | This will use Docker to create some static assets like [v86](https://github.com/copy/v86), 22 | then it will use Go to compile `env86` and write the executable to the current directory. Move this into your `PATH` or you can run `./env86` and get: 23 | 24 | ``` 25 | Usage: 26 | env86 [command] 27 | 28 | Available Commands: 29 | boot boot and run a VM 30 | prepare prepare a VM for publishing on the web 31 | network run virtual network and relay 32 | serve serve a VM and debug console over HTTP 33 | create create an image from directory or using Docker 34 | 35 | Flags: 36 | -v show version 37 | 38 | Use "env86 [command] -help" for more information about a command. 39 | ``` 40 | 41 | ### Creating Images 42 | 43 | `env86` images are directories or tarballs with an `image.json` file and some v86 specific files 44 | for the filesystem and initial state. With the `create` subcommand you can make an image from a Linux root directory 45 | or using Docker, either an image to pull or a Dockerfile to build. Here's a Dockerfile to make an 46 | Alpine Linux image: 47 | 48 | ```Dockerfile 49 | FROM i386/alpine:3.18.6 50 | RUN apk add openrc agetty 51 | RUN sed -i 's/getty 38400 tty1/agetty --autologin root tty1 linux/' /etc/inittab 52 | RUN echo 'ttyS0::once:/sbin/agetty --autologin root -s ttyS0 115200 vt100' >> /etc/inittab 53 | RUN echo "root:root" | chpasswd 54 | ``` 55 | 56 | The v86 emulator is 32-bit x86, so this is a 32-bit based Alpine. Any Docker commands run are run with 57 | `--platform=linux/386` behind the scenes, so always keep this in mind when making images. We can build this 58 | image and write it to `./alpine-vm`: 59 | 60 | ```sh 61 | env86 create --from-docker=./path/to/Dockerfile ./alpine-vm 62 | ``` 63 | 64 | ### Booting VMs 65 | 66 | Once we have an env86 image, we can boot it. Booting has the most options: 67 | 68 | ``` 69 | Usage: 70 | env86 boot 71 | 72 | Flags: 73 | -cdp 74 | use headless chrome 75 | -cold 76 | cold boot without initial state 77 | -console-url 78 | show the URL to the console 79 | -n 80 | enable networking (shorthand) 81 | -net 82 | enable networking 83 | -no-console 84 | disable console window 85 | -no-keyboard 86 | disable keyboard 87 | -no-mouse 88 | disable mouse 89 | -p string 90 | forward TCP port (ex: 8080:80) 91 | -save 92 | save initial state to image on exit 93 | -ttyS0 94 | open TTY over serial0 95 | ``` 96 | 97 | We can boot our Alpine VM with `--save` so we can skip cold booting in future boots: 98 | 99 | ```sh 100 | env86 boot --save ./alpine-vm 101 | ``` 102 | 103 | This should pop open a window showing the screen console (though please submit an issue if this isn't 104 | working on Windows or Linux). Once it gets to the shell, at the terminal we can hit `Ctrl+D` to send EOF, 105 | which terminates the VM, but with `--save` it will first save the current state to the initial state of the image. 106 | 107 | Now if we boot again, it should restore back to the prompt we ended it at. If we ever don't want this, 108 | we can pass `--cold` to cold boot without restoring initial state. 109 | 110 | We can also boot without the console window and just interact with the VM via ttyS0 in the terminal: 111 | 112 | ```sh 113 | env86 boot --ttyS0 --no-console ./alpine-vm 114 | ``` 115 | 116 | ### Publishing VMs 117 | 118 | Once an image is in a state you want to share and you want to make it run on the web, you can use `prepare` to 119 | generate all the static files needed to serve this VM over HTTP: 120 | 121 | ```sh 122 | env86 prepare ./alpine-vm www 123 | ``` 124 | 125 | This will make a `www` directory with an example `index.html` and all the files that need to be served over 126 | HTTP to run this VM in the browser including the `v86.wasm` file. The image files are slightly different when prepared, splitting the initial state into 10MB parts for more efficiently loading over the web. 127 | 128 | ### Networking 129 | 130 | If you boot with `--net` a virtual network stack and switch is created and wired up to the VM virtual NIC that will forward packets to your host computer network. The guest image will need to have network drivers and then be configured *after* booting to use the Internet. Here is a Dockerfile to make an env86 image that has a `./networking.sh` script to run after 131 | booting to use the network provided by `--net`: 132 | 133 | ```Dockerfile 134 | FROM i386/alpine:3.18.6 135 | 136 | ENV KERNEL=lts 137 | ENV HOSTNAME=localhost 138 | ENV PASSWORD='root' 139 | 140 | RUN apk add openrc \ 141 | alpine-base \ 142 | agetty \ 143 | alpine-conf 144 | 145 | # Install mkinitfs from edge (todo: remove this when 3.19+ has worked properly with 9pfs) 146 | RUN apk add mkinitfs --no-cache --allow-untrusted --repository https://dl-cdn.alpinelinux.org/alpine/edge/main/ 147 | 148 | RUN if [ "$KERNEL" == "lts" ]; then \ 149 | apk add linux-lts \ 150 | linux-firmware-none \ 151 | linux-firmware-sb16; \ 152 | else \ 153 | apk add linux-$KERNEL; \ 154 | fi 155 | 156 | # Adding networking.sh script (works only on lts kernel yet) 157 | RUN if [ "$KERNEL" == "lts" ]; then \ 158 | echo -e "echo '127.0.0.1 localhost' >> /etc/hosts && rmmod ne2k-pci && modprobe ne2k-pci\nhwclock -s\nsetup-interfaces -a -r" > /root/networking.sh && \ 159 | chmod +x /root/networking.sh; \ 160 | fi 161 | 162 | RUN sed -i 's/getty 38400 tty1/agetty --autologin root tty1 linux/' /etc/inittab 163 | RUN echo 'ttyS0::once:/sbin/agetty --autologin root -s ttyS0 115200 vt100' >> /etc/inittab 164 | RUN echo "root:$PASSWORD" | chpasswd 165 | 166 | # https://wiki.alpinelinux.org/wiki/Alpine_Linux_in_a_chroot#Preparing_init_services 167 | RUN for i in devfs dmesg mdev hwdrivers; do rc-update add $i sysinit; done 168 | RUN for i in hwclock modules sysctl hostname bootmisc; do rc-update add $i boot; done 169 | RUN rc-update add killprocs shutdown 170 | 171 | # Generate initramfs with 9p modules 172 | RUN mkinitfs -F "ata base ide scsi virtio ext4 9p" $(cat /usr/share/kernel/$KERNEL/kernel.release) 173 | ``` 174 | 175 | Then we can run: 176 | 177 | ```sh 178 | env86 create --from-docker=./path/to/Dockerfile ./alpine-net 179 | env86 boot --net --save ./alpine-net 180 | ``` 181 | 182 | At the prompt we can run `./networking.sh` and it should get an IP and be able to connect to the Internet. 183 | 184 | We can use networking from the browser if we add `network_relay_url` to the config passed to `env86.boot()` in `index.html`. We can run `env86 network` to start a virtual network and get a URL to use for `network_relay_url`. 185 | 186 | ### More Features 187 | 188 | A few more features are tucked away or are in progress. The next major focus is on a standard guest service 189 | for Linux VMs that will open up more functionality in `env86` like Docker-style `run` and `build` commands, 190 | mounting local directories in the VM, and more. 191 | 192 | ### Using as a Library 193 | 194 | The `env86` command line tool is a wrapper around a Go library you can use to work with and run VMs in regular 195 | Go programs outside the browser. 196 | 197 | 198 | ## Thanks 199 | 200 | This project was made possible by [my sponsors](https://github.com/sponsors/progrium) but also the amazing work of the [v86](https://github.com/copy/v86) team. Also thanks to Joël and Adam for introducing me to v86. 201 | 202 | ## License 203 | 204 | MIT -------------------------------------------------------------------------------- /assets/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/progrium/env86/fsutil" 10 | 11 | "github.com/evanw/esbuild/pkg/api" 12 | "tractor.dev/toolkit-go/engine/fs" 13 | "tractor.dev/toolkit-go/engine/fs/osfs" 14 | ) 15 | 16 | //go:embed * 17 | var assets embed.FS 18 | 19 | var Dir = fs.LiveDir(assets) 20 | 21 | func BundleJavaScript() ([]byte, error) { 22 | tmpDir, err := os.MkdirTemp("", "env86-js") 23 | if err != nil { 24 | return nil, err 25 | } 26 | defer os.RemoveAll(tmpDir) 27 | 28 | if err := fsutil.CopyFS(Dir, "env86.js", osfs.New(), filepath.Join(tmpDir, "env86.js")); err != nil { 29 | return nil, err 30 | } 31 | if err := fsutil.CopyFS(Dir, "duplex.min.js", osfs.New(), filepath.Join(tmpDir, "duplex.min.js")); err != nil { 32 | return nil, err 33 | } 34 | 35 | result := api.Build(api.BuildOptions{ 36 | EntryPoints: []string{filepath.Join(tmpDir, "env86.js")}, 37 | Bundle: true, 38 | Outdir: tmpDir, 39 | MinifyWhitespace: true, 40 | MinifyIdentifiers: true, 41 | MinifySyntax: true, 42 | Format: api.FormatESModule, 43 | Platform: api.PlatformBrowser, 44 | }) 45 | if len(result.Errors) > 0 { 46 | return nil, fmt.Errorf("esbuild: %s [%s:%d]", 47 | result.Errors[0].Text, 48 | filepath.Base(result.Errors[0].Location.File), 49 | result.Errors[0].Location.Line) 50 | } 51 | 52 | libv86, err := fs.ReadFile(Dir, "libv86.js") 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return append(libv86, result.OutputFiles[0].Contents...), nil 58 | } 59 | -------------------------------------------------------------------------------- /assets/console.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 |
23 |
24 | 25 |
26 | 37 | 38 | -------------------------------------------------------------------------------- /assets/duplex.min.js: -------------------------------------------------------------------------------- 1 | var hr=Object.defineProperty;var yr=(t,e)=>{for(var r in e)hr(t,r,{get:e[r],enumerable:!0})};var zt=class{debug;constructor(e=!1){this.debug=e}encoder(e){return new nt(e,this.debug)}decoder(e){return new st(e,this.debug)}},nt=class{w;enc;debug;constructor(e,r=!1){this.w=e,this.enc=new TextEncoder,this.debug=r}async encode(e){this.debug&&console.log("<<",e);let r=this.enc.encode(JSON.stringify(e)),n=0;for(;n>",s),Promise.resolve(s)}};var it;try{it=new TextDecoder}catch{}var p,ee,f=0;var qt=[],pr=105,mr=57342,xr=57343,Nt=57337;var Ht=6,ae={},ot=qt,at=0,U={},O,Pe,ke=0,ge=0,R,H,M=[],ct=[],T,V,we,jt={useRecords:!1,mapsAsObjects:!0},be=!1,te=class t{constructor(e){if(e&&((e.keyMap||e._keyMap)&&!e.useRecords&&(e.useRecords=!1,e.mapsAsObjects=!0),e.useRecords===!1&&e.mapsAsObjects===void 0&&(e.mapsAsObjects=!0),e.getStructures&&(e.getShared=e.getStructures),e.getShared&&!e.structures&&((e.structures=[]).uninitialized=!0),e.keyMap)){this.mapKey=new Map;for(let[r,n]of Object.entries(e.keyMap))this.mapKey.set(n,r)}Object.assign(this,e)}decodeKey(e){return this.keyMap&&this.mapKey.get(e)||e}encodeKey(e){return this.keyMap&&this.keyMap.hasOwnProperty(e)?this.keyMap[e]:e}encodeKeys(e){if(!this._keyMap)return e;let r=new Map;for(let[n,s]of Object.entries(e))r.set(this._keyMap.hasOwnProperty(n)?this._keyMap[n]:n,s);return r}decodeKeys(e){if(!this._keyMap||e.constructor.name!="Map")return e;if(!this._mapKey){this._mapKey=new Map;for(let[n,s]of Object.entries(this._keyMap))this._mapKey.set(s,n)}let r={};return e.forEach((n,s)=>r[v(this._mapKey.has(s)?this._mapKey.get(s):s)]=n),r}mapDecode(e,r){let n=this.decode(e);if(this._keyMap)switch(n.constructor.name){case"Array":return n.map(s=>this.decodeKeys(s))}return n}decode(e,r){if(p)return Xt(()=>(Oe(),this?this.decode(e,r):t.prototype.decode.call(jt,e,r)));ee=r>-1?r:e.length,f=0,at=0,ge=0,Pe=null,ot=qt,R=null,p=e;try{V=e.dataView||(e.dataView=new DataView(e.buffer,e.byteOffset,e.byteLength))}catch(n){throw p=null,e instanceof Uint8Array?n:new Error("Source must be a Uint8Array or Buffer but was a "+(e&&typeof e=="object"?e.constructor.name:typeof e))}if(this instanceof t){if(U=this,T=this.sharedValues&&(this.pack?new Array(this.maxPrivatePackedValues||16).concat(this.sharedValues):this.sharedValues),this.structures)return O=this.structures,Se();(!O||O.length>0)&&(O=[])}else U=jt,(!O||O.length>0)&&(O=[]),T=null;return Se()}decodeMultiple(e,r){let n,s=0;try{let o=e.length;be=!0;let l=this?this.decode(e,o):ht.decode(e,o);if(r){if(r(l)===!1)return;for(;f=R.postBundlePosition){let e=new Error("Unexpected bundle position");throw e.incomplete=!0,e}f=R.postBundlePosition,R=null}if(f==ee)O=null,p=null,H&&(H=null);else if(f>ee){let e=new Error("Unexpected end of CBOR data");throw e.incomplete=!0,e}else if(!be)throw new Error("Data read, but end of buffer not reached");return t}catch(t){throw Oe(),(t instanceof RangeError||t.message.startsWith("Unexpected end of buffer"))&&(t.incomplete=!0),t}}function D(){let t=p[f++],e=t>>5;if(t=t&31,t>23)switch(t){case 24:t=p[f++];break;case 25:if(e==7)return Ir();t=V.getUint16(f),f+=2;break;case 26:if(e==7){let r=V.getFloat32(f);if(U.useFloat32>2){let n=Me[(p[f]&127)<<1|p[f+1]>>7];return f+=4,(n*r+(r>0?.5:-.5)>>0)/n}return f+=4,r}t=V.getUint32(f),f+=4;break;case 27:if(e==7){let r=V.getFloat64(f);return f+=8,r}if(e>1){if(V.getUint32(f)>0)throw new Error("JavaScript does not support arrays, maps, or strings with length over 4294967295");t=V.getUint32(f+4)}else U.int64AsNumber?(t=V.getUint32(f)*4294967296,t+=V.getUint32(f+4)):t=V.getBigUint64(f);f+=8;break;case 31:switch(e){case 2:case 3:throw new Error("Indefinite length not supported for byte or text strings");case 4:let r=[],n,s=0;for(;(n=D())!=ae;)r[s++]=n;return e==4?r:e==3?r.join(""):Buffer.concat(r);case 5:let o;if(U.mapsAsObjects){let l={};if(U.keyMap)for(;(o=D())!=ae;)l[v(U.decodeKey(o))]=D();else for(;(o=D())!=ae;)l[v(o)]=D();return l}else{we&&(U.mapsAsObjects=!0,we=!1);let l=new Map;if(U.keyMap)for(;(o=D())!=ae;)l.set(U.decodeKey(o),D());else for(;(o=D())!=ae;)l.set(o,D());return l}case 7:return ae;default:throw new Error("Invalid major type for indefinite length "+e)}default:throw new Error("Unknown token "+t)}switch(e){case 0:return t;case 1:return~t;case 2:return br(t);case 3:if(ge>=f)return Pe.slice(f-ke,(f+=t)-ke);if(ge==0&&ee<140&&t<32){let s=t<16?Gt(t):gr(t);if(s!=null)return s}return wr(t);case 4:let r=new Array(t);for(let s=0;s=Nt){let s=O[t&8191];if(s)return s.read||(s.read=lt(s)),s.read();if(t<65536){if(t==xr){let o=le(),l=D(),m=D();dt(l,m);let g={};if(U.keyMap)for(let x=2;x23)switch(r){case 24:r=p[f++];break;case 25:r=V.getUint16(f),f+=2;break;case 26:r=V.getUint32(f),f+=4;break;default:throw new Error("Expected array header, but got "+p[f-1])}let n=this.compiledReader;for(;n;){if(n.propertyCount===r)return n(D);n=n.next}if(this.slowReads++>=3){let o=this.length==r?this:this.slice(0,r);return n=U.keyMap?new Function("r","return {"+o.map(l=>U.decodeKey(l)).map(l=>Kt.test(l)?v(l)+":r()":"["+JSON.stringify(l)+"]:r()").join(",")+"}"):new Function("r","return {"+o.map(l=>Kt.test(l)?v(l)+":r()":"["+JSON.stringify(l)+"]:r()").join(",")+"}"),this.compiledReader&&(n.next=this.compiledReader),n.propertyCount=r,this.compiledReader=n,n(D)}let s={};if(U.keyMap)for(let o=0;o64&&it)return it.decode(p.subarray(f,f+=t));let r=f+t,n=[];for(e="";f65535&&(g-=65536,n.push(g>>>10&1023|55296),g=56320|g&1023),n.push(g)}else n.push(s);n.length>=4096&&(e+=B.apply(String,n),n.length=0)}return n.length>0&&(e+=B.apply(String,n)),e}var B=String.fromCharCode;function gr(t){let e=f,r=new Array(t);for(let n=0;n0){f=e;return}r[n]=s}return B.apply(String,r)}function Gt(t){if(t<4)if(t<2){if(t===0)return"";{let e=p[f++];if((e&128)>1){f-=1;return}return B(e)}}else{let e=p[f++],r=p[f++];if((e&128)>0||(r&128)>0){f-=2;return}if(t<3)return B(e,r);let n=p[f++];if((n&128)>0){f-=3;return}return B(e,r,n)}else{let e=p[f++],r=p[f++],n=p[f++],s=p[f++];if((e&128)>0||(r&128)>0||(n&128)>0||(s&128)>0){f-=4;return}if(t<6){if(t===4)return B(e,r,n,s);{let o=p[f++];if((o&128)>0){f-=5;return}return B(e,r,n,s,o)}}else if(t<8){let o=p[f++],l=p[f++];if((o&128)>0||(l&128)>0){f-=6;return}if(t<7)return B(e,r,n,s,o,l);let m=p[f++];if((m&128)>0){f-=7;return}return B(e,r,n,s,o,l,m)}else{let o=p[f++],l=p[f++],m=p[f++],g=p[f++];if((o&128)>0||(l&128)>0||(m&128)>0||(g&128)>0){f-=8;return}if(t<10){if(t===8)return B(e,r,n,s,o,l,m,g);{let x=p[f++];if((x&128)>0){f-=9;return}return B(e,r,n,s,o,l,m,g,x)}}else if(t<12){let x=p[f++],k=p[f++];if((x&128)>0||(k&128)>0){f-=10;return}if(t<11)return B(e,r,n,s,o,l,m,g,x,k);let S=p[f++];if((S&128)>0){f-=11;return}return B(e,r,n,s,o,l,m,g,x,k,S)}else{let x=p[f++],k=p[f++],S=p[f++],F=p[f++];if((x&128)>0||(k&128)>0||(S&128)>0||(F&128)>0){f-=12;return}if(t<14){if(t===12)return B(e,r,n,s,o,l,m,g,x,k,S,F);{let W=p[f++];if((W&128)>0){f-=13;return}return B(e,r,n,s,o,l,m,g,x,k,S,F,W)}}else{let W=p[f++],z=p[f++];if((W&128)>0||(z&128)>0){f-=14;return}if(t<15)return B(e,r,n,s,o,l,m,g,x,k,S,F,W,z);let q=p[f++];if((q&128)>0){f-=15;return}return B(e,r,n,s,o,l,m,g,x,k,S,F,W,z,q)}}}}}function br(t){return U.copyBuffers?Uint8Array.prototype.slice.call(p,f,f+=t):p.subarray(f,f+=t)}var Yt=new Float32Array(1),Ce=new Uint8Array(Yt.buffer,0,4);function Ir(){let t=p[f++],e=p[f++],r=(t&127)>>2;if(r===31)return e||t&3?NaN:t&128?-1/0:1/0;if(r===0){let n=((t&3)<<8|e)/16777216;return t&128?-n:n}return Ce[3]=t&128|(r>>1)+56,Ce[2]=(t&7)<<5|e>>3,Ce[1]=e<<5,Ce[0]=0,Yt[0]}var en=new Array(4096);var j=class{constructor(e,r){this.value=e,this.tag=r}};M[0]=t=>new Date(t);M[1]=t=>new Date(Math.round(t*1e3));M[2]=t=>{let e=BigInt(0);for(let r=0,n=t.byteLength;rBigInt(-1)-M[2](t);M[4]=t=>+(t[1]+"e"+t[0]);M[5]=t=>t[1]*Math.exp(t[0]*Math.log(2));var dt=(t,e)=>{t=t-57344;let r=O[t];r&&r.isShared&&((O.restoreStructures||(O.restoreStructures=[]))[t]=r),O[t]=e,e.read=lt(e)};M[pr]=t=>{let e=t.length,r=t[1];dt(t[0],r);let n={};for(let s=2;sR?R[0].slice(R.position0,R.position0+=t):new j(t,14);M[15]=t=>R?R[1].slice(R.position1,R.position1+=t):new j(t,15);var Ar={Error,RegExp};M[27]=t=>(Ar[t[0]]||Error)(t[1],t[2]);var Jt=t=>{if(p[f++]!=132)throw new Error("Packed values structure must be followed by a 4 element array");let e=t();return T=T?e.concat(T.slice(e.length)):e,T.prefixes=t(),T.suffixes=t(),t()};Jt.handlesRead=!0;M[51]=Jt;M[Ht]=t=>{if(!T)if(U.getShared)ut();else return new j(t,Ht);if(typeof t=="number")return T[16+(t>=0?2*t:-2*t-1)];throw new Error("No support for non-integer packed references yet")};M[28]=t=>{H||(H=new Map,H.id=0);let e=H.id++,r=p[f],n;r>>5==4?n=[]:n={};let s={target:n};H.set(e,s);let o=t();return s.used?Object.assign(n,o):(s.target=o,o)};M[28].handlesRead=!0;M[29]=t=>{let e=H.get(t);return e.used=!0,e.target};M[258]=t=>new Set(t);(M[259]=t=>(U.mapsAsObjects&&(U.mapsAsObjects=!1,we=!0),t())).handlesRead=!0;function ce(t,e){return typeof t=="string"?t+e:t instanceof Array?t.concat(e):Object.assign({},t,e)}function Q(){if(!T)if(U.getShared)ut();else throw new Error("No packed values available");return T}var Dr=1399353956;ct.push((t,e)=>{if(t>=225&&t<=255)return ce(Q().prefixes[t-224],e);if(t>=28704&&t<=32767)return ce(Q().prefixes[t-28672],e);if(t>=1879052288&&t<=2147483647)return ce(Q().prefixes[t-1879048192],e);if(t>=216&&t<=223)return ce(e,Q().suffixes[t-216]);if(t>=27647&&t<=28671)return ce(e,Q().suffixes[t-27639]);if(t>=1811940352&&t<=1879048191)return ce(e,Q().suffixes[t-1811939328]);if(t==Dr)return{packedValues:T,structures:O.slice(0),version:e};if(t==55799)return e});var Ur=new Uint8Array(new Uint16Array([1]).buffer)[0]==1,$t=[Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array,typeof BigUint64Array>"u"?{name:"BigUint64Array"}:BigUint64Array,Int8Array,Int16Array,Int32Array,typeof BigInt64Array>"u"?{name:"BigInt64Array"}:BigInt64Array,Float32Array,Float64Array],Er=[64,68,69,70,71,72,77,78,79,85,86];for(let t=0;t<$t.length;t++)Sr($t[t],Er[t]);function Sr(t,e){let r="get"+t.name.slice(0,-5);typeof t!="function"&&(t=null);let n=t.BYTES_PER_ELEMENT;for(let s=0;s<2;s++){if(!s&&n==1)continue;let o=n==2?1:n==4?2:3;M[s?e:e-4]=n==1||s==Ur?l=>{if(!t)throw new Error("Could not find typed array for code "+e);return new t(Uint8Array.prototype.slice.call(l,0).buffer)}:l=>{if(!t)throw new Error("Could not find typed array for code "+e);let m=new DataView(l.buffer,l.byteOffset,l.byteLength),g=l.length>>o,x=new t(g),k=m[r];for(let S=0;S23)switch(t){case 24:t=p[f++];break;case 25:t=V.getUint16(f),f+=2;break;case 26:t=V.getUint32(f),f+=4;break}return t}function ut(){if(U.getShared){let t=Xt(()=>(p=null,U.getShared()))||{},e=t.structures||[];U.sharedVersion=t.version,T=U.sharedValues=t.packedValues,O===!0?U.structures=O=e:O.splice.apply(O,[0,e.length].concat(e))}}function Xt(t){let e=ee,r=f,n=at,s=ke,o=ge,l=Pe,m=ot,g=H,x=R,k=new Uint8Array(p.slice(0,ee)),S=O,F=U,W=be,z=t();return ee=e,f=r,at=n,ke=s,ge=o,Pe=l,ot=m,H=g,R=x,p=k,be=W,O=S,U=F,V=new DataView(p.buffer,p.byteOffset,p.byteLength),z}function Oe(){p=null,H=null,O=null}function Zt(t){M[t.tag]=t.decode}var Me=new Array(147);for(let t=0;t<256;t++)Me[t]=+("1e"+Math.floor(45.15-t*.30103));var ht=new te({useRecords:!1}),yt=ht.decode,Pr=ht.decodeMultiple,Re={NEVER:0,ALWAYS:1,DECIMAL_ROUND:3,DECIMAL_FIT:4};var _e;try{_e=new TextEncoder}catch{}var Fe,bt,Ve=globalThis.Buffer,Ae=typeof Ve<"u",pt=Ae?Ve.allocUnsafeSlow:Uint8Array,Qt=Ae?Ve:Uint8Array,er=256,tr=Ae?4294967296:2144337920;var mt,a,P,i=0,Y,_=null,kr=61440,Or=/[\u0080-\uFFFF]/,L=Symbol("record-id"),Ie=class extends te{constructor(e){super(e),this.offset=0;let r,n,s,o,l,m;e=e||{};let g=Qt.prototype.utf8Write?function(c,y,d){return a.utf8Write(c,y,d)}:_e&&_e.encodeInto?function(c,y){return _e.encodeInto(c,a.subarray(y)).written}:!1,x=this,k=e.structures||e.saveStructures,S=e.maxSharedStructures;if(S==null&&(S=k?128:0),S>8190)throw new Error("Maximum maxSharedStructure is 8190");let F=e.sequential;F&&(S=0),this.structures||(this.structures=[]),this.saveStructures&&(this.saveShared=this.saveStructures);let W,z,q=e.sharedValues,N;if(q){N=Object.create(null);for(let c=0,y=q.length;cthis.encodeKeys(d));break}return this.encode(c,y)},this.encode=function(c,y){if(a||(a=new pt(8192),P=new DataView(a.buffer,0,8192),i=0),Y=a.length-10,Y-i<2048?(a=new pt(a.length),P=new DataView(a.buffer,0,a.length),Y=a.length-10,i=0):y===gt&&(i=i+7&2147483640),n=i,x.useSelfDescribedHeader&&(P.setUint32(i,3654940416),i+=3),m=x.structuredClone?new Map:null,x.bundleStrings&&typeof c!="string"?(_=[],_.size=1/0):_=null,s=x.structures,s){if(s.uninitialized){let h=x.getShared()||{};x.structures=s=h.structures||[],x.sharedVersion=h.version;let u=x.sharedValues=h.packedValues;if(u){N={};for(let w=0,b=u.length;wS&&!F&&(d=S),!s.transitions){s.transitions=Object.create(null);for(let h=0;h0){a[i++]=216,a[i++]=51,$(4);let h=d.values;C(h),$(0),$(0),z=Object.create(N||null);for(let u=0,w=h.length;uY&&me(i),x.offset=i;let d=_r(a.subarray(n,i),m.idsToInsert);return m=null,d}return y>?(a.start=n,a.end=i,a):a.subarray(n,i)}finally{if(s){if(Ee<10&&Ee++,s.length>S&&(s.length=S),et>1e4)s.transitions=null,Ee=0,et=0,G.length>0&&(G=[]);else if(G.length>0&&!F){for(let d=0,h=G.length;dS&&(x.structures=x.structures.slice(0,S));let d=a.subarray(n,i);return x.updateSharedData()===!1?x.encode(c):d}y&vr&&(i=n)}},this.findCommonStringsToPack=()=>(W=new Map,N||(N=Object.create(null)),c=>{let y=c&&c.threshold||4,d=this.pack?c.maxPrivatePackedValues||16:0;q||(q=this.sharedValues=[]);for(let[h,u]of W)u.count>y&&(N[h]=d++,q.push(h),o=!0);for(;this.saveShared&&this.updateSharedData()===!1;);W=null});let C=c=>{i>Y&&(a=me(i));var y=typeof c,d;if(y==="string"){if(z){let b=z[c];if(b>=0){b<16?a[i++]=b+224:(a[i++]=198,b&1?C(15-b>>1):C(b-16>>1));return}else if(W&&!e.pack){let I=W.get(c);I?I.count++:W.set(c,{count:1})}}let h=c.length;if(_&&h>=4&&h<1024){if((_.size+=h)>kr){let I,A=(_[0]?_[0].length*3+_[1].length:0)+10;i+A>Y&&(a=me(i+A)),a[i++]=217,a[i++]=223,a[i++]=249,a[i++]=_.position?132:130,a[i++]=26,I=i-n,i+=4,_.position&&sr(n,C),_=["",""],_.size=0,_.position=I}let b=Or.test(c);_[b?0:1]+=c,a[i++]=b?206:207,C(h);return}let u;h<32?u=1:h<256?u=2:h<65536?u=3:u=5;let w=h*3;if(i+w>Y&&(a=me(i+w)),h<64||!g){let b,I,A,E=i+u;for(b=0;b>6|192,a[E++]=I&63|128):(I&64512)===55296&&((A=c.charCodeAt(b+1))&64512)===56320?(I=65536+((I&1023)<<10)+(A&1023),b++,a[E++]=I>>18|240,a[E++]=I>>12&63|128,a[E++]=I>>6&63|128,a[E++]=I&63|128):(a[E++]=I>>12|224,a[E++]=I>>6&63|128,a[E++]=I&63|128);d=E-i-u}else d=g(c,i+u,w);d<24?a[i++]=96|d:d<256?(u<2&&a.copyWithin(i+2,i+1,i+1+d),a[i++]=120,a[i++]=d):d<65536?(u<3&&a.copyWithin(i+3,i+2,i+2+d),a[i++]=121,a[i++]=d>>8,a[i++]=d&255):(u<5&&a.copyWithin(i+5,i+3,i+3+d),a[i++]=122,P.setUint32(i,d),i+=4),i+=d}else if(y==="number")if(!this.alwaysUseFloat&&c>>>0===c)c<24?a[i++]=c:c<256?(a[i++]=24,a[i++]=c):c<65536?(a[i++]=25,a[i++]=c>>8,a[i++]=c&255):(a[i++]=26,P.setUint32(i,c),i+=4);else if(!this.alwaysUseFloat&&c>>0===c)c>=-24?a[i++]=31-c:c>=-256?(a[i++]=56,a[i++]=~c):c>=-65536?(a[i++]=57,P.setUint16(i,~c),i+=2):(a[i++]=58,P.setUint32(i,~c),i+=4);else{let h;if((h=this.useFloat32)>0&&c<4294967296&&c>=-2147483648){a[i++]=250,P.setFloat32(i,c);let u;if(h<4||(u=c*Me[(a[i]&127)<<1|a[i+1]>>7])>>0===u){i+=4;return}else i--}a[i++]=251,P.setFloat64(i,c),i+=8}else if(y==="object")if(!c)a[i++]=246;else{if(m){let u=m.get(c);if(u){if(a[i++]=216,a[i++]=29,a[i++]=25,!u.references){let w=m.idsToInsert||(m.idsToInsert=[]);u.references=[],w.push(u)}u.references.push(i-n),i+=2;return}else m.set(c,{offset:i-n})}let h=c.constructor;if(h===Object)tt(c,!0);else if(h===Array){d=c.length,d<24?a[i++]=128|d:$(d);for(let u=0;u>8,a[i++]=d&255):(a[i++]=186,P.setUint32(i,d),i+=4),x.keyMap)for(let[u,w]of c)C(x.encodeKey(u)),C(w);else for(let[u,w]of c)C(u),C(w);else{for(let u=0,w=Fe.length;u>8,a[i++]=A&255):A>-1&&(a[i++]=218,P.setUint32(i,A),i+=4),I.encode.call(this,c,C,me);return}}if(c[Symbol.iterator]){if(mt){let u=new Error("Iterable should be serialized as iterator");throw u.iteratorNotHandled=!0,u}a[i++]=159;for(let u of c)C(u);a[i++]=255;return}if(c[Symbol.asyncIterator]||xt(c)){let u=new Error("Iterable/blob should be serialized as iterator");throw u.iteratorNotHandled=!0,u}tt(c,!c.hasOwnProperty)}}else if(y==="boolean")a[i++]=c?245:244;else if(y==="bigint"){if(c=0)a[i++]=27,P.setBigUint64(i,c);else if(c>-(BigInt(1)<{let y=Object.keys(c),d=Object.values(c),h=y.length;h<24?a[i++]=160|h:h<256?(a[i++]=184,a[i++]=h):h<65536?(a[i++]=185,a[i++]=h>>8,a[i++]=h&255):(a[i++]=186,P.setUint32(i,h),i+=4);let u;if(x.keyMap)for(let w=0;w{a[i++]=185;let d=i-n;i+=2;let h=0;if(x.keyMap)for(let u in c)(y||c.hasOwnProperty(u))&&(C(x.encodeKey(u)),C(c[u]),h++);else for(let u in c)(y||c.hasOwnProperty(u))&&(C(u),C(c[u]),h++);a[d+++n]=h>>8,a[d+n]=h&255}:(c,y)=>{let d,h=l.transitions||(l.transitions=Object.create(null)),u=0,w=0,b,I;if(this.keyMap){I=Object.keys(c).map(E=>this.encodeKey(E)),w=I.length;for(let E=0;E>8|224,a[i++]=A&255;else if(I||(I=h.__keys__||(h.__keys__=Object.keys(c))),b===void 0?(A=l.nextId++,A||(A=0,l.nextId=1),A>=er&&(l.nextId=(A=S)+1)):A=b,l[A]=I,A>8|224,a[i++]=A&255,h=l.transitions;for(let E=0;E=er-S&&(G.shift()[L]=void 0),G.push(h),$(w+2),C(57344+A),C(I),y===null)return;for(let E in c)(y||c.hasOwnProperty(E))&&C(c[E]);return}if(w<24?a[i++]=128|w:$(w),y!==null)for(let E in c)(y||c.hasOwnProperty(E))&&C(c[E])},me=c=>{let y;if(c>16777216){if(c-n>tr)throw new Error("Encoded buffer would be larger than maximum buffer size");y=Math.min(tr,Math.round(Math.max((c-n)*(c>67108864?1.25:2),4194304)/4096)*4096)}else y=(Math.max(c-n<<2,a.length-1)>>12)+1<<12;let d=new pt(y);return P=new DataView(d.buffer,0,y),a.copy?a.copy(d,0,n,c):d.set(a.slice(n,c)),i-=n,n=0,Y=d.length-10,a=d},Z=100,Vt=1e3;this.encodeAsIterable=function(c,y){return Tt(c,y,oe)},this.encodeAsAsyncIterable=function(c,y){return Tt(c,y,Lt)};function*oe(c,y,d){let h=c.constructor;if(h===Object){let u=x.useRecords!==!1;u?tt(c,null):rr(Object.keys(c).length,160);for(let w in c){let b=c[w];u||C(w),b&&typeof b=="object"?y[w]?yield*oe(b,y[w]):yield*rt(b,y,w):C(b)}}else if(h===Array){let u=c.length;$(u);for(let w=0;wZ)?y.element?yield*oe(b,y.element):yield*rt(b,y,"element"):C(b)}}else if(c[Symbol.iterator]){a[i++]=159;for(let u of c)u&&(typeof u=="object"||i-n>Z)?y.element?yield*oe(u,y.element):yield*rt(u,y,"element"):C(u);a[i++]=255}else xt(c)?(rr(c.size,64),yield a.subarray(n,i),yield c,xe()):c[Symbol.asyncIterator]?(a[i++]=159,yield a.subarray(n,i),yield c,xe(),a[i++]=255):C(c);d&&i>n?yield a.subarray(n,i):i-n>Z&&(yield a.subarray(n,i),xe())}function*rt(c,y,d){let h=i-n;try{C(c),i-n>Z&&(yield a.subarray(n,i),xe())}catch(u){if(u.iteratorNotHandled)y[d]={},i=n+h,yield*oe.call(this,c,y[d]);else throw u}}function xe(){Z=Vt,x.encode(null,wt)}function Tt(c,y,d){return y&&y.chunkThreshold?Z=Vt=y.chunkThreshold:Z=100,c&&typeof c=="object"?(x.encode(null,wt),d(c,x.iterateProperties||(x.iterateProperties={}),!0)):[x.encode(c)]}async function*Lt(c,y){for(let d of oe(c,y,!0)){let h=d.constructor;if(h===Qt||h===Uint8Array)yield d;else if(xt(d)){let u=d.stream().getReader(),w;for(;!(w=await u.read()).done;)yield w.value}else if(d[Symbol.asyncIterator])for await(let u of d)xe(),u?yield*Lt(u,y.async||(y.async={})):yield x.encode(u);else yield d}}}useBuffer(e){a=e,P=new DataView(a.buffer,a.byteOffset,a.byteLength),i=0}clearSharedData(){this.structures&&(this.structures=[]),this.sharedValues&&(this.sharedValues=void 0)}updateSharedData(){let e=this.sharedVersion||0;this.sharedVersion=e+1;let r=this.structures.slice(0),n=new We(r,this.sharedValues,this.sharedVersion),s=this.saveShared(n,o=>(o&&o.version||0)==e);return s===!1?(n=this.getShared()||{},this.structures=n.structures||[],this.sharedValues=n.packedValues,this.sharedVersion=n.version,this.structures.nextId=this.structures.length):r.forEach((o,l)=>this.structures[l]=o),s}};function rr(t,e){t<24?a[i++]=e|t:t<256?(a[i++]=e|24,a[i++]=t):t<65536?(a[i++]=e|25,a[i++]=t>>8,a[i++]=t&255):(a[i++]=e|26,P.setUint32(i,t),i+=4)}var We=class{constructor(e,r,n){this.structures=e,this.packedValues=r,this.version=n}};function $(t){t<24?a[i++]=128|t:t<256?(a[i++]=152,a[i++]=t):t<65536?(a[i++]=153,a[i++]=t>>8,a[i++]=t&255):(a[i++]=154,P.setUint32(i,t),i+=4)}var Mr=typeof Blob>"u"?function(){}:Blob;function xt(t){if(t instanceof Mr)return!0;let e=t[Symbol.toStringTag];return e==="Blob"||e==="File"}function Be(t,e){switch(typeof t){case"string":if(t.length>3){if(e.objectMap[t]>-1||e.values.length>=e.maxValues)return;let n=e.get(t);if(n)++n.count==2&&e.values.push(t);else if(e.set(t,{count:1}),e.samplingPackedValues){let s=e.samplingPackedValues.get(t);s?s.count++:e.samplingPackedValues.set(t,{count:1})}}break;case"object":if(t)if(t instanceof Array)for(let n=0,s=t.length;n"u"?function(){}:BigUint64Array,Int8Array,Int16Array,Int32Array,typeof BigInt64Array>"u"?function(){}:BigInt64Array,Float32Array,Float64Array,We];Fe=[{tag:1,encode(t,e){let r=t.getTime()/1e3;(this.useTimestamp32||t.getMilliseconds()===0)&&r>=0&&r<4294967296?(a[i++]=26,P.setUint32(i,r),i+=4):(a[i++]=251,P.setFloat64(i,r),i+=8)}},{tag:258,encode(t,e){let r=Array.from(t);e(r)}},{tag:27,encode(t,e){e([t.name,t.message])}},{tag:27,encode(t,e){e(["RegExp",t.source,t.flags])}},{getTag(t){return t.tag},encode(t,e){e(t.value)}},{encode(t,e,r){nr(t,r)}},{getTag(t){if(t.constructor===Uint8Array&&(this.tagUint8Array||Ae&&this.tagUint8Array!==!1))return 64},encode(t,e,r){nr(t,r)}},K(68,1),K(69,2),K(70,4),K(71,8),K(72,1),K(77,2),K(78,4),K(79,8),K(85,4),K(86,8),{encode(t,e){let r=t.packedValues||[],n=t.structures||[];if(r.values.length>0){a[i++]=216,a[i++]=51,$(4);let s=r.values;e(s),$(0),$(0),packedObjectMap=Object.create(sharedPackedObjectMap||null);for(let o=0,l=s.length;o1&&(t-=4),{tag:t,encode:function(n,s){let o=n.byteLength,l=n.byteOffset||0,m=n.buffer||n;s(Ae?Ve.from(m,l,o):new Uint8Array(m,l,o))}}}function nr(t,e){let r=t.byteLength;r<24?a[i++]=64+r:r<256?(a[i++]=88,a[i++]=r):r<65536?(a[i++]=89,a[i++]=r>>8,a[i++]=r&255):(a[i++]=90,P.setUint32(i,r),i+=4),i+r>=a.length&&e(i+r),a.set(t.buffer?t:new Uint8Array(t),i),i+=r}function _r(t,e){let r,n=e.length*2,s=t.length-n;e.sort((o,l)=>o.offset>l.offset?1:-1);for(let o=0;o>8,t[m]=o&255}for(;r=e.pop();){let o=r.offset;t.copyWithin(o+n,o,s),n-=2;let l=o+n;t[l++]=216,t[l++]=28,s=o}return t}function sr(t,e){P.setUint32(_.position+t,i-_.position-t+1);let r=_;_=null,e(r[0]),e(r[1])}function It(t){if(t.Class){if(!t.encode)throw new Error("Extension has no encode function");bt.unshift(t.Class),Fe.unshift(t)}Zt(t)}var At=new Ie({useRecords:!1}),Dt=At.encode,Br=At.encodeAsIterable,Fr=At.encodeAsAsyncIterable,{NEVER:Wr,ALWAYS:Vr,DECIMAL_ROUND:Tr,DECIMAL_FIT:Lr}=Re,gt=512,vr=1024,wt=2048;var ir=class{debug;constructor(e=!1,r){this.debug=e,r&&r.forEach(It)}encoder(e){return new Ut(e,this.debug)}decoder(e){return new Et(e,this.debug)}},Ut=class{w;debug;constructor(e,r=!1){this.w=e,this.debug=r}async encode(e){this.debug&&console.log("<<",e);let r=Dt(e),n=0;for(;n>",s),Promise.resolve(s)}};function Te(t,e,r=0){r=Math.max(0,Math.min(r,e.byteLength));let n=e.byteLength-r;return t.byteLength>n&&(t=t.subarray(0,n)),e.set(t,r),t.byteLength}var Le=32*1024,St=2**32-2,ve=class{_buf;_off;constructor(e){this._buf=e===void 0?new Uint8Array(0):new Uint8Array(e),this._off=0}bytes(e={copy:!0}){return e.copy===!1?this._buf.subarray(this._off):this._buf.slice(this._off)}empty(){return this._buf.byteLength<=this._off}get length(){return this._buf.byteLength-this._off}get capacity(){return this._buf.buffer.byteLength}truncate(e){if(e===0){this.reset();return}if(e<0||e>this.length)throw Error("bytes.Buffer: truncation out of range");this._reslice(this._off+e)}reset(){this._reslice(0),this._off=0}_tryGrowByReslice(e){let r=this._buf.byteLength;return e<=this.capacity-r?(this._reslice(r+e),r):-1}_reslice(e){this._buf=new Uint8Array(this._buf.buffer,0,e)}readSync(e){if(this.empty())return this.reset(),e.byteLength===0?0:null;let r=Te(this._buf.subarray(this._off),e);return this._off+=r,r}read(e){let r=this.readSync(e);return Promise.resolve(r)}writeSync(e){let r=this._grow(e.byteLength);return Te(e,this._buf,r)}write(e){let r=this.writeSync(e);return Promise.resolve(r)}_grow(e){let r=this.length;r===0&&this._off!==0&&this.reset();let n=this._tryGrowByReslice(e);if(n>=0)return n;let s=this.capacity;if(e<=Math.floor(s/2)-r)Te(this._buf.subarray(this._off),this._buf);else{if(s+e>St)throw new Error("The buffer cannot be grown beyond the maximum size.");{let o=new Uint8Array(Math.min(2*s+e,St));Te(this._buf.subarray(this._off),o),this._buf=o}}return this._off=0,this._reslice(Math.min(r+e,St)),r}grow(e){if(e<0)throw Error("Buffer.grow: negative count");let r=this._grow(e);this._reslice(r)}async readFrom(e){let r=0,n=new Uint8Array(Le);for(;;){let s=this.capacity-this.length{},{path:r,callable:n}),{get(s,o,l){return o.startsWith("__")?Reflect.get(s,o,l):e(s.path?`${s.path}.${o}`:o,s.callable)},apply(s,o,l=[]){return s.callable(s.path,l)}})}return e("",t.call.bind(t))}function Ue(t){return{respondRPC:t}}function zr(){return Ue((t,e)=>{t.return(new Error(`not found: ${e.selector}`))})}function kt(t){return t===""?"/":(t[0]!="/"&&(t="/"+t),t=t.replace(".","/"),t.toLowerCase())}var J=class{handlers;constructor(){this.handlers={}}async respondRPC(e,r){await this.handler(r).respondRPC(e,r)}handler(e){let r=this.match(e.selector);return r||zr()}remove(e){e=kt(e);let r=this.match(e);return delete this.handlers[e],r||null}match(e){if(e=kt(e),this.handlers.hasOwnProperty(e))return this.handlers[e];let r=Object.keys(this.handlers).filter(n=>n.endsWith("/"));r.sort((n,s)=>s.length-n.length);for(let n of r)if(e.startsWith(n)){let s=this.handlers[n],o=s;return o.match&&o.match instanceof Function?o.match(e.slice(n.length)):s}return null}handle(e,r){if(e==="")throw"handle: invalid selector";let n=kt(e),s=r;if(s.match&&s.match instanceof Function&&!n.endsWith("/")&&(n=n+"/"),!r)throw"handle: invalid handler";if(this.match(n))throw"handle: selector already registered";this.handlers[n]=r}};async function cr(t,e,r){let n=new fe(e),s=n.decoder(t),o=await s.decode(),l=new Ne(o.S,t,s);l.caller=new de(t.session,e);let m=new He,g=new Ot(t,n,m);return r||(r=new J),await r.respondRPC(g,l),g.responded||await g.return(null),Promise.resolve()}var Ot=class{header;ch;codec;responded;constructor(e,r,n){this.ch=e,this.codec=r,this.header=n,this.responded=!1}send(e){return this.codec.encoder(this.ch).encode(e)}return(e){return this.respond(e,!1)}async continue(e){return await this.respond(e,!0),this.ch}async respond(e,r){return this.responded=!0,this.header.C=r,e instanceof Error&&(this.header.E=e.message,e=null),await this.send(this.header),await this.send(e),r||await this.ch.close(),Promise.resolve()}};var Ne=class{selector;channel;caller;decoder;constructor(e,r,n){this.selector=e,this.channel=r,this.decoder=n}receive(){return this.decoder.decode()}},He=class{E;C;constructor(){this.E=void 0,this.C=!1}},ze=class{error;continue;value;channel;codec;constructor(e,r){this.channel=e,this.codec=r,this.error=void 0,this.continue=!1}send(e){return this.codec.encoder(this.channel).encode(e)}receive(){return this.codec.decoder(this.channel).decode()}};var je=class{session;caller;codec;responder;constructor(e,r){this.session=e,this.codec=r,this.caller=new de(e,r),this.responder=new J}close(){return this.session.close()}async respond(){for(;;){let e=await this.session.accept();if(e===null)break;cr(e,this.codec,this.responder)}}async call(e,r){return this.caller.call(e,r)}handle(e,r){this.responder.handle(e,r)}respondRPC(e,r){this.responder.respondRPC(e,r)}virtualize(){return ar(this.caller)}};var lr=new Map([[100,12],[101,16],[102,4],[103,8],[104,8],[105,4],[106,4]]);var Ke=class{w;constructor(e){this.w=e}async encode(e){ue.messages&&console.log("<{r.set(s,n),n+=s.length}),r}var pe=class{q;waiters;closed;constructor(){this.q=[],this.waiters=[],this.closed=!1}push(e){if(this.closed)throw"closed queue";if(this.waiters.length>0){let r=this.waiters.shift();r&&r(e);return}this.q.push(e)}shift(){return this.closed?Promise.resolve(null):new Promise(e=>{if(this.q.length>0){e(this.q.shift()||null);return}this.waiters.push(e)})}close(){this.closed||(this.closed=!0,this.waiters.forEach(e=>{e(null)}))}},qe=class{gotEOF;readBuf;readers;constructor(){this.readBuf=new Uint8Array(0),this.gotEOF=!1,this.readers=[]}read(e){return new Promise(r=>{let n=()=>{if(this.readBuf===void 0){r(null);return}if(this.readBuf.length==0){if(this.gotEOF){this.readBuf=void 0,r(null);return}this.readers.push(n);return}let s=this.readBuf.slice(0,e.length);this.readBuf=this.readBuf.slice(s.length),this.readBuf.length==0&&this.gotEOF&&(this.readBuf=void 0),e.set(s),r(s.length)};n()})}write(e){for(this.readBuf&&(this.readBuf=Ge([this.readBuf,e],this.readBuf.length+e.length));!this.readBuf||this.readBuf.length>0;){let r=this.readers.shift();if(!r)break;r()}return Promise.resolve(e.length)}eof(){this.gotEOF=!0,this.flushReaders()}close(){this.readBuf=void 0,this.flushReaders()}flushReaders(){for(;;){let e=this.readers.shift();if(!e)return;e()}}};var Ye=class{r;constructor(e){this.r=e}async decode(){let e=await jr(this.r);if(e===null)return Promise.resolve(null);ue.bytes&&console.log(">>DEC",e);let r=Kr(e);return ue.messages&&console.log(">>DEC",r),r}};async function jr(t){let e=new Uint8Array(1);if(await t.read(e)===null)return Promise.resolve(null);let n=e[0],s=lr.get(n);if(s===void 0||n<100||n>106)return Promise.reject(`bad packet: ${n}`);let o=new Uint8Array(s);if(await t.read(o)===null)return Promise.reject("unexpected EOF reading packet");if(n===104){let g=new DataView(o.buffer).getUint32(4),x=0,k=[];for(;x_t){await this.enc.encode({ID:102,channelID:e.senderID});return}let r=this.newChannel();r.remoteId=e.senderID,r.maxRemotePayload=e.maxPacketSize,r.remoteWin=e.windowSize,r.maxIncomingPayload=Xe,this.incoming.push(r),await this.enc.encode({ID:101,channelID:r.remoteId,senderID:r.localId,windowSize:r.myWindow,maxPacketSize:r.maxIncomingPayload})}newChannel(){let e=new Ze(this);return e.remoteWin=0,e.myWindow=fr,e.localId=this.addCh(e),e}getCh(e){let r=this.channels[e];return r&&r.localId!==e&&console.log("bad ids:",e,r.localId,r.remoteId),r}addCh(e){return this.channels.forEach((r,n)=>{if(r===void 0)return this.channels[n]=e,n}),this.channels.push(e),this.channels.length-1}rmCh(e){delete this.channels[e]}};var Xe=1<<24,fr=64*Xe,Ze=class{localId;remoteId;maxIncomingPayload;maxRemotePayload;session;ready;sentEOF;sentClose;remoteWin;myWindow;readBuf;writers;constructor(e){this.localId=0,this.remoteId=0,this.maxIncomingPayload=0,this.maxRemotePayload=0,this.sentEOF=!1,this.sentClose=!1,this.remoteWin=0,this.myWindow=0,this.ready=new pe,this.session=e,this.writers=[],this.readBuf=new qe}ident(){return this.localId}async read(e){let r=await this.readBuf.read(e);if(r!==null)try{await this.adjustWindow(r)}catch(n){if(n!=="EOF"&&n.name!=="BadResource")throw n}return r}write(e){return this.sentEOF?Promise.reject("EOF"):new Promise((r,n)=>{let s=0,o=()=>{if(this.sentEOF||this.sentClose){n("EOF");return}if(e.byteLength==0){r(s);return}let l=Math.min(this.maxRemotePayload,e.length),m=this.reserveWindow(l);if(m==0){this.writers.push(o);return}let g=e.slice(0,m);this.send({ID:104,channelID:this.remoteId,length:g.length,data:g}).then(()=>{if(s+=g.length,e=e.slice(g.length),e.length==0){r(s);return}this.writers.push(o)})};o()})}reserveWindow(e){return this.remoteWin0;){let r=this.writers.shift();if(!r)break;r()}}async closeWrite(){this.sentEOF=!0,await this.send({ID:105,channelID:this.remoteId}),this.writers.forEach(e=>e()),this.writers=[]}async close(){if(this.readBuf.eof(),!this.sentClose){for(await this.send({ID:106,channelID:this.remoteId}),this.sentClose=!0;await this.ready.shift()!==null;);return}this.shutdown()}shutdown(){this.readBuf.close(),this.writers.forEach(e=>e()),this.ready.close(),this.session.rmCh(this.localId)}async adjustWindow(e){this.myWindow+=e,await this.send({ID:103,channelID:this.remoteId,additionalBytes:e})}send(e){if(this.sentClose)throw"EOF";return this.sentClose=e.ID===106,this.session.enc.encode(e)}handle(e){if(e.ID===104){this.handleData(e);return}if(e.ID===106){this.close();return}if(e.ID===105&&this.readBuf.eof(),e.ID===102){this.session.rmCh(e.channelID),this.ready.push(!1);return}if(e.ID===101){if(e.maxPacketSize_t)throw"invalid max packet size";this.remoteId=e.senderID,this.maxRemotePayload=e.maxPacketSize,this.addWindow(e.windowSize),this.ready.push(!0);return}e.ID===103&&this.addWindow(e.additionalBytes)}handleData(e){if(e.length>this.maxIncomingPayload)throw"incoming packet exceeds maximum payload size";if(this.myWindowQe,connect:()=>qr});function qr(t,e){return new Promise(r=>{let n=new WebSocket(t);n.onopen=()=>r(new Qe(n)),e&&(n.onclose=e)})}var Qe=class{ws;waiters;chunks;isClosed;constructor(e){this.isClosed=!1,this.waiters=[],this.chunks=[],this.ws=e,this.ws.binaryType="arraybuffer",this.ws.onmessage=n=>{let s=new Uint8Array(n.data);if(this.chunks.push(s),this.waiters.length>0){let o=this.waiters.shift();o&&o()}};let r=this.ws.onclose;this.ws.onclose=n=>{r&&r.bind(this.ws)(n),this.close()}}read(e){return new Promise(r=>{var n=()=>{if(this.isClosed){r(null);return}if(this.chunks.length===0){this.waiters.push(n);return}let s=0;for(;sl.length){let m=o.slice(l.length);this.chunks.unshift(m)}}r(s)};n()})}write(e){return this.ws.send(e),Promise.resolve(e.byteLength)}close(){this.isClosed||(this.isClosed=!0,this.waiters.forEach(e=>e()),this.ws.close())}};var Gr={transport:Bt};async function jn(t,e){let r=await Gr.transport.connect(t);return Yr(r,e)}function Yr(t,e,r){let n=new Je(t),s=new je(n,e);if(r){for(let o in r)s.handle(o,Ue(r[o]));s.respond()}return s}function qn(t){return t instanceof Function?ur(t):Jr(t)}function Jr(t){let e=new J;for(let r of Xr(t))if(!(["constructor","respondRPC"].includes(r)||r.startsWith("_")))if(t[r]instanceof Function){let n=ur(t[r],t);t[`_${r}RPC`]instanceof Function&&(n={respondRPC:t[`_${r}RPC`].bind(t)}),e.handle(r,n)}else t[r]&&t[r].respondRPC&&e.handle(r,t[r]);return t.respondRPC&&e.handle("/",t),e}function ur(t,e){return Ue(async(r,n)=>{try{let s=t.apply(e,await n.receive());if(s instanceof Promise){let o=await s;r.return(o)}else r.return(s)}catch(s){if(typeof s=="string"){r.return(new Error(s));return}r.return(s)}})}function Xr(t){let e=new Set;Object.getOwnPropertyNames(t).forEach(r=>e.add(r));for(let r=t;r!=null;r=Object.getPrototypeOf(r))r.constructor.name!=="Object"&&Object.getOwnPropertyNames(r).forEach(n=>e.add(n));return[...e]}function Yn(t){function e(r,n){return new Proxy(Object.assign(()=>{},{path:r,callable:n}),{get(s,o,l){return o.startsWith("__")?Reflect.get(s,o,l):e(s.path?`${s.path}.${o}`:o,s.callable)},apply(s,o,l=[]){return s.callable(s.path,l)}})}return e("",t.call.bind(t))}var es=null;async function ts(t,e){let r=new Uint8Array(32768),n=0;for(;;){let s=await e.read(r);if(s===null)break;n+=await t.write(r.subarray(0,s))}return n}var Ft=class{port;waiters;chunks;isClosed;constructor(e){this.isClosed=!1,this.waiters=[],this.chunks=[],this.port=e,this.port.onmessage=r=>{let n=new Uint8Array(r.data);if(this.chunks.push(n),this.waiters.length>0){let s=this.waiters.shift();s&&s()}}}read(e){return new Promise(r=>{var n=()=>{if(this.isClosed){r(null);return}if(this.chunks.length===0){this.waiters.push(n);return}let s=0;for(;sl.length){let m=o.slice(l.length);this.chunks.unshift(m)}}r(s)};n()})}write(e){return this.port.postMessage(e,[e.buffer]),Promise.resolve(e.byteLength)}close(){this.isClosed||(this.isClosed=!0,this.waiters.forEach(e=>e()),this.port.close())}};var Wt=class{worker;waiters;chunks;isClosed;constructor(e){this.isClosed=!1,this.waiters=[],this.chunks=[],this.worker=e,this.worker.onmessage=r=>{if(!r.data.duplex)return;let n=new Uint8Array(r.data.duplex);if(this.chunks.push(n),this.waiters.length>0){let s=this.waiters.shift();s&&s()}}}read(e){return new Promise(r=>{var n=()=>{if(this.isClosed){r(null);return}if(this.chunks.length===0){this.waiters.push(n);return}let s=0;for(;sl.length){let m=o.slice(l.length);this.chunks.unshift(m)}}r(s)};n()})}write(e){return this.worker.postMessage({duplex:e.buffer}),Promise.resolve(e.byteLength)}close(){this.isClosed||(this.isClosed=!0,this.waiters.forEach(e=>e()))}};export{ve as Buffer,ir as CBORCodec,Et as CBORDecoder,Ut as CBOREncoder,Ne as Call,Ze as Channel,de as Client,es as EOF,fe as FrameCodec,Pt as FrameDecoder,Ct as FrameEncoder,Ue as HandlerFunc,zt as JSONCodec,st as JSONDecoder,nt as JSONEncoder,zr as NotFoundHandler,je as Peer,Ft as PortConn,cr as Respond,J as RespondMux,ze as Response,He as ResponseHeader,Je as Session,ar as VirtualCaller,Wt as WorkerConn,Xe as channelMaxPacket,fr as channelWindowSize,jn as connect,ts as copy,qn as handlerFrom,_t as maxPacketLength,Yn as methodProxy,Rt as minPacketLength,Yr as open,Gr as options}; 2 | -------------------------------------------------------------------------------- /assets/env86.js: -------------------------------------------------------------------------------- 1 | 2 | import * as duplex from "./duplex.min.js"; 3 | 4 | export { duplex }; 5 | 6 | export async function boot(imageURL, options) { 7 | const resp = await fetch(`${imageURL}/image.json`); 8 | const imageConfig = await resp.json(); 9 | 10 | let config = Object.assign(imageConfig, options); 11 | const initStateChunks = imageConfig["initial_state_parts"]; 12 | const downloader = async () => await downloadChunks(generateRange(initStateChunks).map(suffix => `${imageURL}/state/initial.state.${suffix}`)); 13 | if (initStateChunks && !config["initial_state"]) { 14 | config["initial_state"] = { 15 | buffer: await downloader() 16 | }; 17 | } 18 | if (typeof config["initial_state"] === 'function') { 19 | config["initial_state"] = { 20 | buffer: await config["initial_state"](downloader) 21 | }; 22 | } 23 | 24 | if (!config["wasm_path"]) { 25 | const url = new URL(import.meta.url); 26 | const path = url.pathname.split("/"); 27 | path.pop(); 28 | path.push("v86.wasm"); 29 | url.pathname = path.join("/"); 30 | config["wasm_path"] = url.toString(); 31 | } 32 | config["autostart"] = true; 33 | if (!config["memory_size"]) { 34 | config["memory_size"] = 512 * 1024 * 1024; // 512MB 35 | } 36 | if (!config["vga_memory_size"]) { 37 | config["vga_memory_size"] = 8 * 1024 * 1024; // 8MB 38 | } 39 | if (!config["filesystem"]) { 40 | config["filesystem"] = { 41 | baseurl: `${imageURL}/fs/`, 42 | basefs: `${imageURL}/fs.json`, 43 | }; 44 | } 45 | 46 | ["bios", "vga_bios", "initrd", "bzimage"].forEach(key => { 47 | if (config[key] && config[key]["url"].startsWith("./")) { 48 | config[key]["url"] = imageURL+config[key]["url"].slice(1) 49 | } 50 | }); 51 | 52 | let peer = undefined; 53 | if (config["control_url"]) { 54 | peer = await duplex.connect(config["control_url"], new duplex.CBORCodec()); 55 | const resp = await peer.call("config"); 56 | config = Object.assign(config, resp.value); 57 | } 58 | 59 | // always, why not. 60 | // for possible guest service 61 | config.uart1 = true; 62 | 63 | const vm = new window.V86(config); 64 | 65 | if (peer) { 66 | let tty = undefined; 67 | const enc = new TextEncoder(); 68 | vm.add_listener("emulator-loaded", async () => { 69 | 70 | if (config.EnableTTY && tty === undefined) { 71 | const tty = await peer.call("tty"); 72 | vm.add_listener("serial0-output-byte", (b) => { 73 | tty.channel.write(enc.encode(String.fromCharCode(b))); 74 | }); 75 | (async () => { 76 | const buf = new Uint8Array(1024); 77 | while (true) { 78 | const n = await tty.channel.read(buf); 79 | if (n === null) { 80 | break; 81 | } 82 | const data = new Uint8Array(buf.slice(0, n)); 83 | vm.serial_send_bytes(0, data); 84 | } 85 | })(); 86 | } 87 | 88 | if (config.has_guest_service) { 89 | const guest = new WebSocket(config["control_url"].replace("ctl", "guest")); 90 | guest.binaryType = "arraybuffer"; 91 | const messageSizes = new Map([ 92 | [100, 13], // open 93 | [101, 17], // open-confirm 94 | [102, 5], // open-failure 95 | [103, 9], // window-adjust 96 | [104, 9], // data 97 | [105, 5], // eof 98 | [106, 5], // close 99 | ]); 100 | let buf = []; 101 | vm.add_listener("serial1-output-byte", (byte) => { 102 | if (buf.length === 0) { 103 | buf.push(byte); 104 | return; 105 | } 106 | buf.push(byte); 107 | let expectedSize = undefined; 108 | // check if data message 109 | if (buf[0] === 104 && buf.length >= 9) { 110 | const view = new DataView((new Uint8Array(buf)).buffer); 111 | expectedSize = 9+view.getUint32(5); 112 | } else { 113 | expectedSize = messageSizes.get(buf[0]); 114 | } 115 | if (expectedSize === undefined) { 116 | console.warn("unexpected buffer:", buf); 117 | buf = []; 118 | return; 119 | } 120 | if (buf.length < expectedSize) { 121 | return; 122 | } 123 | const buf2 = new Uint8Array(buf); 124 | guest.send(buf2); 125 | buf = []; 126 | }); 127 | guest.onmessage = (event) => { 128 | const data = new Uint8Array(event.data) 129 | vm.serial_send_bytes(1, data); 130 | } 131 | } 132 | 133 | peer.call("loaded", []); 134 | }); 135 | 136 | peer.handle("pause", duplex.handlerFrom(() => vm.stop())); 137 | peer.handle("unpause", duplex.handlerFrom(() => vm.run())); 138 | peer.handle("save", duplex.handlerFrom(async () => { 139 | const buf = await vm.save_state(); 140 | return new Uint8Array(buf); 141 | })); 142 | peer.handle("restore", duplex.handlerFrom((data) => { 143 | vm.restore_state(data.buffer); 144 | })); 145 | peer.handle("sendKeyboard", duplex.handlerFrom((text) => { 146 | vm.keyboard_send_text(text); 147 | })); 148 | peer.handle("setScale", duplex.handlerFrom((x, y) => { 149 | vm.screen_set_scale(x, y); 150 | })); 151 | peer.handle("setFullscreen", duplex.handlerFrom(() => { 152 | vm.screen_go_fullscreen(); 153 | })); 154 | peer.handle("mac", duplex.handlerFrom(() => { 155 | return vm.v86.cpu.devices.net.mac.map(el => el.toString(16).padStart(2, "0")).join(":"); 156 | })); 157 | peer.handle("screenshot", duplex.handlerFrom(() => { 158 | const image = vm.screen_make_screenshot(); 159 | if (image === null) { 160 | return null; 161 | } 162 | let binary = atob(image.src.split(',')[1]); 163 | var array = []; 164 | for (var i = 0; i < binary.length; i++) { 165 | array.push(binary.charCodeAt(i)); 166 | } 167 | return new Uint8Array(array); 168 | })); 169 | 170 | peer.respond(); 171 | } 172 | 173 | vm.config = config; 174 | return vm; 175 | } 176 | 177 | 178 | async function downloadChunks(urls) { 179 | const responses = await Promise.all(urls.map(url => fetch(url).then(response => { 180 | if (response.status === 404) { 181 | return null; 182 | } 183 | return response; 184 | }))); 185 | const validResponses = responses.filter(response => response !== null); 186 | const arrayBuffers = await Promise.all(validResponses.map(response => response.arrayBuffer())); 187 | 188 | const totalLength = arrayBuffers.reduce((sum, buffer) => sum + buffer.byteLength, 0); 189 | const concatenatedBuffer = new Uint8Array(totalLength); 190 | let offset = 0; 191 | for (const buffer of arrayBuffers) { 192 | concatenatedBuffer.set(new Uint8Array(buffer), offset); 193 | offset += buffer.byteLength; 194 | } 195 | return concatenatedBuffer.buffer; 196 | } 197 | 198 | function generateRange(x) { 199 | if (typeof x !== 'number' || x < 0) { 200 | throw new Error('Input must be a non-negative number'); 201 | } 202 | 203 | const result = []; 204 | for (let i = 0; i < x; i++) { 205 | result.push(i.toString()); 206 | } 207 | 208 | return result; 209 | } -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 |
23 |
24 | 25 |
26 | 35 | 36 | -------------------------------------------------------------------------------- /cmd/env86/assets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | 8 | "github.com/progrium/env86/assets" 9 | 10 | "tractor.dev/toolkit-go/engine/cli" 11 | ) 12 | 13 | func assetsCmd() *cli.Command { 14 | cmd := &cli.Command{ 15 | Hidden: true, 16 | Usage: "assets ", 17 | Short: "", 18 | Args: cli.MinArgs(1), 19 | Run: func(ctx *cli.Context, args []string) { 20 | if args[0] == "env86.min.js" { 21 | b, err := assets.BundleJavaScript() 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | os.Stdout.Write(b) 26 | return 27 | } 28 | f, err := assets.Dir.Open(args[0]) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | defer f.Close() 33 | io.Copy(os.Stdout, f) 34 | }, 35 | } 36 | return cmd 37 | } 38 | -------------------------------------------------------------------------------- /cmd/env86/boot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/progrium/env86" 13 | 14 | "github.com/progrium/go-netstack/vnet" 15 | "tractor.dev/toolkit-go/engine/cli" 16 | ) 17 | 18 | func bootCmd() *cli.Command { 19 | var ( 20 | noKeyboard bool 21 | noMouse bool 22 | enableTTY bool 23 | noConsole bool 24 | exitOn string 25 | enableNet bool 26 | coldBoot bool 27 | saveOnExit bool 28 | portForward string 29 | consoleURL bool 30 | useCDP bool 31 | ) 32 | cmd := &cli.Command{ 33 | Usage: "boot ", 34 | Short: "boot and run a VM", 35 | Args: cli.MinArgs(1), 36 | Run: func(ctx *cli.Context, args []string) { 37 | imagePath := args[0] 38 | var err error 39 | if !strings.HasPrefix(imagePath, "./") && !strings.HasPrefix(imagePath, ".\\") { 40 | exists, fullPath := globalImage(imagePath) 41 | if !exists { 42 | log.Fatal("global image not found") 43 | } 44 | imagePath = fullPath 45 | } else { 46 | imagePath, err = filepath.Abs(imagePath) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | } 51 | 52 | image, err := env86.LoadImage(imagePath) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | 57 | cfg, err := image.Config() 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | cfg.DisableKeyboard = noKeyboard 62 | cfg.DisableMouse = noMouse 63 | cfg.ColdBoot = coldBoot 64 | cfg.SaveOnExit = saveOnExit 65 | cfg.NoConsole = noConsole 66 | cfg.EnableTTY = enableTTY 67 | cfg.ExitPattern = exitOn 68 | cfg.EnableNetwork = enableNet 69 | cfg.ChromeDP = useCDP 70 | 71 | cfg.ConsoleAddr = env86.ListenAddr() 72 | 73 | vm, err := env86.New(image, cfg) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | vm.Start() 78 | 79 | if consoleURL { 80 | fmt.Printf("Console URL: http://%s/console.html\n", env86.LocalhostAddr(cfg.ConsoleAddr)) 81 | } 82 | 83 | if !cfg.EnableTTY { 84 | go func() { 85 | buffer := make([]byte, 1024) 86 | for { 87 | _, err := os.Stdin.Read(buffer) 88 | if err == io.EOF { 89 | vm.Exit("Ctrl-D detected") 90 | return 91 | } 92 | } 93 | }() 94 | } 95 | 96 | if enableNet { 97 | // todo: not sure how to decide when to do this since most of the 98 | // time we don't really want to ... 99 | if cfg.PreserveMAC { 100 | // this adds the vm nic to the switch route table 101 | // so we can immediately dial the vm nic in port forwarding 102 | vm.Console().SendKeyboard("ping -c 1 192.168.127.1\n") 103 | } 104 | 105 | if portForward != "" { 106 | go forwardPort(vm.Network(), portForward) 107 | } 108 | } 109 | 110 | vm.Wait() 111 | }, 112 | } 113 | cmd.Flags().BoolVar(&useCDP, "cdp", false, "use headless chrome") 114 | cmd.Flags().BoolVar(&consoleURL, "console-url", false, "show the URL to the console") 115 | cmd.Flags().BoolVar(&saveOnExit, "save", false, "save initial state to image on exit") 116 | cmd.Flags().BoolVar(&coldBoot, "cold", false, "cold boot without initial state") 117 | cmd.Flags().BoolVar(&enableNet, "net", false, "enable networking") 118 | cmd.Flags().BoolVar(&enableNet, "n", false, "enable networking (shorthand)") 119 | cmd.Flags().BoolVar(&noConsole, "no-console", false, "disable console window") 120 | cmd.Flags().BoolVar(&enableTTY, "ttyS0", false, "open TTY over serial0") 121 | cmd.Flags().BoolVar(&noMouse, "no-mouse", false, "disable mouse") 122 | cmd.Flags().BoolVar(&noKeyboard, "no-keyboard", false, "disable keyboard") 123 | cmd.Flags().StringVar(&exitOn, "exit-on", "", "exit when string is matched in serial TTY") 124 | cmd.Flags().StringVar(&portForward, "p", "", "forward TCP port (ex: 8080:80)") 125 | return cmd 126 | } 127 | 128 | func forwardPort(vn *vnet.VirtualNetwork, spec string) error { 129 | parts := strings.Split(spec, ":") 130 | l, err := net.Listen("tcp", ":"+parts[0]) 131 | if err != nil { 132 | return err 133 | } 134 | defer l.Close() 135 | targetAddr := "192.168.127.2:" + parts[1] 136 | handle := func(conn net.Conn) { 137 | defer conn.Close() 138 | backend, err := vn.Dial("tcp", targetAddr) 139 | if err != nil { 140 | log.Printf("Failed to connect to target server: %v", err) 141 | return 142 | } 143 | defer backend.Close() 144 | done := make(chan struct{}) 145 | go func() { 146 | io.Copy(backend, conn) 147 | done <- struct{}{} 148 | }() 149 | go func() { 150 | io.Copy(conn, backend) 151 | done <- struct{}{} 152 | }() 153 | <-done 154 | } 155 | for { 156 | conn, err := l.Accept() 157 | if err != nil { 158 | continue 159 | } 160 | go handle(conn) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /cmd/env86/create.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "path" 9 | "path/filepath" 10 | 11 | "github.com/progrium/env86/assets" 12 | "github.com/progrium/env86/fsutil" 13 | "tractor.dev/toolkit-go/engine/cli" 14 | "tractor.dev/toolkit-go/engine/fs" 15 | "tractor.dev/toolkit-go/engine/fs/osfs" 16 | ) 17 | 18 | func createCmd() *cli.Command { 19 | var ( 20 | dir string 21 | docker string 22 | guest bool 23 | ) 24 | cmd := &cli.Command{ 25 | Usage: "create ", 26 | Short: "create an image from directory or using Docker", 27 | Args: cli.MinArgs(1), 28 | Run: func(ctx *cli.Context, args []string) { 29 | imagePath, err := filepath.Abs(args[0]) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | exists, err := fs.Exists(fsutil.RootFS(imagePath), fsutil.RootFSRelativePath(imagePath)) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | if exists { 38 | log.Fatal("image filepath already exists") 39 | } 40 | 41 | if dir != "" { 42 | dir, err := filepath.Abs(dir) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | isDir, err := fs.IsDir(fsutil.RootFS(dir), fsutil.RootFSRelativePath(dir)) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | if !isDir { 51 | log.Fatal("specified dir does not exist") 52 | } 53 | } 54 | 55 | // if docker image or dockerfile specified, generate dir from docker export 56 | if docker != "" { 57 | imageName := docker 58 | 59 | var err error 60 | docker, err = filepath.Abs(docker) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | isDockerfile, err := fs.Exists(fsutil.RootFS(docker), fsutil.RootFSRelativePath(docker)) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | if isDockerfile { 69 | ctxDir := filepath.Dir(docker) 70 | imageName = "env86-build" 71 | run(ctxDir, "docker", "build", "--platform=linux/386", "-t", imageName, "-f", docker, ".") 72 | } 73 | 74 | outDir, err := os.MkdirTemp("", "env86-create") 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | defer os.RemoveAll(outDir) 79 | 80 | run(outDir, "docker", "create", "--platform=linux/386", "--name=env86-create", imageName) 81 | run(outDir, "docker", "export", "env86-create", "-o", "fs.tar") 82 | run(outDir, "docker", "rm", "env86-create") 83 | os.MkdirAll(filepath.Join(outDir, "fs"), 0755) 84 | run(outDir, "tar", "-xvf", "fs.tar", "-C", "fs") 85 | run(outDir, "sh", "-c", "chmod -R +r fs") 86 | os.RemoveAll(filepath.Join(outDir, "fs.tar")) 87 | os.RemoveAll(filepath.Join(outDir, "fs/.dockerenv")) 88 | 89 | dir = filepath.Join(outDir, "fs") 90 | } 91 | 92 | if dir == "" { 93 | log.Fatal("nothing to create from") 94 | } 95 | 96 | if guest { 97 | if err := fsutil.CopyFS(assets.Dir, "guest86", osfs.New(), path.Join(dir, "bin/guest86")); err != nil { 98 | log.Fatal(err) 99 | } 100 | } 101 | 102 | if err := os.MkdirAll(imagePath, 0755); err != nil { 103 | log.Fatal(err) 104 | } 105 | 106 | GenerateIndex(filepath.Join(imagePath, "fs.json"), dir, nil) 107 | CopyToSha256(dir, filepath.Join(imagePath, "fs")) 108 | 109 | imageConfig := map[string]any{ 110 | "cmdline": "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci console=ttyS0 console=tty1", 111 | } 112 | 113 | if guest { 114 | imageConfig["has_guest_service"] = true 115 | } 116 | 117 | // look for bootable kernel 118 | var kernelMatches []string 119 | for _, p := range []string{"vmlinuz*", "boot/vmlinuz*", "bzimage*", "boot/bzimage*"} { 120 | m, err := fs.Glob(osfs.New(), filepath.Join(dir, p)) 121 | if err != nil { 122 | log.Fatal(err) 123 | } 124 | kernelMatches = append(kernelMatches, m...) 125 | } 126 | 127 | // look for initrd 128 | var initrdMatches []string 129 | for _, p := range []string{"initrd*", "boot/initrd*", "initramfs*", "boot/initramfs*"} { 130 | m, err := fs.Glob(osfs.New(), filepath.Join(dir, p)) 131 | if err != nil { 132 | log.Fatal(err) 133 | } 134 | initrdMatches = append(initrdMatches, m...) 135 | } 136 | 137 | // if both are found, assume use them 138 | if len(kernelMatches) > 0 && len(initrdMatches) > 0 { 139 | imageConfig["bzimage_initrd_from_filesystem"] = true 140 | } 141 | 142 | b, err := json.MarshalIndent(imageConfig, "", " ") 143 | if err != nil { 144 | log.Fatal(err) 145 | } 146 | if err := os.WriteFile(filepath.Join(imagePath, "image.json"), b, 0644); err != nil { 147 | log.Fatal(err) 148 | } 149 | 150 | }, 151 | } 152 | cmd.Flags().StringVar(&dir, "from-dir", "", "make image from directory root") 153 | cmd.Flags().StringVar(&docker, "from-docker", "", "make image from Docker image or Dockerfile") 154 | cmd.Flags().BoolVar(&guest, "with-guest", false, "add guest service to /bin") 155 | return cmd 156 | } 157 | 158 | func run(dir, name string, args ...string) { 159 | cmd := exec.Command(name, args...) 160 | cmd.Dir = dir 161 | out, err := cmd.CombinedOutput() 162 | if err != nil { 163 | os.Stderr.Write(out) 164 | log.Fatal(err) 165 | } 166 | } 167 | 168 | func stream(dir, name string, args ...string) { 169 | cmd := exec.Command(name, args...) 170 | cmd.Dir = dir 171 | cmd.Stdout = os.Stdout 172 | cmd.Stderr = os.Stderr 173 | if err := cmd.Run(); err != nil { 174 | log.Fatal(err) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /cmd/env86/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "os/user" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/progrium/env86/fsutil" 13 | 14 | "tractor.dev/toolkit-go/desktop" 15 | "tractor.dev/toolkit-go/engine/cli" 16 | "tractor.dev/toolkit-go/engine/fs" 17 | ) 18 | 19 | var Version = "dev" 20 | 21 | func main() { 22 | log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) 23 | 24 | root := &cli.Command{ 25 | Version: Version, 26 | Usage: "env86", 27 | Long: `env86 manages v86 emulated virtual machines`, 28 | } 29 | 30 | root.AddCommand(bootCmd()) 31 | root.AddCommand(prepareCmd()) 32 | root.AddCommand(networkCmd()) 33 | root.AddCommand(serveCmd()) 34 | root.AddCommand(createCmd()) 35 | root.AddCommand(assetsCmd()) 36 | root.AddCommand(runCmd()) 37 | root.AddCommand(pullCmd()) 38 | 39 | desktop.Start(func() { 40 | if err := cli.Execute(context.Background(), root, os.Args[1:]); err != nil { 41 | log.Fatal(err) 42 | } 43 | desktop.Stop() 44 | }) 45 | } 46 | 47 | func env86Path() string { 48 | path := os.Getenv("ENV86_PATH") 49 | if path == "" { 50 | if runtime.GOOS == "windows" { 51 | path = filepath.Join(os.Getenv("APPDATA"), "env86") 52 | } else { 53 | usr, _ := user.Current() 54 | path = usr.HomeDir + "/.env86" 55 | } 56 | } 57 | return path 58 | } 59 | 60 | // globalImage resolves a pathspec to a global image path 61 | // On Unix-like systems: 62 | // github.com/progrium/alpine@latest => ~/.env86/github.com/progrium/alpine/3.18 63 | // On Windows: 64 | // github.com/progrium/alpine@latest => %APPDATA%\env86\github.com\progrium\alpine\3.18 65 | func globalImage(pathspec string) (bool, string) { 66 | parts := strings.Split(pathspec, "@") 67 | image := strings.TrimSuffix(parts[0], "-env86") 68 | tag := "latest" 69 | if len(parts) > 1 { 70 | tag = parts[1] 71 | } 72 | 73 | path := filepath.Join(env86Path(), image, tag) 74 | resolved, err := filepath.EvalSymlinks(path) 75 | if err == nil { 76 | path = resolved 77 | } 78 | ok, err := fs.Exists(fsutil.RootFS(path), fsutil.RootFSRelativePath(path)) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | if ok { 83 | return true, path 84 | } 85 | 86 | // if tag explicitly specified and not found 87 | if len(parts) > 1 { 88 | return false, path 89 | } 90 | // if no tag specified and latest not found, try local 91 | path = filepath.Join(env86Path(), image, "local") 92 | ok, err = fs.Exists(fsutil.RootFS(path), fsutil.RootFSRelativePath(path)) 93 | if err != nil { 94 | log.Fatal(err) 95 | } 96 | if ok { 97 | return true, path 98 | } 99 | return false, path 100 | } 101 | -------------------------------------------------------------------------------- /cmd/env86/network.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/progrium/env86" 10 | "github.com/progrium/env86/network" 11 | 12 | "github.com/progrium/go-netstack/vnet" 13 | "tractor.dev/toolkit-go/engine/cli" 14 | ) 15 | 16 | func networkCmd() *cli.Command { 17 | cmd := &cli.Command{ 18 | Usage: "network", 19 | Short: "run virtual network and relay", 20 | // Args: cli.MinArgs(1), 21 | Run: func(ctx *cli.Context, args []string) { 22 | vn, err := vnet.New(&vnet.Configuration{ 23 | Debug: false, 24 | MTU: 1500, 25 | Subnet: "192.168.127.0/24", 26 | GatewayIP: "192.168.127.1", 27 | GatewayMacAddress: "5a:94:ef:e4:0c:dd", 28 | GatewayVirtualIPs: []string{"192.168.127.253"}, 29 | }) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | addr := env86.ListenAddr() 35 | hostname := strings.ReplaceAll(addr, "0.0.0.0", "localhost") 36 | fmt.Printf("Network URL: ws://%s\n", hostname) 37 | if err := http.ListenAndServe(addr, network.Handler(vn)); err != nil { 38 | log.Fatal(err) 39 | } 40 | }, 41 | } 42 | return cmd 43 | } 44 | -------------------------------------------------------------------------------- /cmd/env86/prepare.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/progrium/env86" 10 | "github.com/progrium/env86/assets" 11 | "github.com/progrium/env86/fsutil" 12 | 13 | "tractor.dev/toolkit-go/engine/cli" 14 | "tractor.dev/toolkit-go/engine/fs" 15 | "tractor.dev/toolkit-go/engine/fs/osfs" 16 | ) 17 | 18 | func prepareCmd() *cli.Command { 19 | cmd := &cli.Command{ 20 | Usage: "prepare ", 21 | Short: "prepare a VM for publishing on the web", 22 | Args: cli.MinArgs(2), 23 | Run: func(ctx *cli.Context, args []string) { 24 | imagePath := args[0] 25 | var err error 26 | if !strings.HasPrefix(imagePath, "./") && !strings.HasPrefix(imagePath, ".\\") { 27 | exists, fullPath := globalImage(imagePath) 28 | if !exists { 29 | log.Fatal("global image not found") 30 | } 31 | imagePath = fullPath 32 | } else { 33 | imagePath, err = filepath.Abs(imagePath) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | } 38 | 39 | image, err := env86.LoadImage(imagePath) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | dstPath, err := filepath.Abs(args[1]) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | exists, err := fs.DirExists(fsutil.RootFS(dstPath), fsutil.RootFSRelativePath(dstPath)) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | if exists { 53 | log.Fatal("destination dir already exists") 54 | } 55 | os.MkdirAll(dstPath, 0755) 56 | 57 | preparedImage, err := image.Prepare() 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | 62 | // osfs currently works with os native paths 63 | dst := osfs.Dir(dstPath) 64 | dst.MkdirAll("image", 0755) 65 | if err := fsutil.CopyFS(preparedImage, ".", dst, "image"); err != nil { 66 | log.Fatal(err) 67 | } 68 | 69 | bundle, err := assets.BundleJavaScript() 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | if err := fs.WriteFile(dst, "env86.min.js", bundle, 0644); err != nil { 74 | log.Fatal(err) 75 | } 76 | if err := fsutil.CopyFS(assets.Dir, "v86.wasm", dst, "v86.wasm"); err != nil { 77 | log.Fatal(err) 78 | } 79 | if err := fsutil.CopyFS(assets.Dir, "index.html", dst, "index.html"); err != nil { 80 | log.Fatal(err) 81 | } 82 | }, 83 | } 84 | return cmd 85 | } 86 | -------------------------------------------------------------------------------- /cmd/env86/pull.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "os" 11 | "path" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/progrium/env86/fsutil" 16 | "tractor.dev/toolkit-go/engine/cli" 17 | "tractor.dev/toolkit-go/engine/fs/osfs" 18 | ) 19 | 20 | func pullCmd() *cli.Command { 21 | cmd := &cli.Command{ 22 | Usage: "pull [@tag] []", 23 | Short: "pull an image from a repository, optionally as a new image", 24 | Args: cli.MinArgs(1), 25 | Run: func(ctx *cli.Context, args []string) { 26 | if strings.HasPrefix(args[0], "http://") || strings.HasPrefix(args[0], "https://") { 27 | log.Fatal("malformed image path. protocols are not supported") 28 | } 29 | if !strings.HasPrefix(args[0], "github.com") { 30 | log.Fatal("only github repositories are supported at the moment") 31 | } 32 | parts := strings.Split(args[0], "@") 33 | repo := parts[0] 34 | imageBase := strings.TrimSuffix(filepath.Base(repo), "-env86") 35 | tag := "latest" 36 | if len(parts) > 1 { 37 | tag = parts[1] 38 | } 39 | 40 | exists, imageDst := globalImage(args[0]) 41 | if exists { 42 | log.Fatal("image already exists") 43 | } 44 | 45 | // fail if repo doesn't exist (trying also with -env86 suffix) 46 | repoURL := fmt.Sprintf("https://%s", repo) 47 | resp, err := http.Get(repoURL) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | if resp.StatusCode == 404 { 52 | repoURL = fmt.Sprintf("https://%s-env86", repo) 53 | resp, err = http.Get(repoURL) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | if resp.StatusCode == 404 { 58 | log.Fatal("image repo does not exist:", repoURL) 59 | } 60 | } 61 | 62 | // fail if repo releases don't exist 63 | latestReleaseURL := fmt.Sprintf("%s/releases/latest", repoURL) 64 | resp, err = http.Get(latestReleaseURL) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | if resp.Request.URL.String() == fmt.Sprintf("%s/releases", repoURL) { 69 | log.Fatal("image repo has no releases") 70 | } 71 | 72 | // fail if specific release doesn't exist 73 | if tag != "latest" { 74 | releaseURL := fmt.Sprintf("%s/releases/%s", repoURL, tag) 75 | resp, err = http.Get(releaseURL) 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | if resp.StatusCode == 404 { 80 | log.Fatal("image repo tag does not exist:", releaseURL) 81 | } 82 | } else { 83 | tag = path.Base(resp.Request.URL.Path) 84 | } 85 | if filepath.Base(imageDst) == "local" { 86 | imageDst = filepath.Join(filepath.Dir(imageDst), tag) 87 | } 88 | 89 | downloadURL := fmt.Sprintf("%s/releases/download/%s/%s-%s.tgz", repoURL, tag, imageBase, tag) 90 | resp, err = http.Get(downloadURL) 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | if resp.StatusCode != 200 { 95 | log.Fatal("unexpected status code fetching image repo tag asset: ", resp.StatusCode, downloadURL) 96 | } 97 | defer resp.Body.Close() 98 | 99 | tmpFile, err := os.CreateTemp("", "env86-pull") 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | defer os.Remove(tmpFile.Name()) 104 | 105 | _, err = io.Copy(tmpFile, resp.Body) 106 | if err != nil { 107 | log.Fatal(err) 108 | } 109 | _, err = tmpFile.Seek(0, 0) 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | imageUnzipped, err := gzip.NewReader(tmpFile) 114 | if err != nil { 115 | log.Fatal(err) 116 | } 117 | defer imageUnzipped.Close() 118 | 119 | imageTar := tar.NewReader(imageUnzipped) 120 | 121 | if err := os.MkdirAll(imageDst, 0755); err != nil { 122 | log.Fatal(err) 123 | } 124 | 125 | for { 126 | header, err := imageTar.Next() 127 | if err == io.EOF { 128 | break 129 | } 130 | if err != nil { 131 | log.Fatal(err) 132 | } 133 | 134 | path := filepath.Join(imageDst, header.Name) 135 | 136 | switch header.Typeflag { 137 | case tar.TypeDir: 138 | if err := os.MkdirAll(path, os.FileMode(header.Mode)); err != nil { 139 | log.Fatal(err) 140 | } 141 | case tar.TypeReg: 142 | outFile, err := os.Create(path) 143 | if err != nil { 144 | log.Fatal(err) 145 | } 146 | defer outFile.Close() 147 | 148 | if _, err := io.Copy(outFile, imageTar); err != nil { 149 | log.Fatal(err) 150 | } 151 | default: 152 | log.Printf("Unknown type: %v in %s\n", header.Typeflag, header.Name) 153 | } 154 | } 155 | 156 | // TODO: set latest symlink if specified or implied tag was latest 157 | 158 | if len(args) < 2 { 159 | return 160 | } 161 | newImage := args[1] 162 | if !strings.HasPrefix(newImage, "./") && !strings.HasPrefix(newImage, ".\\") { 163 | exists, fullPath := globalImage(newImage) 164 | if exists { 165 | log.Fatal("global image already exists") 166 | } 167 | newImage = fullPath 168 | } else { 169 | newImage, err = filepath.Abs(newImage) 170 | if err != nil { 171 | log.Fatal(err) 172 | } 173 | } 174 | os.MkdirAll(filepath.Dir(newImage), 0755) 175 | if err := fsutil.CopyFS(osfs.New(), imageDst, osfs.New(), newImage); err != nil { 176 | log.Fatal(err) 177 | } 178 | }, 179 | } 180 | return cmd 181 | } 182 | -------------------------------------------------------------------------------- /cmd/env86/run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | 8 | "github.com/progrium/env86" 9 | "golang.org/x/term" 10 | 11 | "tractor.dev/toolkit-go/engine/cli" 12 | ) 13 | 14 | func runCmd() *cli.Command { 15 | var ( 16 | enableNet bool 17 | portForward string 18 | useCDP bool 19 | mountSpec string 20 | ) 21 | cmd := &cli.Command{ 22 | Usage: "run [...]", 23 | Short: "run a program in the VM (requires guest service)", 24 | Args: cli.MinArgs(2), 25 | Run: func(ctx *cli.Context, args []string) { 26 | image, err := env86.LoadImage(args[0]) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | cfg, err := image.Config() 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | cfg.EnableNetwork = enableNet 36 | cfg.ChromeDP = useCDP 37 | cfg.ConsoleAddr = env86.ListenAddr() 38 | cfg.NoConsole = true 39 | 40 | vm, err := env86.New(image, cfg) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | vm.Start() 45 | 46 | if vm.Guest() == nil { 47 | log.Fatal("guest not found") 48 | } 49 | 50 | if err := vm.Guest().ResetNetwork(); err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | if mountSpec != "" { 55 | parts := strings.SplitN(mountSpec, ":", 2) 56 | go func() { 57 | if err := vm.Guest().Mount(parts[0], parts[1]); err != nil { 58 | log.Println(err) 59 | } 60 | }() 61 | } 62 | 63 | if enableNet { 64 | // todo: not sure how to decide when to do this since most of the 65 | // time we don't really want to ... 66 | if cfg.PreserveMAC { 67 | // this adds the vm nic to the switch route table 68 | // so we can immediately dial the vm nic in port forwarding 69 | vm.Console().SendKeyboard("ping -c 1 192.168.127.1\n") 70 | } 71 | 72 | if portForward != "" { 73 | go forwardPort(vm.Network(), portForward) 74 | } 75 | } 76 | 77 | cmd := vm.Guest().Command(args[1], args[2:]...) 78 | cmd.Stdin = os.Stdin 79 | cmd.Stdout = os.Stdout 80 | cmd.Stderr = os.Stderr 81 | oldstate, err := term.MakeRaw(int(os.Stdin.Fd())) 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | status, err := cmd.Run() 86 | term.Restore(int(os.Stdin.Fd()), oldstate) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | vm.Stop() 91 | os.Exit(status) 92 | }, 93 | } 94 | cmd.Flags().BoolVar(&useCDP, "cdp", false, "use headless chrome") 95 | cmd.Flags().BoolVar(&enableNet, "net", false, "enable networking") 96 | cmd.Flags().BoolVar(&enableNet, "n", false, "enable networking (shorthand)") 97 | cmd.Flags().StringVar(&portForward, "p", "", "forward TCP port (ex: 8080:80)") 98 | cmd.Flags().StringVar(&mountSpec, "m", "", "mount a directory (ex: .:/mnt/host)") 99 | return cmd 100 | } 101 | -------------------------------------------------------------------------------- /cmd/env86/serve.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/progrium/env86" 11 | "github.com/progrium/env86/assets" 12 | "github.com/progrium/env86/namespacefs" 13 | 14 | "tractor.dev/toolkit-go/engine/cli" 15 | ) 16 | 17 | func serveCmd() *cli.Command { 18 | cmd := &cli.Command{ 19 | Usage: "serve ", 20 | Short: "serve a VM and debug console over HTTP", 21 | Args: cli.MinArgs(1), 22 | Run: func(ctx *cli.Context, args []string) { 23 | image, err := env86.LoadImage(args[0]) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | preparedImage, err := image.Prepare() 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | fsys := namespacefs.New() 34 | if err := fsys.Mount(assets.Dir, "/"); err != nil { 35 | log.Fatal(err) 36 | } 37 | if err := fsys.Mount(preparedImage, "image"); err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | bundle, err := assets.BundleJavaScript() 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | mux := http.NewServeMux() 47 | mux.Handle("/", http.FileServerFS(fsys)) 48 | mux.Handle("/env86.min.js", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 | w.Header().Add("content-type", "text/javascript") 50 | io.Copy(w, bytes.NewBuffer(bundle)) 51 | })) 52 | 53 | fmt.Println("serving: http://localhost:9999/") 54 | http.ListenAndServe(":9999", mux) 55 | }, 56 | } 57 | return cmd 58 | } 59 | -------------------------------------------------------------------------------- /cmd/env86/v86util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/gzip" 7 | "crypto/sha256" 8 | "encoding/hex" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "io/fs" 13 | "log" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | ) 18 | 19 | // v86 tool scripts fs2json.py and copy-to-sha256.py 20 | // ported to go by Michael Hermenault 21 | 22 | const ( 23 | VERSION = 3 24 | IDX_NAME = 0 25 | IDX_SIZE = 1 26 | IDX_MTIME = 2 27 | IDX_MODE = 3 28 | IDX_UID = 4 29 | IDX_GID = 5 30 | IDX_TARGET = 6 31 | IDX_FILENAME = 6 32 | HASH_LENGTH = 8 33 | S_IFLNK = 0xA000 34 | S_IFREG = 0x8000 35 | S_IFDIR = 0x4000 36 | ) 37 | 38 | func GenerateIndex(outFile string, path string, exclude []string) { 39 | excludes := stringSlice(exclude) 40 | path = filepath.Clean(path) 41 | var root []interface{} 42 | var totalSize int64 43 | 44 | fi, err := os.Stat(path) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | if fi.IsDir() { 50 | root, totalSize = indexHandleDir(path, excludes) 51 | } else { 52 | f, err := os.Open(path) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | defer f.Close() 57 | 58 | } 59 | 60 | result := map[string]interface{}{ 61 | "fsroot": root, 62 | "version": VERSION, 63 | "size": totalSize, 64 | } 65 | 66 | // logger.Println("Creating json ...") 67 | enc := json.NewEncoder(os.Stdout) 68 | if outFile != "" { 69 | f, err := os.Create(outFile) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | defer f.Close() 74 | enc = json.NewEncoder(f) 75 | } 76 | enc.Encode(result) 77 | } 78 | 79 | func indexHandleDir(path string, excludes []string) ([]interface{}, int64) { 80 | var totalSize int64 81 | mainRoot := make([]interface{}, 0) 82 | filenameToHash := make(map[string]string) 83 | 84 | err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error { 85 | if err != nil { 86 | log.Printf("Error accessing path %q: %v\n", filePath, err) 87 | return nil 88 | } 89 | 90 | relPath, err := filepath.Rel(path, filePath) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | parts := strings.Split(relPath, string(os.PathSeparator)) 96 | for _, exclude := range excludes { 97 | if parts[0] == exclude { 98 | if info.IsDir() { 99 | return filepath.SkipDir 100 | } 101 | return nil 102 | } 103 | } 104 | 105 | name := parts[len(parts)-1] 106 | dir := &mainRoot 107 | if name == "." { 108 | return nil 109 | } 110 | for _, p := range parts[:len(parts)-1] { 111 | for _, lilD := range *dir { 112 | lilD, _ := lilD.([]interface{}) 113 | name, ok := lilD[IDX_NAME].(string) 114 | if ok && name == p { 115 | newDir, ok := lilD[IDX_TARGET].(*[]interface{}) 116 | if !ok { 117 | log.Panic("could not cast refrence to slice") 118 | } 119 | dir = newDir 120 | break 121 | } 122 | } 123 | } 124 | obj := make([]interface{}, 7) 125 | 126 | _, err = os.Lstat(filePath) 127 | if err != nil { 128 | log.Fatal(err) 129 | } 130 | 131 | var UID int 132 | var GID int 133 | // if stat, ok := statInfo.Sys().(*syscall.Stat_t); ok { 134 | // UID = int(stat.Uid) 135 | // GID = int(stat.Gid) 136 | // } 137 | 138 | obj[IDX_NAME] = name 139 | obj[IDX_SIZE] = info.Size() 140 | obj[IDX_MTIME] = info.ModTime().Unix() 141 | obj[IDX_MODE] = int64(info.Mode()) 142 | obj[IDX_UID] = UID // Not available in Go's os.FileInfo 143 | obj[IDX_GID] = GID // Not available in Go's os.FileInfo 144 | 145 | if info.Mode()&os.ModeSymlink != 0 { 146 | obj[IDX_MODE] = obj[IDX_MODE].(int64) | S_IFLNK 147 | target, err := os.Readlink(filePath) 148 | if err != nil { 149 | return err 150 | } 151 | obj[IDX_TARGET] = target 152 | } else if info.IsDir() { 153 | obj[IDX_MODE] = obj[IDX_MODE].(int64) | S_IFDIR 154 | newDir := make([]interface{}, 0) 155 | obj[IDX_TARGET] = &newDir 156 | } else { 157 | obj[IDX_MODE] = obj[IDX_MODE].(int64) | S_IFREG 158 | fileHash, err := hashFile(filePath) 159 | if err != nil { 160 | return err 161 | } 162 | filename := fileHash[:HASH_LENGTH] + ".bin" 163 | if existing, ok := filenameToHash[filename]; ok { 164 | if existing != fileHash { 165 | return fmt.Errorf("collision in short hash (%s and %s)", existing, fileHash) 166 | } 167 | } 168 | filenameToHash[filename] = fileHash 169 | obj[IDX_FILENAME] = filename 170 | } 171 | 172 | totalSize += info.Size() 173 | *dir = append(*dir, obj) 174 | 175 | return nil 176 | }) 177 | 178 | if err != nil { 179 | log.Fatal(err) 180 | } 181 | return mainRoot, totalSize 182 | } 183 | 184 | type stringSlice []string 185 | 186 | func (s *stringSlice) String() string { 187 | return strings.Join(*s, ",") 188 | } 189 | 190 | func (s *stringSlice) Set(value string) error { 191 | *s = append(*s, value) 192 | return nil 193 | } 194 | 195 | func hashFile(filename string) (string, error) { 196 | f, err := os.Open(filename) 197 | if err != nil { 198 | return "", err 199 | } 200 | defer f.Close() 201 | return hashFileObject(f), nil 202 | } 203 | 204 | func hashFileObject(f io.Reader) string { 205 | h := sha256.New() 206 | if _, err := io.Copy(h, f); err != nil { 207 | log.Fatal(err) 208 | } 209 | return hex.EncodeToString(h.Sum(nil)) 210 | } 211 | 212 | ////// 213 | 214 | func CopyToSha256(fromPath, toPath string) { 215 | os.MkdirAll(toPath, 0755) 216 | fromFile, err := os.Open(fromPath) 217 | if err != nil { 218 | log.Fatal(err) 219 | } 220 | _, err = gzip.NewReader(fromFile) 221 | 222 | fromFile.Close() 223 | if err != nil { 224 | handleDir(fromPath, toPath) 225 | return 226 | } 227 | handleTar(fromPath, toPath) 228 | 229 | } 230 | 231 | func handleTar(fromPath, toPath string) { 232 | fromFile, err := os.Open(fromPath) 233 | if err != nil { 234 | log.Fatal(err) 235 | } 236 | defer fromFile.Close() 237 | gzip, _ := gzip.NewReader(fromFile) 238 | 239 | tarReader := tar.NewReader(gzip) 240 | 241 | for { 242 | header, err := tarReader.Next() 243 | 244 | if err == io.EOF { 245 | break 246 | } 247 | 248 | if err != nil { 249 | fmt.Println(err) 250 | os.Exit(1) 251 | } 252 | 253 | switch header.Typeflag { 254 | case tar.TypeDir: 255 | continue 256 | case tar.TypeReg, tar.TypeLink: 257 | var buf bytes.Buffer 258 | tee := io.TeeReader(tarReader, &buf) 259 | fileHash := hashFileObject(tee) 260 | absPath := filepath.Join(toPath, fileHash[0:HASH_LENGTH]+".bin") 261 | if _, err := os.Stat(absPath); err == nil { 262 | //log.Printf("Exists, skipped %s (%s)\n", absPath, header.Name) 263 | continue 264 | } 265 | copyFileObject(&buf, absPath) 266 | } 267 | } 268 | } 269 | 270 | func handleDir(fromPath, toPath string) { 271 | visit := func(path string, di fs.DirEntry, dirError error) error { 272 | if di.Type() == fs.ModeSymlink || di.Type() == fs.ModeCharDevice || di.Type() == fs.ModeDevice || di.Type() == fs.ModeDevice || di.Type() == fs.ModeSocket || di.Type() == fs.ModeDir { 273 | return nil 274 | } 275 | fromFile, err := os.Open(path) 276 | if err != nil { 277 | log.Fatalf("Error reading file %s\n", path) 278 | } 279 | defer fromFile.Close() 280 | fileHash := hashFileObject(fromFile) 281 | absPath := filepath.Join(toPath, fileHash[0:HASH_LENGTH]+".bin") 282 | if _, err := os.Stat(absPath); err == nil { 283 | //log.Printf("Exists, skipped %s (%s)\n", absPath, path) 284 | return nil 285 | } 286 | copyFileContents(path, absPath) 287 | return nil 288 | } 289 | 290 | filepath.WalkDir(fromPath, visit) 291 | } 292 | 293 | func copyFileContents(src, dst string) (err error) { 294 | in, err := os.Open(src) 295 | if err != nil { 296 | return 297 | } 298 | defer in.Close() 299 | copyFileObject(in, dst) 300 | return 301 | } 302 | 303 | func copyFileObject(src io.Reader, dst string) (err error) { 304 | out, err := os.Create(dst) 305 | if err != nil { 306 | return 307 | } 308 | defer func() { 309 | cerr := out.Close() 310 | if err == nil { 311 | err = cerr 312 | } 313 | }() 314 | if _, err = io.Copy(out, src); err != nil { 315 | return 316 | } 317 | err = out.Sync() 318 | return 319 | } 320 | -------------------------------------------------------------------------------- /cmd/guest86/exec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os/exec" 7 | 8 | "github.com/creack/pty" 9 | "tractor.dev/toolkit-go/duplex/rpc" 10 | ) 11 | 12 | type RunInput struct { 13 | Name string 14 | Args []string 15 | Env []string 16 | Dir string 17 | PTY bool 18 | } 19 | 20 | type RunOutput struct { 21 | Stdout []byte 22 | Stderr []byte 23 | Status *int 24 | } 25 | 26 | func (api *API) Run(r rpc.Responder, c *rpc.Call) { 27 | var in RunInput 28 | c.Receive(&in) 29 | 30 | cmd := exec.Command(in.Name, in.Args...) 31 | cmd.Dir = in.Dir 32 | cmd.Env = in.Env 33 | 34 | var ch io.Closer 35 | var err error 36 | if in.PTY { 37 | ch, err = api.runPty(r, cmd) 38 | } else { 39 | ch, err = api.runNoPty(r, cmd) 40 | } 41 | if err != nil { 42 | r.Return(err) 43 | return 44 | } 45 | defer ch.Close() 46 | 47 | status := 0 48 | if err := cmd.Wait(); err != nil { 49 | if exitErr, ok := err.(*exec.ExitError); ok { 50 | status = exitErr.ExitCode() 51 | } else { 52 | log.Println(err) 53 | } 54 | } 55 | r.Send(RunOutput{Status: &status}) 56 | } 57 | 58 | func (api *API) runPty(r rpc.Responder, cmd *exec.Cmd) (io.Closer, error) { 59 | tty, err := pty.Start(cmd) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | ch, err := r.Continue(cmd.Process.Pid) 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | go func() { 70 | io.Copy(tty, ch) 71 | }() 72 | 73 | go func() { 74 | buf := make([]byte, 1024) 75 | for { 76 | n, err := tty.Read(buf) 77 | if err != nil { 78 | if err != io.EOF { 79 | log.Println(err) 80 | } 81 | return 82 | } 83 | r.Send(RunOutput{Stdout: buf[:n]}) 84 | } 85 | }() 86 | 87 | return ch, nil 88 | } 89 | 90 | func (api *API) runNoPty(r rpc.Responder, cmd *exec.Cmd) (io.Closer, error) { 91 | stdin, err := cmd.StdinPipe() 92 | if err != nil { 93 | panic(err) 94 | } 95 | stdout, err := cmd.StdoutPipe() 96 | if err != nil { 97 | panic(err) 98 | } 99 | stderr, err := cmd.StderrPipe() 100 | if err != nil { 101 | panic(err) 102 | } 103 | 104 | // todo: set group id for subprocs 105 | 106 | if err := cmd.Start(); err != nil { 107 | return nil, err 108 | } 109 | 110 | ch, err := r.Continue(cmd.Process.Pid) 111 | if err != nil { 112 | panic(err) 113 | } 114 | 115 | go func() { 116 | io.Copy(stdin, ch) 117 | stdin.Close() 118 | }() 119 | 120 | go func() { 121 | buf := make([]byte, 1024) 122 | for { 123 | n, err := stdout.Read(buf) 124 | if err != nil { 125 | if err != io.EOF { 126 | log.Println(err) 127 | } 128 | return 129 | } 130 | r.Send(RunOutput{Stdout: buf[:n]}) 131 | } 132 | }() 133 | 134 | go func() { 135 | buf := make([]byte, 1024) 136 | for { 137 | n, err := stderr.Read(buf) 138 | if err != nil { 139 | if err != io.EOF { 140 | log.Println(err) 141 | } 142 | return 143 | } 144 | r.Send(RunOutput{Stderr: buf[:n]}) 145 | } 146 | }() 147 | 148 | return ch, nil 149 | } 150 | 151 | func (api *API) Signal(pid, sig int) error { 152 | // TODO 153 | return nil 154 | } 155 | 156 | func (api *API) Terminate(pid int) error { 157 | // TODO 158 | return nil 159 | } 160 | -------------------------------------------------------------------------------- /cmd/guest86/fs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | 6 | "tractor.dev/toolkit-go/engine/fs" 7 | ) 8 | 9 | type Entry struct { 10 | IsDir bool 11 | Ctime int 12 | Mtime int 13 | Size int 14 | Name string 15 | } 16 | 17 | func (api *API) Stat(path string) (*Entry, error) { 18 | fi, err := fs.Stat(api.FS, path) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return &Entry{ 23 | Name: fi.Name(), 24 | Mtime: int(fi.ModTime().Unix()), 25 | IsDir: fi.IsDir(), 26 | Ctime: 0, 27 | Size: int(fi.Size()), 28 | }, nil 29 | } 30 | 31 | func (api *API) ReadFile(path string) ([]byte, error) { 32 | return fs.ReadFile(api.FS, path) 33 | } 34 | 35 | func (api *API) ReadDir(path string) ([]Entry, error) { 36 | dir, err := fs.ReadDir(api.FS, path) 37 | if err != nil { 38 | return nil, err 39 | } 40 | var entries []Entry 41 | for _, e := range dir { 42 | fi, _ := e.Info() 43 | entries = append(entries, Entry{ 44 | Name: fi.Name(), 45 | Mtime: int(fi.ModTime().Unix()), 46 | IsDir: fi.IsDir(), 47 | Ctime: 0, 48 | Size: int(fi.Size()), 49 | }) 50 | } 51 | return entries, nil 52 | } 53 | 54 | func (api *API) WriteFile(path string, data []byte) error { 55 | return fs.WriteFile(api.FS, path, data, 0644) 56 | } 57 | 58 | func (api *API) MakeDir(path string) error { 59 | return fs.MkdirAll(api.FS, path, 0744) 60 | } 61 | 62 | func (api *API) RemoveAll(path string) error { 63 | rfs, ok := api.FS.(interface { 64 | RemoveAll(path string) error 65 | }) 66 | if !ok { 67 | return errors.ErrUnsupported 68 | } 69 | return rfs.RemoveAll(path) 70 | } 71 | 72 | func (api *API) Rename(path, newpath string) error { 73 | rfs, ok := api.FS.(interface { 74 | Rename(oldname, newname string) error 75 | }) 76 | if !ok { 77 | return errors.ErrUnsupported 78 | } 79 | return rfs.Rename(path, newpath) 80 | } 81 | -------------------------------------------------------------------------------- /cmd/guest86/go.mod: -------------------------------------------------------------------------------- 1 | module guest 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/creack/pty v1.1.21 7 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 8 | tractor.dev/toolkit-go v0.0.0-20240731233937-ae3586204eaa 9 | ) 10 | 11 | require ( 12 | github.com/fxamacker/cbor/v2 v2.5.0 // indirect 13 | github.com/mitchellh/mapstructure v1.5.0 // indirect 14 | github.com/x448/float16 v0.8.4 // indirect 15 | golang.org/x/net v0.17.0 // indirect 16 | golang.org/x/sys v0.13.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /cmd/guest86/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= 2 | github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 3 | github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= 4 | github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= 5 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 6 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 7 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= 8 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 9 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 10 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 11 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 12 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 13 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 14 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | tractor.dev/toolkit-go v0.0.0-20240731233937-ae3586204eaa h1:Y/12K1pZSD2eNP66ZsNPdV1RapbchCyuWzarZl8SGQs= 16 | tractor.dev/toolkit-go v0.0.0-20240731233937-ae3586204eaa/go.mod h1:vI9Jf9tepHLrUqGQf7XZuRcQySNajWRKBjPD4+Ay72I= 17 | -------------------------------------------------------------------------------- /cmd/guest86/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os/exec" 7 | 8 | "github.com/tarm/serial" 9 | "tractor.dev/toolkit-go/duplex/codec" 10 | "tractor.dev/toolkit-go/duplex/fn" 11 | "tractor.dev/toolkit-go/duplex/mux" 12 | "tractor.dev/toolkit-go/duplex/talk" 13 | "tractor.dev/toolkit-go/engine/fs" 14 | "tractor.dev/toolkit-go/engine/fs/osfs" 15 | ) 16 | 17 | var Version = "dev" 18 | 19 | func main() { 20 | flag.Parse() 21 | serialPort := flag.Arg(0) 22 | if serialPort == "" { 23 | serialPort = "/dev/ttyS1" 24 | } 25 | 26 | port, err := serial.OpenPort(&serial.Config{ 27 | Name: serialPort, 28 | Baud: 115200, 29 | }) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | peer := talk.NewPeer(mux.New(port), codec.CBORCodec{}) 35 | peer.Handle("vm", fn.HandlerFrom(&API{ 36 | FS: osfs.New(), 37 | })) 38 | log.Println("guest service running on", serialPort) 39 | peer.Respond() 40 | } 41 | 42 | type API struct { 43 | FS fs.FS 44 | } 45 | 46 | func (api *API) Version() string { 47 | return Version 48 | } 49 | 50 | // this is somewhat specific to Alpine... 51 | func (api *API) ResetNetwork() error { 52 | cmd := exec.Command("sh", "-c", "rmmod ne2k-pci && modprobe ne2k-pci && hwclock -s && ifconfig lo up && hostname localhost && setup-interfaces -a -r") 53 | _, err := cmd.CombinedOutput() 54 | return err 55 | 56 | } 57 | -------------------------------------------------------------------------------- /cmd/guest86/mount.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net" 8 | "os" 9 | "os/exec" 10 | "sync" 11 | 12 | "tractor.dev/toolkit-go/duplex/rpc" 13 | ) 14 | 15 | func (api *API) Mount9P(r rpc.Responder, c *rpc.Call) { 16 | var mountPath string 17 | c.Receive(&mountPath) 18 | l, err := net.Listen("tcp4", ":0") 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | _, port, _ := net.SplitHostPort(l.Addr().String()) 23 | os.MkdirAll(mountPath, 0755) 24 | go func() { 25 | cmd := exec.Command("mount", "-t", "9p", "-o", fmt.Sprintf("trans=tcp,port=%s", port), "127.0.0.1", mountPath) 26 | if err := cmd.Run(); err != nil { 27 | log.Fatal(err) 28 | } 29 | }() 30 | conn, err := l.Accept() 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | ch, err := r.Continue(nil) 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | var wg sync.WaitGroup 40 | 41 | wg.Add(1) 42 | go func() { 43 | io.Copy(ch, conn) 44 | wg.Done() 45 | }() 46 | wg.Add(1) 47 | go func() { 48 | io.Copy(conn, ch) 49 | wg.Done() 50 | }() 51 | 52 | wg.Wait() 53 | ch.Close() 54 | } 55 | 56 | func (api *API) MountFuse(path, selector string) error { 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /cmd/guest86/tcp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync" 7 | 8 | "tractor.dev/toolkit-go/duplex/rpc" 9 | ) 10 | 11 | func (api *API) Dial(r rpc.Responder, c *rpc.Call) { 12 | var addr string 13 | c.Receive(&addr) 14 | 15 | conn, err := net.Dial("tcp", addr) 16 | if err != nil { 17 | r.Return(err) 18 | return 19 | } 20 | 21 | ch, err := r.Continue() 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | var wg sync.WaitGroup 27 | 28 | wg.Add(1) 29 | go func() { 30 | io.Copy(ch, conn) 31 | wg.Done() 32 | }() 33 | wg.Add(1) 34 | go func() { 35 | io.Copy(conn, ch) 36 | wg.Done() 37 | }() 38 | 39 | wg.Wait() 40 | ch.Close() 41 | } 42 | -------------------------------------------------------------------------------- /cmd/guest86/tty.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os/exec" 6 | "sync" 7 | 8 | "github.com/creack/pty" 9 | "tractor.dev/toolkit-go/duplex/rpc" 10 | ) 11 | 12 | func (api *API) Terminal(r rpc.Responder, c *rpc.Call) { 13 | c.Receive(nil) 14 | 15 | cmd := exec.Command("/bin/sh") 16 | f, err := pty.Start(cmd) 17 | if err != nil { 18 | r.Return(err) 19 | return 20 | } 21 | 22 | ch, err := r.Continue() 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | var wg sync.WaitGroup 28 | 29 | wg.Add(1) 30 | go func() { 31 | io.Copy(ch, f) 32 | wg.Done() 33 | }() 34 | wg.Add(1) 35 | go func() { 36 | io.Copy(f, ch) 37 | wg.Done() 38 | }() 39 | 40 | wg.Wait() 41 | ch.Close() 42 | } 43 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package env86 2 | 3 | type Config struct { 4 | V86Config 5 | NoConsole bool 6 | EnableTTY bool 7 | ColdBoot bool 8 | SaveOnExit bool 9 | ExitPattern string 10 | EnableNetwork bool 11 | ChromeDP bool 12 | ConsoleAddr string 13 | } 14 | 15 | type V86Config struct { 16 | WasmPath string `json:"wasm_path,omitempty"` 17 | BIOS *ImageConfig `json:"bios,omitempty"` 18 | VGABIOS *ImageConfig `json:"vga_bios,omitempty"` 19 | MemorySize int `json:"memory_size,omitempty"` 20 | VGAMemorySize int `json:"vga_memory_size,omitempty"` 21 | InitialState *ImageConfig `json:"initial_state,omitempty"` 22 | NetworkRelayURL string `json:"network_relay_url,omitempty"` 23 | Filesystem *FilesystemConfig `json:"filesystem,omitempty"` 24 | Autostart bool `json:"autostart,omitempty"` 25 | BZImageInitrdFromFilesystem bool `json:"bzimage_initrd_from_filesystem,omitempty"` 26 | ScreenContainer string `json:"screen_container,omitempty"` 27 | Cmdline string `json:"cmdline,omitempty"` 28 | DisableKeyboard bool `json:"disable_keyboard,omitempty"` 29 | DisableMouse bool `json:"disable_mouse,omitempty"` 30 | HDA *ImageConfig `json:"hda,omitempty"` 31 | FDA *ImageConfig `json:"fda,omitempty"` 32 | CDROM *ImageConfig `json:"cdrom,omitempty"` 33 | BZImage *ImageConfig `json:"bzimage,omitempty"` 34 | Initrd *ImageConfig `json:"initrd,omitempty"` 35 | SerialContainer string `json:"serial_container,omitempty"` 36 | PreserveMAC bool `json:"preserve_mac_from_state_image,omitempty"` 37 | 38 | InitialStateParts int `json:"initial_state_parts,omitempty"` 39 | HasGuestService bool `json:"has_guest_service,omitempty"` 40 | } 41 | 42 | type FilesystemConfig struct { 43 | BaseURL string `json:"baseurl,omitempty"` 44 | BaseFS string `json:"basefs,omitempty"` 45 | } 46 | 47 | type ImageConfig struct { 48 | URL string `json:"url,omitempty"` 49 | Async bool `json:"async,omitempty"` 50 | Size int `json:"size,omitempty"` 51 | } 52 | -------------------------------------------------------------------------------- /console.go: -------------------------------------------------------------------------------- 1 | package env86 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "tractor.dev/toolkit-go/duplex/fn" 8 | ) 9 | 10 | type Console struct { 11 | vm *VM 12 | } 13 | 14 | // func (c *Console) Show() {} 15 | // func (c *Console) Hide() {} 16 | 17 | func (c *Console) Screenshot() ([]byte, error) { 18 | if c.vm.peer == nil { 19 | return nil, fmt.Errorf("not ready") 20 | } 21 | var data []byte 22 | _, err := c.vm.peer.Call(context.TODO(), "screenshot", nil, &data) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return data, nil 27 | } 28 | 29 | func (c *Console) SetScale(x, y float64) error { 30 | if c.vm.peer == nil { 31 | return fmt.Errorf("not ready") 32 | } 33 | _, err := c.vm.peer.Call(context.TODO(), "setScale", fn.Args{x, y}, nil) 34 | return err 35 | } 36 | 37 | // TODO: not working, but webview window might not allow this 38 | func (c *Console) SetFullscreen() error { 39 | if c.vm.peer == nil { 40 | return fmt.Errorf("not ready") 41 | } 42 | _, err := c.vm.peer.Call(context.TODO(), "setFullscreen", nil, nil) 43 | return err 44 | } 45 | 46 | // func (c *Console) EnableKeyboard() {} 47 | // func (c *Console) KeyboardEnabled() {} 48 | 49 | func (c *Console) SendKeyboard(text string) error { 50 | if c.vm.peer == nil { 51 | return fmt.Errorf("not ready") 52 | } 53 | _, err := c.vm.peer.Call(context.TODO(), "sendKeyboard", fn.Args{text}, nil) 54 | return err 55 | } 56 | 57 | // func (c *Console) EnableMouse() {} 58 | // func (c *Console) MouseEnabled() {} 59 | -------------------------------------------------------------------------------- /fsutil/copy.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path" 9 | 10 | "tractor.dev/toolkit-go/engine/fs" 11 | ) 12 | 13 | // CopyAll recursively copies the file, directory or symbolic link at src 14 | // to dst. The destination must not exist. Symbolic links are not 15 | // followed. 16 | // 17 | // If the copy fails half way through, the destination might be left 18 | // partially written. 19 | func CopyAll(fsys fs.FS, src, dst string) error { 20 | return CopyFS(fsys, src, fsys, dst) 21 | } 22 | 23 | func CopyFS(srcFS fs.FS, srcPath string, dstFS fs.FS, dstPath string) error { 24 | mfs, ok := dstFS.(fs.MutableFS) 25 | if !ok { 26 | return errors.New("not a mutable filesystem") 27 | } 28 | srcInfo, srcErr := fs.Stat(srcFS, srcPath) 29 | if srcErr != nil { 30 | return srcErr 31 | } 32 | dstInfo, dstErr := fs.Stat(dstFS, dstPath) 33 | if dstErr == nil && !dstInfo.IsDir() { 34 | return fmt.Errorf("will not overwrite %q", dstPath) 35 | } 36 | switch mode := srcInfo.Mode(); mode & fs.ModeType { 37 | // case os.ModeSymlink: 38 | // return copySymLink(src, dst) 39 | case os.ModeDir: 40 | return copyDir(srcFS, srcPath, mfs, dstPath, mode) 41 | case 0: 42 | return copyFile(srcFS, srcPath, mfs, dstPath, mode) 43 | default: 44 | return fmt.Errorf("cannot copy file with mode %v", mode) 45 | } 46 | } 47 | 48 | // func copySymLink(src, dst string) error { 49 | // target, err := os.Readlink(src) 50 | // if err != nil { 51 | // return err 52 | // } 53 | // return os.Symlink(target, dst) 54 | // } 55 | 56 | func copyFile(srcFS fs.FS, srcPath string, dstFS fs.MutableFS, dstPath string, mode fs.FileMode) error { 57 | srcf, err := srcFS.Open(srcPath) 58 | if err != nil { 59 | return err 60 | } 61 | defer srcf.Close() 62 | dstf, err := dstFS.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode.Perm()) 63 | if err != nil { 64 | return err 65 | } 66 | defer dstf.Close() 67 | // Make the actual permissions match the source permissions 68 | // even in the presence of umask. 69 | if err := dstFS.Chmod(dstPath, mode.Perm()); err != nil { 70 | return fmt.Errorf("chmod1: %w", err) 71 | } 72 | wdstf, ok := dstf.(io.Writer) 73 | if !ok { 74 | return fmt.Errorf("cannot copy %q to %q: dst not writable", srcPath, dstPath) 75 | } 76 | if _, err := io.Copy(wdstf, srcf); err != nil { 77 | return fmt.Errorf("cannot copy %q to %q: %v", srcPath, dstPath, err) 78 | } 79 | return nil 80 | } 81 | 82 | func copyDir(srcFS fs.FS, srcPath string, dstFS fs.MutableFS, dstPath string, mode fs.FileMode) error { 83 | srcf, err := srcFS.Open(srcPath) 84 | if err != nil { 85 | return err 86 | } 87 | defer srcf.Close() 88 | if mode&0500 == 0 { 89 | // The source directory doesn't have write permission, 90 | // so give the new directory write permission anyway 91 | // so that we have permission to create its contents. 92 | // We'll make the permissions match at the end. 93 | mode |= 0500 94 | } 95 | if err := dstFS.MkdirAll(dstPath, mode.Perm()); err != nil { 96 | return err 97 | } 98 | entries, err := fs.ReadDir(srcFS, srcPath) 99 | if err != nil { 100 | return fmt.Errorf("error reading directory %q: %v", srcPath, err) 101 | } 102 | for _, entry := range entries { 103 | if err := CopyFS(srcFS, path.Join(srcPath, entry.Name()), dstFS, path.Join(dstPath, entry.Name())); err != nil { 104 | return err 105 | } 106 | } 107 | if dstPath == "." { 108 | return nil 109 | } 110 | if err := dstFS.Chmod(dstPath, mode.Perm()); err != nil { 111 | return err 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /fsutil/path.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | 9 | "tractor.dev/toolkit-go/engine/fs" 10 | ) 11 | 12 | func RootFSDir(path string) string { 13 | if runtime.GOOS == "windows" { 14 | return filepath.VolumeName(path) 15 | } 16 | return "/" 17 | } 18 | 19 | func RootFS(path string) fs.FS { 20 | return os.DirFS(RootFSDir(path)) 21 | } 22 | 23 | /** 24 | * RootFSRelativePath returns the path relative to the root of the filesystem. 25 | * This is useful for checking if a file exists in a filesystem. 26 | * On Unix: 27 | * RootFSRelativePath("/foo/bar") => "foo/bar" 28 | * On Windows: 29 | * RootFSRelativePath("C:/foo/bar") => "foo/bar" 30 | */ 31 | func RootFSRelativePath(path string) string { 32 | if runtime.GOOS == "windows" { 33 | path = filepath.ToSlash(strings.TrimPrefix(path, filepath.VolumeName(path))) 34 | } 35 | return strings.TrimPrefix(path, "/") 36 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/progrium/env86 2 | 3 | go 1.23 4 | 5 | replace golang.org/x/sys => github.com/progrium/sys-wasm v0.0.0-20240620001524-43ddd9475fa9 6 | 7 | require ( 8 | github.com/chromedp/chromedp v0.11.1 9 | github.com/evanw/esbuild v0.24.0 10 | github.com/gorilla/websocket v1.5.3 11 | github.com/hugelgupf/p9 v0.3.0 12 | github.com/klauspost/compress v1.17.11 13 | github.com/progrium/go-netstack v0.0.0-20240720002214-37b2b8227b91 14 | golang.org/x/net v0.30.0 15 | golang.org/x/term v0.25.0 16 | tractor.dev/toolkit-go v0.0.0-20241010005851-214d91207d07 17 | tractor.dev/toolkit-go/desktop v0.0.0-20241125202453-a7a809374e73 18 | ) 19 | 20 | require ( 21 | github.com/apparentlymart/go-cidr v1.1.0 // indirect 22 | github.com/chromedp/cdproto v0.0.0-20241030022559-23c28aebe8cb // indirect 23 | github.com/chromedp/sysutil v1.1.0 // indirect 24 | github.com/ebitengine/purego v0.8.1 // indirect 25 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 26 | github.com/gobwas/httphead v0.1.0 // indirect 27 | github.com/gobwas/pool v0.2.1 // indirect 28 | github.com/gobwas/ws v1.4.0 // indirect 29 | github.com/google/btree v1.1.3 // indirect 30 | github.com/google/gopacket v1.1.19 // indirect 31 | github.com/inetaf/tcpproxy v0.0.0-20240214030015-3ce58045626c // indirect 32 | github.com/jchv/go-webview2 v0.0.0-20221223143126-dc24628cff85 // indirect 33 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect 34 | github.com/josharian/intern v1.0.0 // indirect 35 | github.com/mailru/easyjson v0.7.7 // indirect 36 | github.com/miekg/dns v1.1.62 // indirect 37 | github.com/mitchellh/mapstructure v1.5.0 // indirect 38 | github.com/pkg/errors v0.9.1 // indirect 39 | github.com/progrium/darwinkit v0.5.0 // indirect 40 | github.com/sirupsen/logrus v1.9.3 // indirect 41 | github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect 42 | github.com/x448/float16 v0.8.4 // indirect 43 | golang.org/x/mod v0.21.0 // indirect 44 | golang.org/x/sync v0.8.0 // indirect 45 | golang.org/x/sys v0.27.0 // indirect 46 | golang.org/x/time v0.7.0 // indirect 47 | golang.org/x/tools v0.26.0 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= 2 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= 3 | github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= 4 | github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= 5 | github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= 6 | github.com/chromedp/cdproto v0.0.0-20241030022559-23c28aebe8cb h1:yBPpAakATGLWZsVgYRcU9FopbOqzoazzbFaStQ9DCMc= 7 | github.com/chromedp/cdproto v0.0.0-20241030022559-23c28aebe8cb/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM= 8 | github.com/chromedp/chromedp v0.11.1 h1:Spca8egFqUlv+JDW+yIs+ijlHlJDPufgrfXPwtq6NMs= 9 | github.com/chromedp/chromedp v0.11.1/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8= 10 | github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= 11 | github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= 12 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 13 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 18 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 19 | github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= 20 | github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= 21 | github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE= 22 | github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 23 | github.com/evanw/esbuild v0.24.0 h1:GZ78naTLp7FKr+K7eNuM/SLs5maeiHYRPsTg6kmdsSE= 24 | github.com/evanw/esbuild v0.24.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= 25 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 26 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 27 | github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= 28 | github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 29 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 30 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 31 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 32 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 33 | github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= 34 | github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= 35 | github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 36 | github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 37 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 38 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 39 | github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 h1:CVuJwN34x4xM2aT4sIKhmeib40NeBPhRihNjQmpJsA4= 40 | github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= 41 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 42 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 43 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 44 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 45 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 46 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 47 | github.com/hugelgupf/p9 v0.3.0 h1:cjn7I237wQ8DN7OTXKRWieaSILW2M8H8hoXnFy5mwgk= 48 | github.com/hugelgupf/p9 v0.3.0/go.mod h1:QFmcCPNn66imQcu1wUqJ8sHKxYjs00Gq60QLjt9E+VI= 49 | github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= 50 | github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= 51 | github.com/hugelgupf/vmtest v0.0.0-20230810222836-f8c8e381617c h1:4A+BVHylCBQPxlW1NrUITDpRAHCeX6QSZHmzzFQqliU= 52 | github.com/hugelgupf/vmtest v0.0.0-20230810222836-f8c8e381617c/go.mod h1:d2FMzS0rIF+3Daufcw660EZfTJihdNPeEwBBJgO4Ap0= 53 | github.com/inetaf/tcpproxy v0.0.0-20240214030015-3ce58045626c h1:gYfYE403/nlrGNYj6BEOs9ucLCAGB9gstlSk92DttTg= 54 | github.com/inetaf/tcpproxy v0.0.0-20240214030015-3ce58045626c/go.mod h1:Di7LXRyUcnvAcLicFhtM9/MlZl/TNgRSDHORM2c6CMI= 55 | github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c h1:P/3mFnHCv1A/ej4m8pF5EB6FUt9qEL2Q9lfrcUNwCYs= 56 | github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4= 57 | github.com/jchv/go-webview2 v0.0.0-20221223143126-dc24628cff85 h1:t6lhRbwURcdWgp8OsJlq6sOfWMOXP21YuiCicjutHv4= 58 | github.com/jchv/go-webview2 v0.0.0-20221223143126-dc24628cff85/go.mod h1:/BNVc0Sw3Wj6Sz9uSxPwhCEUhhWs92hPde75K2YV24A= 59 | github.com/jchv/go-winloader v0.0.0-20200815041850-dec1ee9a7fd5/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= 60 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= 61 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= 62 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 63 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 64 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 65 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 66 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 67 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 68 | github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= 69 | github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= 70 | github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= 71 | github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= 72 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 73 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 74 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 75 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 76 | github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= 77 | github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= 78 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 79 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 80 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 81 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 82 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 83 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 84 | github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= 85 | github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= 86 | github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= 87 | github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 88 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 89 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 90 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 91 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 92 | github.com/progrium/darwinkit v0.5.0 h1:SwchcMbTOG1py3CQsINmGlsRmYKdlFrbnv3dE4aXA0s= 93 | github.com/progrium/darwinkit v0.5.0/go.mod h1:PxQhZuftnALLkCVaR8LaHtUOfoo4pm8qUDG+3C/sXNs= 94 | github.com/progrium/go-netstack v0.0.0-20240720002214-37b2b8227b91 h1:t3b5g0NdnPz4KlTgFPCxOFfG0qeNmgXDMzEr8j31Rzc= 95 | github.com/progrium/go-netstack v0.0.0-20240720002214-37b2b8227b91/go.mod h1:IWGVCFj8gqgUlsjm+dEKWNsDcBp0gqplYPLz6BdrJQ8= 96 | github.com/progrium/sys-wasm v0.0.0-20240620001524-43ddd9475fa9 h1:T+l3lQ8WHyED3Sc3JP9zBJMOE/CiSH/+QznWLEhk0qc= 97 | github.com/progrium/sys-wasm v0.0.0-20240620001524-43ddd9475fa9/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 98 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 99 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 100 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 101 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 102 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 103 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 104 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 105 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 106 | github.com/u-root/gobusybox/src v0.0.0-20230806212452-e9366a5b9fdc h1:udgfN9Qy573qgHWMEORFgy6YXNDiN/Fd5LlKdlp+/Mo= 107 | github.com/u-root/gobusybox/src v0.0.0-20230806212452-e9366a5b9fdc/go.mod h1:lYt+LVfZBBwDZ3+PHk4k/c/TnKOkjJXiJO73E32Mmpc= 108 | github.com/u-root/u-root v0.11.1-0.20230807200058-f87ad7ccb594 h1:1AIJqOtdEufYfGb3eRpdaqWONzBOpAwrg1fehbWg+Mg= 109 | github.com/u-root/u-root v0.11.1-0.20230807200058-f87ad7ccb594/go.mod h1:PQzg9XJGp6Y1hRmTUruSO7lR7kKR6FpoSObf5n5bTfE= 110 | github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= 111 | github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= 112 | github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= 113 | github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 114 | github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= 115 | github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= 116 | github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= 117 | github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 118 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 119 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 120 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 121 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 122 | golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= 123 | golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= 124 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 125 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 126 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 127 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 128 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 129 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 130 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 131 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 132 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 134 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 135 | golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= 136 | golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= 137 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 138 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 139 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 140 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 141 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 142 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 143 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 144 | google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= 145 | google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= 146 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 147 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 148 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 149 | src.elv.sh v0.16.0-rc1.0.20220116211855-fda62502ad7f h1:pjVeIo9Ba6K1Wy+rlwX91zT7A+xGEmxiNRBdN04gDTQ= 150 | src.elv.sh v0.16.0-rc1.0.20220116211855-fda62502ad7f/go.mod h1:kPbhv5+fBeUh85nET3wWhHGUaUQ64nZMJ8FwA5v5Olg= 151 | tractor.dev/toolkit-go v0.0.0-20241010005851-214d91207d07 h1:g+72jGAVthzXgyb94VMM4gDvI+NlMAEE1NsJ/bsUoE4= 152 | tractor.dev/toolkit-go v0.0.0-20241010005851-214d91207d07/go.mod h1:vI9Jf9tepHLrUqGQf7XZuRcQySNajWRKBjPD4+Ay72I= 153 | tractor.dev/toolkit-go/desktop v0.0.0-20241010005851-214d91207d07 h1:+dN1C2l2ZnAvHJ28Kj1Qfd/FcxOYrU1Fgds450dQF64= 154 | tractor.dev/toolkit-go/desktop v0.0.0-20241010005851-214d91207d07/go.mod h1:4VxzMoi8+OiKr3OrKOndJgtJQgwfVONDetqJE0dYDWg= 155 | tractor.dev/toolkit-go/desktop v0.0.0-20241118202920-e0a1d089f929 h1:FxJkOBnhO5rJonO7N898zb3PDApzdAKnt8MmVgR6hiU= 156 | tractor.dev/toolkit-go/desktop v0.0.0-20241118202920-e0a1d089f929/go.mod h1:4VxzMoi8+OiKr3OrKOndJgtJQgwfVONDetqJE0dYDWg= 157 | tractor.dev/toolkit-go/desktop v0.0.0-20241125202453-a7a809374e73 h1:fpIv99rCL32wzLORjHz1qGZHQTxEElt37CkDcDokYJc= 158 | tractor.dev/toolkit-go/desktop v0.0.0-20241125202453-a7a809374e73/go.mod h1:4VxzMoi8+OiKr3OrKOndJgtJQgwfVONDetqJE0dYDWg= 159 | -------------------------------------------------------------------------------- /guest.go: -------------------------------------------------------------------------------- 1 | package env86 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "net" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | 12 | "github.com/hugelgupf/p9/fsimpl/localfs" 13 | "github.com/hugelgupf/p9/p9" 14 | "golang.org/x/net/websocket" 15 | "tractor.dev/toolkit-go/duplex/codec" 16 | "tractor.dev/toolkit-go/duplex/mux" 17 | "tractor.dev/toolkit-go/duplex/talk" 18 | ) 19 | 20 | func (vm *VM) handleGuest(conn *websocket.Conn) { 21 | conn.PayloadType = websocket.BinaryFrame 22 | sess := mux.New(conn) 23 | defer sess.Close() 24 | 25 | vm.guest = &Guest{ 26 | vm: vm, 27 | peer: talk.NewPeer(sess, codec.CBORCodec{}), 28 | } 29 | vm.guest.cond = sync.NewCond(&vm.guest.mu) 30 | 31 | var v string 32 | _, err := vm.guest.peer.Call(context.TODO(), "vm.Version", nil, &v) 33 | if err != nil { 34 | log.Println("guest:", err) 35 | return 36 | } 37 | vm.guest.ver = v 38 | vm.guest.ready = true 39 | vm.guest.cond.Broadcast() 40 | vm.guest.peer.Respond() 41 | } 42 | 43 | type Guest struct { 44 | vm *VM 45 | peer *talk.Peer 46 | ver string 47 | ready bool 48 | mu sync.Mutex 49 | cond *sync.Cond 50 | } 51 | 52 | func (g *Guest) Ready() bool { 53 | g.mu.Lock() 54 | defer g.mu.Unlock() 55 | for !g.ready { 56 | g.cond.Wait() 57 | } 58 | return g.ready 59 | } 60 | 61 | func (g *Guest) Version() string { 62 | return g.ver 63 | } 64 | 65 | type GuestCmd struct { 66 | guestRunInput 67 | guest *Guest 68 | Stdin io.Reader 69 | Stdout io.Writer 70 | Stderr io.Writer 71 | } 72 | 73 | func (gc *GuestCmd) Run() (status int, err error) { 74 | // todo: change to start, get pid and return 75 | resp, err := gc.guest.peer.Call(context.Background(), "vm.Run", gc.guestRunInput, nil) 76 | if err != nil { 77 | return -1, err 78 | } 79 | defer resp.Channel.Close() 80 | if gc.Stdin != nil { 81 | go func() { 82 | io.Copy(resp.Channel, os.Stdin) 83 | }() 84 | } 85 | for { 86 | var out guestRunOutput 87 | err := resp.Receive(&out) 88 | if err != nil { 89 | return -1, err 90 | } 91 | if len(out.Stdout) > 0 { 92 | gc.Stdout.Write(out.Stdout) 93 | continue 94 | } 95 | if len(out.Stderr) > 0 { 96 | gc.Stderr.Write(out.Stderr) 97 | continue 98 | } 99 | if out.Status != nil { 100 | return *(out.Status), nil 101 | } 102 | } 103 | } 104 | 105 | type guestRunInput struct { 106 | Name string 107 | Args []string 108 | Dir string 109 | Env []string 110 | PTY bool 111 | } 112 | 113 | type guestRunOutput struct { 114 | Stdout []byte 115 | Stderr []byte 116 | Status *int 117 | } 118 | 119 | func (g *Guest) Command(name string, args ...string) *GuestCmd { 120 | return &GuestCmd{ 121 | guestRunInput: guestRunInput{ 122 | Name: name, 123 | Args: args, 124 | PTY: true, 125 | }, 126 | guest: g, 127 | } 128 | } 129 | 130 | func (g *Guest) ResetNetwork() error { 131 | _, err := g.peer.Call(context.Background(), "vm.ResetNetwork", nil, nil) 132 | return err 133 | } 134 | 135 | func (g *Guest) Mount(src, dst string) error { 136 | l, err := net.Listen("tcp", ":0") 137 | if err != nil { 138 | return err 139 | } 140 | _, port, _ := net.SplitHostPort(l.Addr().String()) 141 | path, err := filepath.Abs(src) 142 | if err != nil { 143 | return err 144 | } 145 | go func() { 146 | srv := p9.NewServer(localfs.Attacher(path)) 147 | if err := srv.Serve(l); err != nil { 148 | log.Fatal(err) 149 | } 150 | }() 151 | resp, err := g.peer.Call(context.Background(), "vm.Mount9P", dst, nil) 152 | if err != nil { 153 | return err 154 | } 155 | ch := resp.Channel 156 | conn, err := net.Dial("tcp", net.JoinHostPort("localhost", port)) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | var wg sync.WaitGroup 162 | 163 | wg.Add(1) 164 | go func() { 165 | io.Copy(ch, conn) 166 | wg.Done() 167 | }() 168 | wg.Add(1) 169 | go func() { 170 | io.Copy(conn, ch) 171 | wg.Done() 172 | }() 173 | 174 | wg.Wait() 175 | return ch.Close() 176 | } 177 | 178 | // FS(path) fs.FS 179 | // Dial(addr) Conn, error 180 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package env86 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/progrium/env86/assets" 11 | "github.com/progrium/env86/network" 12 | 13 | "golang.org/x/net/websocket" 14 | "tractor.dev/toolkit-go/duplex/codec" 15 | "tractor.dev/toolkit-go/duplex/fn" 16 | "tractor.dev/toolkit-go/duplex/mux" 17 | "tractor.dev/toolkit-go/duplex/rpc" 18 | "tractor.dev/toolkit-go/duplex/talk" 19 | ) 20 | 21 | func (vm *VM) startHTTP() { 22 | bundle, err := assets.BundleJavaScript() 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | mux := http.NewServeMux() 28 | mux.Handle("/net", network.Handler(vm.net)) 29 | mux.Handle("/guest", websocket.Handler(vm.handleGuest)) 30 | mux.Handle("/ctl", websocket.Handler(vm.handleControl)) 31 | mux.Handle("/env86.min.js", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 32 | w.Header().Add("content-type", "text/javascript") 33 | io.Copy(w, bytes.NewBuffer(bundle)) 34 | })) 35 | mux.Handle("/", http.FileServerFS(vm.fsys)) 36 | 37 | vm.srv = &http.Server{ 38 | Addr: vm.addr, 39 | Handler: mux, 40 | } 41 | if err := vm.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 42 | log.Println(err) 43 | } 44 | } 45 | 46 | func (vm *VM) handleControl(conn *websocket.Conn) { 47 | conn.PayloadType = websocket.BinaryFrame 48 | sess := mux.New(conn) 49 | defer sess.Close() 50 | 51 | vm.peer = talk.NewPeer(sess, codec.CBORCodec{}) 52 | vm.peer.Handle("loaded", fn.HandlerFrom(func() { 53 | if vm.loaded != nil { 54 | vm.loaded <- true 55 | } 56 | })) 57 | vm.peer.Handle("log", rpc.HandlerFunc(func(r rpc.Responder, c *rpc.Call) { 58 | var args []any 59 | c.Receive(&args) 60 | log.Println(args...) 61 | })) 62 | vm.peer.Handle("config", fn.HandlerFrom(func() Config { 63 | return vm.config 64 | })) 65 | vm.peer.Handle("tty", rpc.HandlerFunc(func(r rpc.Responder, c *rpc.Call) { 66 | c.Receive(nil) 67 | if !vm.config.EnableTTY { 68 | r.Return(fmt.Errorf("tty is not enabled")) 69 | return 70 | } 71 | ch, err := r.Continue(nil) 72 | if err != nil { 73 | log.Println(err) 74 | return 75 | } 76 | vm.handleTTY(ch) 77 | })) 78 | 79 | vm.peer.Respond() 80 | 81 | // websocket closed, so we assume window was closed 82 | vm.win = nil 83 | vm.Stop() 84 | } 85 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package env86 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/gzip" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "os" 11 | "path" 12 | "path/filepath" 13 | 14 | "github.com/progrium/env86/assets" 15 | "github.com/progrium/env86/fsutil" 16 | "github.com/progrium/env86/tarfs" 17 | 18 | "github.com/klauspost/compress/zstd" 19 | "tractor.dev/toolkit-go/engine/fs" 20 | "tractor.dev/toolkit-go/engine/fs/memfs" 21 | "tractor.dev/toolkit-go/engine/fs/osfs" 22 | ) 23 | 24 | type Image struct { 25 | FS fs.FS 26 | } 27 | 28 | func LoadImageReader(r io.Reader) (*Image, error) { 29 | imageUnzipped, err := gzip.NewReader(r) 30 | if err != nil { 31 | return nil, err 32 | } 33 | defer imageUnzipped.Close() 34 | 35 | return &Image{FS: tarfs.New(tar.NewReader(imageUnzipped))}, nil 36 | } 37 | 38 | func LoadImage(path string) (*Image, error) { 39 | imagePath, err := filepath.Abs(path) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | isDir, err := fs.IsDir(fsutil.RootFS(imagePath), fsutil.RootFSRelativePath(imagePath)) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | if isDir { 50 | return &Image{FS: osfs.Dir(imagePath)}, nil 51 | } 52 | 53 | imageFile, err := os.Open(imagePath) 54 | if err != nil { 55 | return nil, err 56 | } 57 | defer imageFile.Close() 58 | return LoadImageReader(imageFile) 59 | } 60 | 61 | func (i *Image) Config() (Config, error) { 62 | coldboot := !i.HasInitialState() 63 | if ok, _ := fs.Exists(i.FS, "image.json"); !ok { 64 | return Config{}, fmt.Errorf("no image.json found in image") 65 | } 66 | v86conf, err := i.v86Config() 67 | if err != nil { 68 | return Config{}, err 69 | } 70 | return Config{ 71 | V86Config: v86conf, 72 | ColdBoot: coldboot, 73 | }, nil 74 | } 75 | 76 | func (i *Image) v86Config() (V86Config, error) { 77 | b, err := fs.ReadFile(i.FS, "image.json") 78 | if err != nil { 79 | return V86Config{}, err 80 | } 81 | var v86conf V86Config 82 | if err := json.Unmarshal(b, &v86conf); err != nil { 83 | return V86Config{}, err 84 | } 85 | return v86conf, nil 86 | } 87 | 88 | func (i *Image) HasInitialState() bool { 89 | b, _ := fs.Exists(i.FS, "initial.state") 90 | return b || i.hasCompressedInitialState() 91 | } 92 | 93 | func (i *Image) hasCompressedInitialState() (b bool) { 94 | b, _ = fs.Exists(i.FS, "initial.state.zst") 95 | return 96 | } 97 | 98 | func (i *Image) InitialStateConfig() *ImageConfig { 99 | if !i.HasInitialState() { 100 | return nil 101 | } 102 | if i.hasCompressedInitialState() { 103 | fi, err := fs.Stat(i.FS, "initial.state.zst") 104 | if err != nil { 105 | panic(err) 106 | } 107 | return &ImageConfig{ 108 | URL: "/image/initial.state.zst", 109 | Size: int(fi.Size()), 110 | } 111 | } 112 | return &ImageConfig{ 113 | URL: "/image/initial.state", 114 | } 115 | } 116 | 117 | func (i *Image) SaveInitialState(r io.Reader) error { 118 | // TODO: write new tgz if loaded from tgz 119 | 120 | shouldCompress := i.hasCompressedInitialState() // || !i.HasInitialState() 121 | if shouldCompress { 122 | var buf bytes.Buffer 123 | // BUG: this implementation doesn't seem to be compatible with v86's decompressor! 124 | enc, err := zstd.NewWriter(&buf) 125 | if err != nil { 126 | return err 127 | } 128 | defer enc.Close() 129 | _, err = io.Copy(enc, r) 130 | if err != nil { 131 | return err 132 | } 133 | return fs.WriteFile(i.FS, "initial.state.zst", buf.Bytes(), 0644) 134 | } 135 | 136 | b, err := io.ReadAll(r) 137 | if err != nil { 138 | return err 139 | } 140 | return fs.WriteFile(i.FS, "initial.state", b, 0644) 141 | } 142 | 143 | func (i *Image) Prepare() (fs.FS, error) { 144 | fsys := memfs.New() 145 | conf, err := i.v86Config() 146 | if err != nil { 147 | return nil, err 148 | } 149 | 150 | if err := fsutil.CopyFS(i.FS, ".", fsys, "."); err != nil { 151 | return nil, err 152 | } 153 | 154 | if !i.HasInitialState() { 155 | conf.BIOS = &ImageConfig{URL: "./seabios.bin"} 156 | if err := fsutil.CopyFS(assets.Dir, "seabios.bin", fsys, "seabios.bin"); err != nil { 157 | return nil, err 158 | } 159 | 160 | conf.VGABIOS = &ImageConfig{URL: "./vgabios.bin"} 161 | if err := fsutil.CopyFS(assets.Dir, "vgabios.bin", fsys, "vgabios.bin"); err != nil { 162 | return nil, err 163 | } 164 | 165 | conf.Initrd = &ImageConfig{URL: "./initramfs.bin"} 166 | if err := fsutil.CopyFS(assets.Dir, "initramfs.bin", fsys, "initramfs.bin"); err != nil { 167 | return nil, err 168 | } 169 | 170 | conf.BZImage = &ImageConfig{URL: "./vmlinuz.bin"} 171 | if err := fsutil.CopyFS(assets.Dir, "vmlinuz.bin", fsys, "vmlinuz.bin"); err != nil { 172 | return nil, err 173 | } 174 | 175 | if err := writeConfig(fsys, "image.json", conf); err != nil { 176 | return nil, err 177 | } 178 | } 179 | 180 | if !i.HasInitialState() || i.hasCompressedInitialState() { 181 | return fsys, nil 182 | } 183 | 184 | fsys.MkdirAll("state", 0755) 185 | n, err := splitFile(fsys, "initial.state", "state", 10*1024*1024) 186 | if err != nil { 187 | return nil, err 188 | } 189 | if err := fsys.Remove("initial.state"); err != nil { 190 | return nil, err 191 | } 192 | 193 | conf.InitialStateParts = n 194 | if err := writeConfig(fsys, "image.json", conf); err != nil { 195 | return nil, err 196 | } 197 | 198 | return fsys, nil 199 | } 200 | 201 | func writeConfig(fsys fs.FS, filename string, cfg V86Config) error { 202 | data, err := json.MarshalIndent(cfg, "", " ") 203 | if err != nil { 204 | return err 205 | } 206 | return fs.WriteFile(fsys, filename, data, 0644) 207 | } 208 | 209 | func splitFile(fsys fs.FS, filename string, outputDir string, bytesPerFile int) (int, error) { 210 | f, ok := fsys.(interface { 211 | Create(name string) (fs.File, error) 212 | Chmod(name string, mode fs.FileMode) error 213 | }) 214 | if !ok { 215 | return 0, fs.ErrPermission 216 | } 217 | file, err := fsys.Open(filename) 218 | if err != nil { 219 | return 0, err 220 | } 221 | defer file.Close() 222 | 223 | baseName := path.Base(filename) 224 | 225 | buffer := make([]byte, bytesPerFile) 226 | part := 0 227 | for { 228 | bytesRead, rerr := file.Read(buffer) 229 | if bytesRead == 0 { 230 | break 231 | } 232 | 233 | outputFilename := fmt.Sprintf("%s.%d", baseName, part) 234 | outputFile, err := f.Create(path.Join(outputDir, outputFilename)) 235 | if err != nil { 236 | return 0, err 237 | } 238 | 239 | wf, ok := outputFile.(interface { 240 | Write(p []byte) (n int, err error) 241 | }) 242 | if !ok { 243 | return 0, fs.ErrPermission 244 | } 245 | 246 | _, err = wf.Write(buffer[:bytesRead]) 247 | if err != nil { 248 | return 0, err 249 | } 250 | outputFile.Close() 251 | if err := f.Chmod(path.Join(outputDir, outputFilename), 0644); err != nil { 252 | return 0, err 253 | } 254 | 255 | if rerr == io.EOF { 256 | break 257 | } 258 | part++ 259 | } 260 | return part, nil 261 | } 262 | -------------------------------------------------------------------------------- /misc.go: -------------------------------------------------------------------------------- 1 | package env86 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | func ListenAddr() string { 9 | ln, err := net.Listen("tcp4", ":0") 10 | if err != nil { 11 | panic(err) 12 | } 13 | ln.Close() 14 | return ln.Addr().String() 15 | } 16 | 17 | func LocalhostAddr(addr string) string { 18 | return strings.ReplaceAll(addr, "0.0.0.0", "localhost") 19 | } 20 | -------------------------------------------------------------------------------- /namespacefs/dirsfile.go: -------------------------------------------------------------------------------- 1 | package namespacefs 2 | 3 | import ( 4 | "io" 5 | "io/fs" 6 | ) 7 | 8 | type extraDirsFile struct { 9 | fs.File 10 | Dirs []fs.FileInfo 11 | off int 12 | entries []fs.DirEntry 13 | } 14 | 15 | func (f *extraDirsFile) ReadDir(c int) (dir []fs.DirEntry, err error) { 16 | if f.off == 0 { 17 | f.entries, err = f.File.(fs.ReadDirFile).ReadDir(c) 18 | if err != nil { 19 | return nil, err 20 | } 21 | for i := 0; i < len(f.Dirs); i++ { 22 | f.entries = append(f.entries, fs.FileInfoToDirEntry(f.Dirs[i])) 23 | } 24 | } 25 | entries := f.entries[f.off:] 26 | 27 | if c <= 0 { 28 | return entries, nil 29 | } 30 | 31 | if len(entries) == 0 { 32 | return nil, io.EOF 33 | } 34 | 35 | if c > len(entries) { 36 | c = len(entries) 37 | } 38 | 39 | defer func() { f.off += c }() 40 | return entries[:c], nil 41 | } 42 | -------------------------------------------------------------------------------- /namespacefs/fs.go: -------------------------------------------------------------------------------- 1 | package namespacefs 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path" 8 | "slices" 9 | "strings" 10 | "syscall" 11 | "time" 12 | 13 | "tractor.dev/toolkit-go/engine/fs" 14 | "tractor.dev/toolkit-go/engine/fs/fsutil" 15 | ) 16 | 17 | // move into fs 18 | func OpenFile(fsys fs.FS, name string, flag int, perm os.FileMode) (fs.File, error) { 19 | fsopenfile, ok := fsys.(interface { 20 | OpenFile(name string, flag int, perm os.FileMode) (fs.File, error) 21 | }) 22 | if !ok { 23 | fsopen, ok2 := fsys.(interface { 24 | Open(name string) (fs.File, error) 25 | }) 26 | if flag == os.O_RDONLY && perm == 0 && ok2 { 27 | return fsopen.Open(name) 28 | } 29 | return nil, fmt.Errorf("unable to openfile on fs") 30 | } 31 | return fsopenfile.OpenFile(name, flag, perm) 32 | } 33 | 34 | type binding struct { 35 | fsys fs.FS 36 | mountPoint string 37 | } 38 | 39 | // Work in progress FS to implement Plan9-style binding. 40 | // Right now works more like a mountable FS focusing on 41 | // merging mounts into dirs 42 | type FS struct { 43 | binds []binding 44 | } 45 | 46 | func New() *FS { 47 | return &FS{binds: make([]binding, 0, 1)} 48 | } 49 | 50 | func (host *FS) Mount(fsys fs.FS, dirPath string) error { 51 | dirPath = cleanPath(dirPath) 52 | 53 | // if found, _ := host.isPathInMount(dirPath); found { 54 | // return &fs.PathError{Op: "mount", Path: dirPath, Err: fs.ErrExist} 55 | // } 56 | 57 | host.binds = append([]binding{{fsys: fsys, mountPoint: dirPath}}, host.binds...) 58 | return nil 59 | } 60 | 61 | func (host *FS) Unmount(path string) error { 62 | path = cleanPath(path) 63 | for i, m := range host.binds { 64 | if path == m.mountPoint { 65 | host.binds = remove(host.binds, i) 66 | return nil 67 | } 68 | } 69 | 70 | return &fs.PathError{Op: "unmount", Path: path, Err: fs.ErrInvalid} 71 | } 72 | 73 | func remove(s []binding, i int) []binding { 74 | s[i] = s[len(s)-1] 75 | return s[:len(s)-1] 76 | } 77 | 78 | func (host *FS) isPathInMount(path string) (bool, *binding) { 79 | for i, m := range host.binds { 80 | if strings.HasPrefix(path, m.mountPoint) || m.mountPoint == "." { 81 | return true, &host.binds[i] 82 | } 83 | } 84 | return false, nil 85 | } 86 | 87 | func cleanPath(p string) string { 88 | return path.Clean(strings.TrimLeft(p, "/\\")) 89 | } 90 | 91 | func trimMountPoint(path string, mntPoint string) string { 92 | result := strings.TrimPrefix(path, mntPoint) 93 | // Separator is always / for io/fs instances 94 | result = strings.TrimPrefix(result, string("/")) 95 | 96 | if result == "" { 97 | return "." 98 | } else { 99 | return result 100 | } 101 | } 102 | 103 | func (host *FS) Chmod(name string, mode fs.FileMode) error { 104 | name = cleanPath(name) 105 | 106 | if found, mount := host.isPathInMount(name); found { 107 | chmodableFS, ok := mount.fsys.(interface { 108 | Chmod(name string, mode fs.FileMode) error 109 | }) 110 | if ok { 111 | return chmodableFS.Chmod(trimMountPoint(name, mount.mountPoint), mode) 112 | } 113 | } 114 | 115 | return &fs.PathError{Op: "chmod", Path: name, Err: errors.ErrUnsupported} 116 | } 117 | 118 | func (host *FS) Chown(name string, uid, gid int) error { 119 | name = cleanPath(name) 120 | 121 | if found, mount := host.isPathInMount(name); found { 122 | chownableFS, ok := mount.fsys.(interface { 123 | Chown(name string, uid, gid int) error 124 | }) 125 | if ok { 126 | return chownableFS.Chown(trimMountPoint(name, mount.mountPoint), uid, gid) 127 | } 128 | } 129 | 130 | return &fs.PathError{Op: "chown", Path: name, Err: errors.ErrUnsupported} 131 | } 132 | 133 | func (host *FS) Chtimes(name string, atime time.Time, mtime time.Time) error { 134 | name = cleanPath(name) 135 | 136 | if found, mount := host.isPathInMount(name); found { 137 | chtimesableFS, ok := mount.fsys.(interface { 138 | Chtimes(name string, atime time.Time, mtime time.Time) error 139 | }) 140 | if ok { 141 | return chtimesableFS.Chtimes(trimMountPoint(name, mount.mountPoint), atime, mtime) 142 | } 143 | } 144 | 145 | return &fs.PathError{Op: "chtimes", Path: name, Err: errors.ErrUnsupported} 146 | } 147 | 148 | func (host *FS) Create(name string) (fs.File, error) { 149 | name = cleanPath(name) 150 | 151 | if found, mount := host.isPathInMount(name); found { 152 | createableFS, ok := mount.fsys.(interface { 153 | Create(name string) (fs.File, error) 154 | }) 155 | if ok { 156 | return createableFS.Create(trimMountPoint(name, mount.mountPoint)) 157 | } 158 | } 159 | 160 | return nil, &fs.PathError{Op: "create", Path: name, Err: errors.ErrUnsupported} 161 | } 162 | 163 | func (host *FS) Mkdir(name string, perm fs.FileMode) error { 164 | name = cleanPath(name) 165 | 166 | if found, mount := host.isPathInMount(name); found { 167 | mkdirableFS, ok := mount.fsys.(interface { 168 | Mkdir(name string, perm fs.FileMode) error 169 | }) 170 | if ok { 171 | return mkdirableFS.Mkdir(trimMountPoint(name, mount.mountPoint), perm) 172 | } 173 | } 174 | 175 | return &fs.PathError{Op: "mkdir", Path: name, Err: errors.ErrUnsupported} 176 | } 177 | 178 | func (host *FS) MkdirAll(name string, perm fs.FileMode) error { 179 | name = cleanPath(name) 180 | 181 | if found, mount := host.isPathInMount(name); found { 182 | mkdirableFS, ok := mount.fsys.(interface { 183 | MkdirAll(path string, perm fs.FileMode) error 184 | }) 185 | if ok { 186 | return mkdirableFS.MkdirAll(trimMountPoint(name, mount.mountPoint), perm) 187 | } 188 | } 189 | 190 | return &fs.PathError{Op: "mkdirAll", Path: name, Err: errors.ErrUnsupported} 191 | } 192 | 193 | func (host *FS) Open(name string) (fs.File, error) { 194 | return host.OpenFile(name, os.O_RDONLY, 0) 195 | } 196 | 197 | func (host *FS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) { 198 | name = cleanPath(name) 199 | if found, mount := host.isPathInMount(name); found { 200 | f, err := OpenFile(mount.fsys, trimMountPoint(name, mount.mountPoint), flag, perm) 201 | if err != nil { 202 | return nil, err 203 | } 204 | var mounts []fs.FileInfo 205 | for b, m := range host.mountsAtPath(name) { 206 | if b.mountPoint == mount.mountPoint { 207 | continue 208 | } 209 | mf, err := m.Open(".") 210 | if err != nil { 211 | return nil, err 212 | } 213 | s, err := mf.Stat() 214 | if err != nil { 215 | return nil, err 216 | } 217 | mounts = append(mounts, &renamedFileInfo{FileInfo: s, name: path.Base(b.mountPoint)}) 218 | } 219 | if len(mounts) == 0 { 220 | return f, nil 221 | } 222 | return &extraDirsFile{File: f, Dirs: mounts}, nil 223 | } 224 | return nil, &fs.PathError{Op: "open", Path: name, Err: errors.ErrUnsupported} 225 | } 226 | 227 | type renamedFileInfo struct { 228 | fs.FileInfo 229 | name string 230 | } 231 | 232 | func (fi *renamedFileInfo) Name() string { 233 | return fi.name 234 | } 235 | 236 | func (host *FS) mountsAtPath(name string) (b map[binding]fs.FS) { 237 | b = make(map[binding]fs.FS) 238 | for _, m := range host.binds { 239 | if path.Dir(m.mountPoint) == name { 240 | b[m] = m.fsys 241 | } 242 | } 243 | return 244 | } 245 | 246 | type removableFS interface { 247 | fs.FS 248 | Remove(name string) error 249 | } 250 | 251 | // func (host *FS) Remove(name string) error { 252 | // name = cleanPath(name) 253 | 254 | // if name == mount.mountPoint { 255 | // return &fs.PathError{Op: "remove", Path: name, Err: syscall.EBUSY} 256 | // } 257 | 258 | // if removableFS, ok := fsys.(removableFS); ok { 259 | // return removableFS.Remove(trimMountPoint(name, mount.mountPoint)) 260 | // } else { 261 | // return &fs.PathError{Op: "remove", Path: name, Err: errors.ErrUnsupported} 262 | // } 263 | // } 264 | 265 | // func (host *FS) RemoveAll(path string) error { 266 | // path = cleanPath(path) 267 | 268 | // if found, mount := host.isPathInMount(path); found { 269 | // if path == mount.mountPoint { 270 | // return &fs.PathError{Op: "removeAll", Path: path, Err: syscall.EBUSY} 271 | // } 272 | // } else { 273 | // fsys = host.MutableFS 274 | // // check if path contains any mountpoints, and call a custom removeAll 275 | // // if it does. 276 | // var mntPoints []string 277 | // for _, m := range host.binds { 278 | // if path == "." || strings.HasPrefix(m.mountPoint, path) { 279 | // mntPoints = append(mntPoints, m.mountPoint) 280 | // } 281 | // } 282 | 283 | // if len(mntPoints) > 0 { 284 | // return removeAll(host, path, mntPoints) 285 | // } 286 | // } 287 | 288 | // rmAllFS, ok := fsys.(interface { 289 | // RemoveAll(path string) error 290 | // }) 291 | // if !ok { 292 | // if rmFS, ok := fsys.(removableFS); ok { 293 | // return removeAll(rmFS, path, nil) 294 | // } else { 295 | // return &fs.PathError{Op: "removeAll", Path: path, Err: errors.ErrUnsupported} 296 | // } 297 | // } 298 | // return rmAllFS.RemoveAll(trimMountPoint(path, mount.mountPoint)) 299 | // } 300 | 301 | // RemoveAll removes path and any children it contains. It removes everything 302 | // it can but returns the first error it encounters. If the path does not exist, 303 | // RemoveAll returns nil (no error). If there is an error, it will be of type *PathError. 304 | // Additionally, this function errors if attempting to remove a mountpoint. 305 | func removeAll(fsys removableFS, filePath string, mntPoints []string) error { 306 | filePath = path.Clean(filePath) 307 | 308 | if exists, err := fsutil.Exists(fsys, filePath); !exists || err != nil { 309 | return err 310 | } 311 | 312 | return rmRecurse(fsys, filePath, mntPoints) 313 | 314 | } 315 | 316 | func rmRecurse(fsys removableFS, filePath string, mntPoints []string) error { 317 | if mntPoints != nil && slices.Contains(mntPoints, filePath) { 318 | return &fs.PathError{Op: "remove", Path: filePath, Err: syscall.EBUSY} 319 | } 320 | 321 | isdir, dirErr := fsutil.IsDir(fsys, filePath) 322 | if dirErr != nil { 323 | return dirErr 324 | } 325 | 326 | if isdir { 327 | if entries, err := fs.ReadDir(fsys, filePath); err == nil { 328 | for _, entry := range entries { 329 | entryPath := path.Join(filePath, entry.Name()) 330 | 331 | if err := rmRecurse(fsys, entryPath, mntPoints); err != nil { 332 | return err 333 | } 334 | 335 | if err := fsys.Remove(entryPath); err != nil { 336 | return err 337 | } 338 | } 339 | } else { 340 | return err 341 | } 342 | } 343 | 344 | return fsys.Remove(filePath) 345 | } 346 | 347 | // func (host *FS) Rename(oldname, newname string) error { 348 | // oldname = cleanPath(oldname) 349 | // newname = cleanPath(newname) 350 | 351 | // // error if both paths aren't in the same filesystem 352 | // if found, oldMount := host.isPathInMount(oldname); found { 353 | // if found, newMount := host.isPathInMount(newname); found { 354 | // if oldMount != newMount { 355 | // return &fs.PathError{Op: "rename", Path: oldname + " -> " + newname, Err: syscall.EXDEV} 356 | // } 357 | 358 | // if oldname == oldMount.mountPoint || newname == newMount.mountPoint { 359 | // return &fs.PathError{Op: "rename", Path: oldname + " -> " + newname, Err: syscall.EBUSY} 360 | // } 361 | 362 | // fsys = newMount.fsys 363 | // mount.mountPoint = newMount.mountPoint 364 | // } else { 365 | // return &fs.PathError{Op: "rename", Path: oldname + " -> " + newname, Err: syscall.EXDEV} 366 | // } 367 | // } else { 368 | // if found, _ := host.isPathInMount(newname); found { 369 | // return &fs.PathError{Op: "rename", Path: oldname + " -> " + newname, Err: syscall.EXDEV} 370 | // } 371 | 372 | // fsys = host.MutableFS 373 | // } 374 | 375 | // renameableFS, ok := fsys.(interface { 376 | // Rename(oldname, newname string) error 377 | // }) 378 | // if !ok { 379 | // return &fs.PathError{Op: "rename", Path: oldname + " -> " + newname, Err: errors.ErrUnsupported} 380 | // } 381 | // return renameableFS.Rename(trimMountPoint(oldname, mount.mountPoint), trimMountPoint(newname, mount.mountPoint)) 382 | // } 383 | -------------------------------------------------------------------------------- /network/handler.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "encoding/binary" 5 | "log" 6 | "net" 7 | "net/http" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/gorilla/websocket" 13 | "github.com/progrium/go-netstack/vnet" 14 | ) 15 | 16 | func Handler(vn *vnet.VirtualNetwork) http.Handler { 17 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | if vn == nil { 19 | http.Error(w, "network not available", http.StatusNotFound) 20 | return 21 | } 22 | if !websocket.IsWebSocketUpgrade(r) { 23 | http.Error(w, "expecting websocket upgrade", http.StatusBadRequest) 24 | return 25 | } 26 | 27 | ws, err := upgrader.Upgrade(w, r, nil) 28 | if err != nil { 29 | http.Error(w, err.Error(), http.StatusInternalServerError) 30 | log.Println(err) 31 | return 32 | } 33 | defer ws.Close() 34 | 35 | if err := vn.AcceptQemu(r.Context(), &qemuAdapter{Conn: ws}); err != nil { 36 | if strings.Contains(err.Error(), "websocket: close") { 37 | return 38 | } 39 | log.Println(err) 40 | return 41 | } 42 | }) 43 | } 44 | 45 | var upgrader = websocket.Upgrader{ 46 | ReadBufferSize: 1024, 47 | WriteBufferSize: 1024, 48 | CheckOrigin: func(r *http.Request) bool { 49 | return true 50 | }, 51 | } 52 | 53 | type qemuAdapter struct { 54 | *websocket.Conn 55 | mu sync.Mutex 56 | readBuffer []byte 57 | writeBuffer []byte 58 | readOffset int 59 | } 60 | 61 | func (q *qemuAdapter) Read(p []byte) (n int, err error) { 62 | if len(q.readBuffer) == 0 { 63 | _, message, err := q.ReadMessage() 64 | if err != nil { 65 | return 0, err 66 | } 67 | length := uint32(len(message)) 68 | lengthPrefix := make([]byte, 4) 69 | binary.BigEndian.PutUint32(lengthPrefix, length) 70 | q.readBuffer = append(lengthPrefix, message...) 71 | q.readOffset = 0 72 | } 73 | 74 | n = copy(p, q.readBuffer[q.readOffset:]) 75 | q.readOffset += n 76 | if q.readOffset >= len(q.readBuffer) { 77 | q.readBuffer = nil 78 | } 79 | return n, nil 80 | } 81 | 82 | func (q *qemuAdapter) Write(p []byte) (int, error) { 83 | q.mu.Lock() 84 | defer q.mu.Unlock() 85 | 86 | q.writeBuffer = append(q.writeBuffer, p...) 87 | 88 | if len(q.writeBuffer) < 4 { 89 | return len(p), nil 90 | } 91 | 92 | length := binary.BigEndian.Uint32(q.writeBuffer[:4]) 93 | if len(q.writeBuffer) < int(length)+4 { 94 | return len(p), nil 95 | } 96 | 97 | err := q.WriteMessage(websocket.BinaryMessage, q.writeBuffer[4:4+length]) 98 | if err != nil { 99 | return 0, err 100 | } 101 | 102 | q.writeBuffer = q.writeBuffer[4+length:] 103 | return len(p), nil 104 | } 105 | 106 | func (c *qemuAdapter) LocalAddr() net.Addr { 107 | return &net.UnixAddr{} 108 | } 109 | 110 | func (c *qemuAdapter) RemoteAddr() net.Addr { 111 | return &net.UnixAddr{} 112 | } 113 | 114 | func (c *qemuAdapter) SetDeadline(t time.Time) error { 115 | return nil 116 | } 117 | func (c *qemuAdapter) SetReadDeadline(t time.Time) error { 118 | return nil 119 | } 120 | func (c *qemuAdapter) SetWriteDeadline(t time.Time) error { 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /scripts/Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/386 i386/alpine:3.18.6 AS kernel 2 | ENV KERNEL=lts 3 | RUN apk add mkinitfs --no-cache --allow-untrusted --repository https://dl-cdn.alpinelinux.org/alpine/edge/main/ 4 | RUN if [ "$KERNEL" == "lts" ]; then \ 5 | apk add linux-lts \ 6 | linux-firmware-none \ 7 | linux-firmware-sb16; \ 8 | else \ 9 | apk add linux-$KERNEL; \ 10 | fi 11 | RUN mkinitfs -F "ata base ide scsi virtio ext4 9p" $(cat /usr/share/kernel/$KERNEL/kernel.release) 12 | 13 | FROM alpine:3.18 AS v86 14 | WORKDIR /v86 15 | RUN mkdir -p /out 16 | RUN apk add --update curl clang make openjdk8-jre-base npm python3 git openssh 17 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && PATH="${HOME}/.cargo/bin:${PATH}" rustup target add wasm32-unknown-unknown 18 | RUN git clone --depth=1 https://github.com/copy/v86.git . 19 | RUN PATH="${HOME}/.cargo/bin:${PATH}" make all && rm -rf closure-compiler gen lib src .cargo cargo.toml Makefile 20 | RUN cp ./build/libv86.js /out 21 | RUN cp ./build/v86.wasm /out 22 | RUN cp ./bios/seabios.bin /out 23 | RUN cp ./bios/vgabios.bin /out 24 | 25 | FROM alpine:3.20 26 | WORKDIR /app 27 | RUN apk add -u build-base docker pkgconf webkit2gtk-dev gtk+3.0-dev libayatana-appindicator-dev 28 | RUN apk upgrade --no-cache --available \ 29 | && apk add --no-cache \ 30 | chromium-swiftshader \ 31 | ttf-freefont \ 32 | font-noto-emoji \ 33 | && apk add --no-cache \ 34 | --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \ 35 | font-wqy-zenhei go 36 | ENV CHROME_BIN=/usr/bin/chromium-browser \ 37 | CHROME_PATH=/usr/lib/chromium/ 38 | ENV CHROMIUM_FLAGS="--disable-software-rasterizer --disable-dev-shm-usage" 39 | COPY . . 40 | RUN mv ./scripts/local.conf /etc/fonts/local.conf 41 | RUN go mod tidy 42 | COPY --from=kernel /boot/vmlinuz-lts /app/assets/vmlinuz.bin 43 | COPY --from=kernel /boot/initramfs-lts /app/assets/initramfs.bin 44 | COPY --from=v86 /out/* /app/assets/ 45 | RUN make guest 46 | RUN go build -o /bin/env86 ./cmd/env86 47 | 48 | -------------------------------------------------------------------------------- /scripts/Dockerfile.example: -------------------------------------------------------------------------------- 1 | FROM i386/alpine:3.18.6 2 | 3 | ENV KERNEL=lts 4 | ENV HOSTNAME=localhost 5 | ENV PASSWORD='root' 6 | 7 | RUN apk add openrc \ 8 | alpine-base \ 9 | agetty \ 10 | alpine-conf 11 | 12 | # Install mkinitfs from edge (todo: remove this when 3.19+ has worked properly with 9pfs) 13 | RUN apk add mkinitfs --no-cache --allow-untrusted --repository https://dl-cdn.alpinelinux.org/alpine/edge/main/ 14 | 15 | RUN if [ "$KERNEL" == "lts" ]; then \ 16 | apk add linux-lts \ 17 | linux-firmware-none \ 18 | linux-firmware-sb16; \ 19 | else \ 20 | apk add linux-$KERNEL; \ 21 | fi 22 | 23 | # Adding networking.sh script (works only on lts kernel yet) 24 | RUN if [ "$KERNEL" == "lts" ]; then \ 25 | echo -e "echo '127.0.0.1 localhost' >> /etc/hosts && rmmod ne2k-pci && modprobe ne2k-pci\nhwclock -s\nsetup-interfaces -a -r" > /root/networking.sh && \ 26 | chmod +x /root/networking.sh; \ 27 | fi 28 | 29 | RUN sed -i 's/getty 38400 tty1/agetty --autologin root tty1 linux/' /etc/inittab 30 | RUN echo 'ttyS0::once:/sbin/agetty --autologin root -s ttyS0 115200 vt100' >> /etc/inittab 31 | RUN echo "root:$PASSWORD" | chpasswd 32 | 33 | # https://wiki.alpinelinux.org/wiki/Alpine_Linux_in_a_chroot#Preparing_init_services 34 | RUN for i in devfs dmesg mdev hwdrivers; do rc-update add $i sysinit; done 35 | RUN for i in hwclock modules sysctl hostname bootmisc; do rc-update add $i boot; done 36 | RUN rc-update add killprocs shutdown 37 | 38 | # Generate initramfs with 9p modules 39 | RUN mkinitfs -F "ata base ide scsi virtio ext4 9p" $(cat /usr/share/kernel/$KERNEL/kernel.release) 40 | -------------------------------------------------------------------------------- /scripts/Dockerfile.kernel: -------------------------------------------------------------------------------- 1 | FROM i386/alpine:3.18.6 2 | 3 | ENV KERNEL=lts 4 | 5 | # Install mkinitfs from edge (todo: remove this when 3.19+ has worked properly with 9pfs) 6 | RUN apk add mkinitfs --no-cache --allow-untrusted --repository https://dl-cdn.alpinelinux.org/alpine/edge/main/ 7 | 8 | RUN if [ "$KERNEL" == "lts" ]; then \ 9 | apk add linux-lts \ 10 | linux-firmware-none \ 11 | linux-firmware-sb16; \ 12 | else \ 13 | apk add linux-$KERNEL; \ 14 | fi 15 | 16 | # Generate initramfs with 9p modules 17 | RUN mkinitfs -F "ata base ide scsi virtio ext4 9p" $(cat /usr/share/kernel/$KERNEL/kernel.release) 18 | 19 | CMD cp /boot/vmlinuz-lts /dst/vmlinuz.bin && cp /boot/initramfs-lts /dst/initramfs.bin && chmod 644 /dst/vmlinuz.bin /dst/initramfs.bin -------------------------------------------------------------------------------- /scripts/Dockerfile.v86: -------------------------------------------------------------------------------- 1 | FROM alpine:3.18 2 | WORKDIR /v86 3 | RUN mkdir -p /out 4 | 5 | # build v86 6 | RUN apk add --update curl clang make openjdk8-jre-base npm python3 git openssh 7 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && PATH="${HOME}/.cargo/bin:${PATH}" rustup target add wasm32-unknown-unknown 8 | RUN git clone --depth=1 https://github.com/copy/v86.git . 9 | RUN PATH="${HOME}/.cargo/bin:${PATH}" make all && rm -rf closure-compiler gen lib src .cargo cargo.toml Makefile 10 | 11 | RUN cp ./build/libv86.js /out 12 | RUN cp ./build/v86.wasm /out 13 | RUN cp ./bios/seabios.bin /out 14 | RUN cp ./bios/vgabios.bin /out 15 | 16 | CMD cp -r /out/* /dst -------------------------------------------------------------------------------- /scripts/local.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | sans-serif 7 | 8 | Main sans-serif font name goes here 9 | Noto Color Emoji 10 | Noto Emoji 11 | 12 | 13 | 14 | 15 | serif 16 | 17 | Main serif font name goes here 18 | Noto Color Emoji 19 | Noto Emoji 20 | 21 | 22 | 23 | 24 | monospace 25 | 26 | Main monospace font name goes here 27 | Noto Color Emoji 28 | Noto Emoji 29 | 30 | 31 | -------------------------------------------------------------------------------- /tarfs/file.go: -------------------------------------------------------------------------------- 1 | package tarfs 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | "syscall" 11 | ) 12 | 13 | type File struct { 14 | h *tar.Header 15 | data *bytes.Reader 16 | closed bool 17 | fs *FS 18 | } 19 | 20 | func (f *File) Close() error { 21 | if f.closed { 22 | return os.ErrClosed 23 | } 24 | 25 | f.closed = true 26 | f.h = nil 27 | f.data = nil 28 | f.fs = nil 29 | 30 | return nil 31 | } 32 | 33 | func (f *File) Read(p []byte) (n int, err error) { 34 | if f.closed { 35 | return 0, os.ErrClosed 36 | } 37 | 38 | if f.h.Typeflag == tar.TypeDir { 39 | return 0, syscall.EISDIR 40 | } 41 | 42 | return f.data.Read(p) 43 | } 44 | 45 | func (f *File) ReadAt(p []byte, off int64) (n int, err error) { 46 | if f.closed { 47 | return 0, os.ErrClosed 48 | } 49 | 50 | if f.h.Typeflag == tar.TypeDir { 51 | return 0, syscall.EISDIR 52 | } 53 | 54 | return f.data.ReadAt(p, off) 55 | } 56 | 57 | func (f *File) Seek(offset int64, whence int) (int64, error) { 58 | if f.closed { 59 | return 0, os.ErrClosed 60 | } 61 | 62 | if f.h.Typeflag == tar.TypeDir { 63 | return 0, syscall.EISDIR 64 | } 65 | 66 | return f.data.Seek(offset, whence) 67 | } 68 | 69 | func (f *File) Write(p []byte) (n int, err error) { return 0, syscall.EROFS } 70 | 71 | func (f *File) WriteAt(p []byte, off int64) (n int, err error) { return 0, syscall.EROFS } 72 | 73 | func (f *File) Name() string { 74 | return filepath.Join(splitpath(f.h.Name)) 75 | } 76 | 77 | func (f *File) getDirectoryNames() ([]string, error) { 78 | d, ok := f.fs.files[f.Name()] 79 | if !ok { 80 | return nil, &os.PathError{Op: "readdir", Path: f.Name(), Err: syscall.ENOENT} 81 | } 82 | 83 | var names []string 84 | for n := range d { 85 | names = append(names, n) 86 | } 87 | sort.Strings(names) 88 | 89 | return names, nil 90 | } 91 | 92 | func (f *File) ReadDir(count int) ([]fs.DirEntry, error) { 93 | if f.closed { 94 | return nil, os.ErrClosed 95 | } 96 | 97 | if !f.h.FileInfo().IsDir() { 98 | return nil, syscall.ENOTDIR 99 | } 100 | 101 | names, err := f.getDirectoryNames() 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | d := f.fs.files[f.Name()] 107 | var fi []fs.DirEntry 108 | for _, n := range names { 109 | if n == "" { 110 | continue 111 | } 112 | 113 | f := d[n] 114 | fi = append(fi, &dirEntry{f.h.FileInfo()}) 115 | if count > 0 && len(fi) >= count { 116 | break 117 | } 118 | } 119 | 120 | return fi, nil 121 | } 122 | 123 | func (f *File) Readdirnames(n int) ([]string, error) { 124 | fi, err := f.ReadDir(n) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | var names []string 130 | for _, f := range fi { 131 | names = append(names, f.Name()) 132 | } 133 | 134 | return names, nil 135 | } 136 | 137 | func (f *File) Stat() (os.FileInfo, error) { return f.h.FileInfo(), nil } 138 | 139 | func (f *File) Sync() error { return nil } 140 | 141 | func (f *File) Truncate(size int64) error { return syscall.EROFS } 142 | 143 | func (f *File) WriteString(s string) (ret int, err error) { return 0, syscall.EROFS } 144 | 145 | type dirEntry struct { 146 | fs.FileInfo 147 | } 148 | 149 | func (d *dirEntry) Info() (fs.FileInfo, error) { 150 | return d.FileInfo, nil 151 | } 152 | 153 | func (d *dirEntry) Type() fs.FileMode { 154 | return d.FileInfo.Mode() 155 | } 156 | -------------------------------------------------------------------------------- /tarfs/fs.go: -------------------------------------------------------------------------------- 1 | // package tarfs implements a read-only in-memory representation of a tar archive 2 | package tarfs 3 | 4 | import ( 5 | "archive/tar" 6 | "bytes" 7 | "io" 8 | "io/fs" 9 | "os" 10 | "path/filepath" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | var Separator = string(filepath.Separator) 16 | 17 | type FS struct { 18 | files map[string]map[string]*File 19 | } 20 | 21 | func splitpath(name string) (dir, file string) { 22 | name = filepath.ToSlash(name) 23 | if len(name) == 0 || name[0] != '/' { 24 | name = "/" + name 25 | } 26 | name = filepath.Clean(name) 27 | dir, file = filepath.Split(name) 28 | dir = filepath.Clean(dir) 29 | return 30 | } 31 | 32 | func New(t *tar.Reader) *FS { 33 | fs := &FS{files: make(map[string]map[string]*File)} 34 | for { 35 | hdr, err := t.Next() 36 | if err == io.EOF { 37 | break 38 | } 39 | if err != nil { 40 | return nil 41 | } 42 | 43 | d, f := splitpath(hdr.Name) 44 | if _, ok := fs.files[d]; !ok { 45 | fs.files[d] = make(map[string]*File) 46 | } 47 | 48 | var buf bytes.Buffer 49 | size, err := buf.ReadFrom(t) 50 | if err != nil { 51 | panic("tarfs: reading from tar:" + err.Error()) 52 | } 53 | 54 | if size != hdr.Size { 55 | panic("tarfs: size mismatch") 56 | } 57 | 58 | file := &File{ 59 | h: hdr, 60 | data: bytes.NewReader(buf.Bytes()), 61 | fs: fs, 62 | } 63 | fs.files[d][f] = file 64 | 65 | } 66 | 67 | if fs.files[Separator] == nil { 68 | fs.files[Separator] = make(map[string]*File) 69 | } 70 | // Add a pseudoroot 71 | fs.files[Separator][""] = &File{ 72 | h: &tar.Header{ 73 | Name: Separator, 74 | Typeflag: tar.TypeDir, 75 | Size: 0, 76 | }, 77 | data: bytes.NewReader(nil), 78 | fs: fs, 79 | } 80 | 81 | return fs 82 | } 83 | 84 | func (fs *FS) Open(name string) (fs.File, error) { 85 | d, f := splitpath(name) 86 | if _, ok := fs.files[d]; !ok { 87 | return nil, &os.PathError{Op: "open", Path: name, Err: syscall.ENOENT} 88 | } 89 | 90 | file, ok := fs.files[d][f] 91 | if !ok { 92 | return nil, &os.PathError{Op: "open", Path: name, Err: syscall.ENOENT} 93 | } 94 | 95 | nf := *file 96 | br := *nf.data 97 | nf.data = &br 98 | 99 | return &nf, nil 100 | } 101 | 102 | func (fs *FS) Name() string { return "tarfs" } 103 | 104 | func (fs *FS) Create(name string) (fs.File, error) { return nil, syscall.EROFS } 105 | 106 | func (fs *FS) Mkdir(name string, perm os.FileMode) error { return syscall.EROFS } 107 | 108 | func (fs *FS) MkdirAll(path string, perm os.FileMode) error { return syscall.EROFS } 109 | 110 | func (fs *FS) OpenFile(name string, flag int, perm os.FileMode) (fs.File, error) { 111 | if flag != os.O_RDONLY { 112 | return nil, &os.PathError{Op: "open", Path: name, Err: syscall.EPERM} 113 | } 114 | 115 | return fs.Open(name) 116 | } 117 | 118 | func (fs *FS) Remove(name string) error { return syscall.EROFS } 119 | 120 | func (fs *FS) RemoveAll(path string) error { return syscall.EROFS } 121 | 122 | func (fs *FS) Rename(oldname string, newname string) error { return syscall.EROFS } 123 | 124 | func (fs *FS) Stat(name string) (fs.FileInfo, error) { 125 | d, f := splitpath(name) 126 | if _, ok := fs.files[d]; !ok { 127 | return nil, &os.PathError{Op: "stat", Path: name, Err: syscall.ENOENT} 128 | } 129 | 130 | file, ok := fs.files[d][f] 131 | if !ok { 132 | return nil, &os.PathError{Op: "stat", Path: name, Err: syscall.ENOENT} 133 | } 134 | 135 | return file.h.FileInfo(), nil 136 | } 137 | 138 | func (fs *FS) Chmod(name string, mode fs.FileMode) error { return syscall.EROFS } 139 | 140 | func (fs *FS) Chown(name string, uid, gid int) error { return syscall.EROFS } 141 | 142 | func (fs *FS) Chtimes(name string, atime time.Time, mtime time.Time) error { return syscall.EROFS } 143 | -------------------------------------------------------------------------------- /tty.go: -------------------------------------------------------------------------------- 1 | package env86 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "log" 7 | "os" 8 | "sync" 9 | "time" 10 | 11 | "golang.org/x/term" 12 | ) 13 | 14 | func (vm *VM) handleTTY(ch io.ReadWriteCloser) { 15 | if vm.serialPipe != nil { 16 | vm.joinSerialPipe(ch) 17 | return 18 | } 19 | 20 | oldstate, err := term.MakeRaw(int(os.Stdin.Fd())) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | var outReader io.Reader = ch 26 | if vm.config.ExitPattern != "" { 27 | r, w := io.Pipe() 28 | outReader = io.TeeReader(ch, w) 29 | go func() { 30 | exitPattern := []byte(vm.config.ExitPattern) 31 | buffer := make([]byte, 1024) 32 | temp := bytes.NewBuffer(nil) 33 | for { 34 | n, err := r.Read(buffer) 35 | if n > 0 { 36 | temp.Write(buffer[:n]) 37 | if bytes.Contains(temp.Bytes(), exitPattern) { 38 | <-time.After(200 * time.Millisecond) // give moment for stdout to flush 39 | term.Restore(int(os.Stdin.Fd()), oldstate) 40 | ch.Close() 41 | vm.Exit("Exit pattern detected") 42 | return 43 | } 44 | // Keep only the last len(pattern)-1 bytes in temp to handle patterns spanning chunks 45 | if temp.Len() > len(exitPattern) { 46 | temp.Next(temp.Len() - len(exitPattern) + 1) 47 | } 48 | } 49 | if err != nil { 50 | if err != io.EOF { 51 | log.Println(err) 52 | } 53 | break 54 | } 55 | } 56 | }() 57 | } 58 | go io.Copy(os.Stdout, outReader) 59 | 60 | // send newline to trigger new prompt 61 | // since most saves will be at prompt 62 | if vm.image.HasInitialState() { 63 | io.WriteString(ch, "\n") 64 | } 65 | 66 | buffer := make([]byte, 1024) 67 | for { 68 | n, err := os.Stdin.Read(buffer) 69 | if err != nil { 70 | term.Restore(int(os.Stdin.Fd()), oldstate) 71 | log.Fatal("Error reading from stdin:", err) 72 | } 73 | 74 | for i := 0; i < n; i++ { 75 | // Check for Ctrl-D (ASCII 4) 76 | if buffer[i] == 4 { 77 | term.Restore(int(os.Stdin.Fd()), oldstate) 78 | ch.Close() 79 | vm.Exit("Ctrl-D detected") 80 | return 81 | } 82 | } 83 | 84 | _, err = ch.Write(buffer[:n]) 85 | if err != nil { 86 | log.Println(err) 87 | } 88 | } 89 | } 90 | 91 | func (vm *VM) joinSerialPipe(ch io.ReadWriteCloser) { 92 | var wg sync.WaitGroup 93 | 94 | wg.Add(1) 95 | go func() { 96 | io.Copy(ch, vm.serialPipe) 97 | wg.Done() 98 | }() 99 | wg.Add(1) 100 | go func() { 101 | io.Copy(vm.serialPipe, ch) 102 | wg.Done() 103 | }() 104 | 105 | wg.Wait() 106 | } 107 | -------------------------------------------------------------------------------- /vm.go: -------------------------------------------------------------------------------- 1 | package env86 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "net/http" 11 | 12 | "github.com/progrium/env86/assets" 13 | "github.com/progrium/env86/namespacefs" 14 | 15 | "github.com/chromedp/chromedp" 16 | "github.com/progrium/go-netstack/vnet" 17 | "tractor.dev/toolkit-go/desktop" 18 | "tractor.dev/toolkit-go/desktop/app" 19 | "tractor.dev/toolkit-go/desktop/window" 20 | "tractor.dev/toolkit-go/duplex/fn" 21 | "tractor.dev/toolkit-go/duplex/talk" 22 | "tractor.dev/toolkit-go/engine/fs" 23 | ) 24 | 25 | type VM struct { 26 | image *Image 27 | config Config 28 | console *Console 29 | guest *Guest 30 | addr string 31 | fsys fs.FS 32 | net *vnet.VirtualNetwork 33 | srv *http.Server 34 | win *window.Window 35 | app *app.App 36 | peer *talk.Peer 37 | serialPipe net.Conn 38 | loaded chan bool 39 | stopped chan bool 40 | cdpCancel func() 41 | } 42 | 43 | func New(image *Image, config Config) (*VM, error) { 44 | fsys := namespacefs.New() 45 | if err := fsys.Mount(assets.Dir, "/"); err != nil { 46 | return nil, err 47 | } 48 | if err := fsys.Mount(image.FS, "image"); err != nil { 49 | return nil, err 50 | } 51 | 52 | if config.WasmPath == "" { 53 | config.WasmPath = "/v86.wasm" 54 | } 55 | if config.Filesystem == nil { 56 | config.Filesystem = &FilesystemConfig{} 57 | } 58 | if config.Filesystem.BaseFS == "" { 59 | config.Filesystem.BaseFS = "/image/fs.json" 60 | config.Filesystem.BaseURL = "/image/fs/" 61 | } 62 | if config.MemorySize == 0 { 63 | config.MemorySize = 512 * 1024 * 1024 // 512MB 64 | } 65 | if config.VGAMemorySize == 0 { 66 | config.VGAMemorySize = 8 * 1024 * 1024 // 8MB 67 | } 68 | // should this be done in Image.Config()? 69 | if config.InitialState == nil && image.HasInitialState() && !config.ColdBoot { 70 | config.InitialState = image.InitialStateConfig() 71 | } 72 | if config.InitialState == nil && !image.HasInitialState() { 73 | config.ColdBoot = true 74 | } 75 | if config.ColdBoot { 76 | config.BIOS = &ImageConfig{ 77 | URL: "/seabios.bin", 78 | } 79 | config.VGABIOS = &ImageConfig{ 80 | URL: "/vgabios.bin", 81 | } 82 | config.BZImage = &ImageConfig{ 83 | URL: "/vmlinuz.bin", 84 | } 85 | config.Initrd = &ImageConfig{ 86 | URL: "/initramfs.bin", 87 | } 88 | } 89 | config.Autostart = true 90 | 91 | if config.ConsoleAddr == "" { 92 | config.ConsoleAddr = ListenAddr() 93 | } 94 | 95 | vm := &VM{ 96 | image: image, 97 | config: config, 98 | fsys: fsys, 99 | addr: config.ConsoleAddr, 100 | stopped: make(chan bool), 101 | } 102 | vm.console = &Console{vm: vm} 103 | 104 | if config.EnableNetwork { 105 | var err error 106 | vm.net, err = vnet.New(&vnet.Configuration{ 107 | Debug: false, 108 | MTU: 1500, 109 | Subnet: "192.168.127.0/24", 110 | GatewayIP: "192.168.127.1", 111 | GatewayMacAddress: "5a:94:ef:e4:0c:dd", 112 | GatewayVirtualIPs: []string{"192.168.127.253"}, 113 | }) 114 | if err != nil { 115 | return nil, err 116 | } 117 | vm.config.NetworkRelayURL = fmt.Sprintf("ws://%s/net", LocalhostAddr(vm.addr)) 118 | } 119 | 120 | return vm, nil 121 | } 122 | 123 | func (vm *VM) Exit(reason string) { 124 | // not sure if this should be on this API 125 | // because its made for the CLI and writes to stdout 126 | if vm.config.SaveOnExit { 127 | fmt.Printf("\r\n%s. Saving...\n", reason) 128 | err := vm.SaveInitialState() 129 | if err != nil { 130 | log.Fatal(err) 131 | } 132 | } else { 133 | fmt.Printf("\r\n%s. Exiting...\n", reason) 134 | } 135 | vm.Stop() 136 | } 137 | 138 | func (vm *VM) Start() error { 139 | if vm.app == nil && !vm.config.ChromeDP { 140 | launched := make(chan bool) 141 | vm.app = app.Run(app.Options{ 142 | Accessory: true, 143 | Agent: true, 144 | }, func() { 145 | launched <- true 146 | }) 147 | <-launched 148 | } 149 | 150 | if vm.srv == nil { 151 | go vm.startHTTP() 152 | } 153 | 154 | vm.loaded = make(chan bool) 155 | url := fmt.Sprintf("http://%s/console.html", LocalhostAddr(vm.addr)) 156 | if vm.config.ChromeDP { 157 | ctx, cancel := chromedp.NewContext(context.Background()) 158 | vm.cdpCancel = cancel 159 | go func() { 160 | if err := chromedp.Run(ctx, chromedp.Navigate(url)); err != nil { 161 | log.Println(err) 162 | } 163 | }() 164 | } else { 165 | desktop.Dispatch(func() { 166 | vm.win = window.New(window.Options{ 167 | Center: true, 168 | Hidden: vm.config.NoConsole, 169 | Size: window.Size{ 170 | Width: 1004, 171 | Height: 785, 172 | }, 173 | Resizable: true, 174 | URL: url, 175 | }) 176 | vm.win.Reload() 177 | }) 178 | } 179 | <-vm.loaded 180 | vm.loaded = nil 181 | return nil 182 | } 183 | 184 | func (vm *VM) Close() error { 185 | if err := vm.Stop(); err != nil { 186 | return err 187 | } 188 | if vm.srv != nil { 189 | if err := vm.srv.Close(); err != nil { 190 | return err 191 | } 192 | } 193 | return nil 194 | } 195 | 196 | func (vm *VM) Run() error { 197 | if err := vm.Start(); err != nil { 198 | return err 199 | } 200 | return vm.Wait() 201 | } 202 | 203 | func (vm *VM) Stop() error { 204 | if vm.win != nil { 205 | desktop.Dispatch(func() { 206 | vm.win.Unload() 207 | vm.win = nil 208 | }) 209 | } 210 | if vm.cdpCancel != nil { 211 | vm.cdpCancel() 212 | vm.cdpCancel = nil 213 | } 214 | select { 215 | case vm.stopped <- true: 216 | default: 217 | } 218 | return nil 219 | } 220 | 221 | func (vm *VM) Restart() error { 222 | if err := vm.Stop(); err != nil { 223 | return err 224 | } 225 | return vm.Start() 226 | } 227 | 228 | func (vm *VM) Wait() error { 229 | // todo: make work correctly for multiple Waits 230 | <-vm.stopped 231 | return nil 232 | } 233 | 234 | func (vm *VM) Pause() (err error) { 235 | if vm.peer == nil { 236 | return 237 | } 238 | _, err = vm.peer.Call(context.TODO(), "pause", nil, nil) 239 | return 240 | } 241 | 242 | func (vm *VM) Unpause() (err error) { 243 | if vm.peer == nil { 244 | return 245 | } 246 | _, err = vm.peer.Call(context.TODO(), "unpause", nil, nil) 247 | return 248 | } 249 | 250 | // Save saves the state of the VM 251 | func (vm *VM) Save() (io.Reader, error) { 252 | if vm.peer == nil { 253 | return nil, fmt.Errorf("not ready") 254 | } 255 | var b []byte 256 | _, err := vm.peer.Call(context.TODO(), "save", nil, &b) 257 | if err != nil { 258 | return nil, err 259 | } 260 | return bytes.NewBuffer(b), nil 261 | } 262 | 263 | func (vm *VM) SaveInitialState() error { 264 | r, err := vm.Save() 265 | if err != nil { 266 | return err 267 | } 268 | return vm.image.SaveInitialState(r) 269 | } 270 | 271 | // Restore loads state into the VM 272 | func (vm *VM) Restore(state io.Reader) error { 273 | if vm.peer == nil { 274 | return fmt.Errorf("not ready") 275 | } 276 | b, err := io.ReadAll(state) 277 | if err != nil { 278 | return err 279 | } 280 | _, err = vm.peer.Call(context.TODO(), "restore", fn.Args{b}, nil) 281 | return err 282 | } 283 | 284 | // Guest is an API to interact with the guest service 285 | func (vm *VM) Guest() *Guest { 286 | return vm.guest 287 | } 288 | 289 | // Console is an API to interact with the screen, keyboard, and mouse 290 | func (vm *VM) Console() *Console { 291 | return vm.console 292 | } 293 | 294 | // SerialPipe returns an io.ReadWriter to the serial/COM1 port 295 | func (vm *VM) SerialPipe() (io.ReadWriter, error) { 296 | a, b := net.Pipe() 297 | vm.serialPipe = b 298 | return a, nil 299 | } 300 | 301 | // NetworkPipe returns an io.ReadWriter of Ethernet packets to the virtual NIC 302 | func (vm *VM) NetworkPipe() (io.ReadWriter, error) { 303 | return nil, fmt.Errorf("TODO") 304 | } 305 | 306 | func (vm *VM) MacAddress() (string, error) { 307 | if vm.peer == nil { 308 | return "", fmt.Errorf("not ready") 309 | } 310 | var mac string 311 | _, err := vm.peer.Call(context.TODO(), "mac", nil, &mac) 312 | if err != nil { 313 | return "", err 314 | } 315 | return mac, nil 316 | } 317 | 318 | func (vm *VM) Network() *vnet.VirtualNetwork { 319 | return vm.net 320 | } 321 | --------------------------------------------------------------------------------