commit 57f821b30ba9a074eb023518f8d636c09afd52e5 Author: Abhinav Sarkar Date: Thu Oct 10 19:12:48 2024 +0530 Initial commit diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..6cf8f30 --- /dev/null +++ b/.envrc @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +use nix +watch_file shell.nix +watch_file nix/sources.json +watch_file haskell-wasm-repl.cabal diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23ae7e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist-newstyle +result +.gcroots/ +web/*.wasm diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f477ff --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Haskell WASM REPL Template + +A template project to create [REPLs](https://en.wikipedia.org/wiki/REPL) with [Haskell](https://haskell.org) that run in the browser using [WASM](https://en.wikipedia.org/wiki/WebAssembly). + +## Running + +The project is orchestrated using [Nix]. After you enter the Nix shell by running `nix-shell`, you can the following commands: + +- `clean`: Cleans the Cabal build directory. +- `compile`: Build the project using Cabal. +- `run`: Builds and run the project using Cabal. +- `build`: Builds the project using Nix. Builds dynamically linked executables. +- `build-static`: Builds the project using Nix. Builds statically linked executables. Requires either to be run on Linux, or to have a Nix [Linux builder](https://nixcademy.com/posts/macos-linux-builder/) configured. +- `build-docker`: Runs the previous step and creates a [Docker](https://en.wikipedia.org/wiki/Docker_(software)) image with the statically linked executables using Nix. Requires Docker to be set up on the build machine. The image will also contain the contents of the `data` directory. +- `build-wasm`: Runs the previous step and converts the Docker image into a WASM binary `out.wasm`, and copies it into the `web` directory. +- `serve`: Serves the contents of the `web` directory over HTTP locally. + +The `build-static`, `build-docker`, and `build-wasm` commands take a long time to finish when run for the first time, maybe even hours. The built artifacts are cached so subsequent builds finish in minutes. + +## Deploying + +You can copy the contents of the `web` directory to your server and serve them as a static website. For the browser to load the WASM file, the server needs to send the following headers: + +```http +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +It is recommended to configure your server to serve pre-compressed assets, and to pre-compress the `out.wasm` file using Brotli or Gzip. This reduces the browser load time of the website by more than half. + +It is also recommended to configure cache-control headers for the assets in your server so that browsers cache them. If you change the `out.wasm` file frequently, it is recommended to figure out a cache-busting policy for it. + +## Developing + +This project uses the following tools/components: + +- [Nix] is used to build statically linked executables and Docker images. +- [Niv](https://github.com/nmattia/niv) is used to pin Nix dependencies. +- [container2wasm](https://github.com/ktock/container2wasm) is used to convert Docker images to WASM binaries. The [WASI example](https://github.com/ktock/container2wasm/tree/main/examples/wasi-browser/htdocs) from container2wasm forms the basis of the `web` directory. +- [Xterm.js](https://github.com/xtermjs/xterm.js/) is used to create the web terminal in browsers. +- [Xterm-pty](https://github.com/mame/xterm-pty/) is used to run the WASM binary on Xterm. +- [Xterm-fit-addon](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit) is used to make the web terminal fullscreen. + +## Using + +For unknown reasons, the WASM REPL does not work on Safari. Please use it in Firefox or Chrome. + +[Nix]: https://nixos.org diff --git a/app/Main.hs b/app/Main.hs new file mode 100644 index 0000000..eedfffb --- /dev/null +++ b/app/Main.hs @@ -0,0 +1,22 @@ +module Main where + +import Control.Monad (forever) +import Data.Char (isSpace) +import Data.List (dropWhileEnd) +import System.IO (BufferMode (..), hFlush, hSetBuffering, stdin, stdout) +import System.IO.Error (catchIOError, isEOFError) + +main :: IO () +main = do + hSetBuffering stdin LineBuffering + hSetBuffering stdout LineBuffering + loop `catchIOError` \e -> if isEOFError e then return () else ioError e + where + loop = forever $ do + putStrLn "Please enter your name: " + putStr "> " + hFlush stdout + name <- dropWhileEnd isSpace . dropWhile isSpace <$> getLine + if null name + then loop + else putStrLn $ "Hello, " <> name <> "!\n" diff --git a/data/a.txt b/data/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..3152579 --- /dev/null +++ b/default.nix @@ -0,0 +1,11 @@ +{ system ? builtins.currentSystem, static ? false }: +let + pkgs = import ./nix/nixpkgs.nix { inherit system static; }; + static-deps = pkgs.callPackage ./nix/haskell-wasm-repl/static-deps.nix { }; +in if !static then + pkgs.callPackage ./nix/haskell-wasm-repl { static = false; } +else + pkgs.callPackage ./nix/haskell-wasm-repl { + static = true; + inherit (static-deps) gmp6 libffi ncurses zlib; + } diff --git a/docker.nix b/docker.nix new file mode 100644 index 0000000..fead790 --- /dev/null +++ b/docker.nix @@ -0,0 +1,12 @@ +{ system ? builtins.currentSystem }: +let + pkgs = import ./nix/nixpkgs.nix { + system = (builtins.head (builtins.split "-" system)) + "-linux"; + }; + haskell-wasm-repl = import ./. { + inherit system; + static = true; + }; +in pkgs.callPackage ./nix/haskell-wasm-repl/docker.nix { + inherit haskell-wasm-repl; +} diff --git a/haskell-wasm-repl.cabal b/haskell-wasm-repl.cabal new file mode 100644 index 0000000..c4d12e1 --- /dev/null +++ b/haskell-wasm-repl.cabal @@ -0,0 +1,22 @@ +cabal-version: 3.4 +name: haskell-wasm-repl +version: 0.1.0.0 +synopsis: + A template for creating Haskell REPL executables that run in browser using WASM. + +homepage: https://github.com/abhin4v/haskell-wasm-repl +license: NONE +author: Abhinav Sarkar +maintainer: abhinav@abhinavsarkar.net +category: Web +build-type: Simple + +common warnings + ghc-options: -Wall + +executable haskell-wasm-repl + import: warnings + main-is: Main.hs + build-depends: base ^>=4.18.2.1 + hs-source-dirs: app + default-language: GHC2021 diff --git a/nix/haskell-wasm-repl/default.nix b/nix/haskell-wasm-repl/default.nix new file mode 100644 index 0000000..ae95d23 --- /dev/null +++ b/nix/haskell-wasm-repl/default.nix @@ -0,0 +1,21 @@ +{ static, lib, haskell, haskellPackages, gmp6, libffi, ncurses, zlib }: +let hlib = haskell.lib.compose; +in lib.pipe (haskellPackages.callCabal2nix "haskell-wasm-repl" + (lib.cleanSource (lib.sourceFilesBySuffices ../../. [ ".hs" ".cabal" ])) { }) +([ + hlib.dontHaddock + (hlib.overrideCabal (old: { enableParallelBuilding = true; })) +] ++ lib.optionals static [ + hlib.justStaticExecutables + hlib.disableSharedLibraries + hlib.enableDeadCodeElimination + (hlib.appendConfigureFlags [ + "-O2" + "--ghc-option=-fPIC" + "--ghc-option=-optl=-static" + "--extra-lib-dirs=${gmp6}/lib" + "--extra-lib-dirs=${libffi}/lib" + "--extra-lib-dirs=${ncurses}/lib" + "--extra-lib-dirs=${zlib}/lib" + ]) +]) diff --git a/nix/haskell-wasm-repl/docker.nix b/nix/haskell-wasm-repl/docker.nix new file mode 100644 index 0000000..46580a4 --- /dev/null +++ b/nix/haskell-wasm-repl/docker.nix @@ -0,0 +1,13 @@ +{ haskell-wasm-repl, dockerTools }: +dockerTools.buildImage { + name = "haskell-wasm-repl"; + copyToRoot = [ ../../data ]; + runAsRoot = '' + mkdir -p /data + mv *.txt /data/ + ''; + config = { + Cmd = [ "${haskell-wasm-repl}/bin/haskell-wasm-repl" ]; + WorkingDir = "/data"; + }; +} diff --git a/nix/haskell-wasm-repl/static-deps.nix b/nix/haskell-wasm-repl/static-deps.nix new file mode 100644 index 0000000..77e5728 --- /dev/null +++ b/nix/haskell-wasm-repl/static-deps.nix @@ -0,0 +1,9 @@ +{ gmp6, libffi, ncurses, zlib }: { + gmp6 = gmp6.override { withStatic = true; }; + libffi = libffi.overrideAttrs (old: { + dontDisableStatic = true; + doCheck = false; + }); + ncurses = ncurses.override { enableStatic = true; }; + zlib = zlib.static; +} diff --git a/nix/haskell-wasm-repl/wasm.nix b/nix/haskell-wasm-repl/wasm.nix new file mode 100644 index 0000000..0f7fc17 --- /dev/null +++ b/nix/haskell-wasm-repl/wasm.nix @@ -0,0 +1,19 @@ +{ stdenv, docker, container2wasm, haskell-wasm-repl-docker }: + +stdenv.mkDerivation { + pname = "haskell-wasm-repl-wasm"; + version = "1.0"; + buildInputs = [ docker container2wasm ]; + unpackPhase = "true"; + + installPhase = '' + set -euo pipefail; + export HOME=$TMP + mkdir -p "$out/bin" + DOCKER_IMAGE_LABEL=`docker image load -i "${haskell-wasm-repl-docker}" | awk '{print $3}'`; + c2w "$DOCKER_IMAGE_LABEL" "$out/bin/out.wasm"; + docker image rm "$DOCKER_IMAGE_LABEL"; + ''; + + fixupPhase = "true"; +} diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix new file mode 100644 index 0000000..5186aec --- /dev/null +++ b/nix/nixpkgs.nix @@ -0,0 +1,46 @@ +{ system ? builtins.currentSystem, static ? false }: +let + sources = import ./sources.nix; + nixpkgs = import sources.nixpkgs { + system = if !static then + system + else + (builtins.head (builtins.split "-" system)) + "-linux"; + overlays = [ + (final: prev: { + haskellPackages = if !static then + prev.haskellPackages + else + prev.haskellPackages.override { + ghc = prev.haskellPackages.ghc.override { + enableRelocatedStaticLibs = true; + enableShared = false; + enableDwarf = false; + }; + buildHaskellPackages = + prev.haskellPackages.buildHaskellPackages.override + (old: { ghc = final.haskellPackages.ghc; }); + }; + }) + (final: prev: { + haskell = if !static then + prev.haskell + else + prev.haskell // { + packageOverrides = + prev.lib.composeExtensions prev.haskell.packageOverrides + (hfinal: hprev: { + mkDerivation = args: + hprev.mkDerivation (args // { + doCheck = false; + doHaddock = false; + enableLibraryProfiling = false; + enableExecutableProfiling = false; + }); + }); + }; + }) + ]; + config = { }; + }; +in if static then nixpkgs.pkgsMusl else nixpkgs diff --git a/nix/sources.json b/nix/sources.json new file mode 100644 index 0000000..a57afd5 --- /dev/null +++ b/nix/sources.json @@ -0,0 +1,14 @@ +{ + "nixpkgs": { + "branch": "nixpkgs-unstable", + "description": "Nix Packages collection & NixOS", + "homepage": "", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "280db3decab4cbeb22a4599bd472229ab74d25e1", + "sha256": "17n9wji64l7d16s8r100ypwlxkmwrypll4q3wkkfjswbilxkqjr6", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/280db3decab4cbeb22a4599bd472229ab74d25e1.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + } +} diff --git a/nix/sources.nix b/nix/sources.nix new file mode 100644 index 0000000..fe3dadf --- /dev/null +++ b/nix/sources.nix @@ -0,0 +1,198 @@ +# This file has been generated by Niv. + +let + + # + # The fetchers. fetch_ fetches specs of type . + # + + fetch_file = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true then + builtins_fetchurl { inherit (spec) url sha256; name = name'; } + else + pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; + + fetch_tarball = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true then + builtins_fetchTarball { name = name'; inherit (spec) url sha256; } + else + pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; + + fetch_git = name: spec: + let + ref = + spec.ref or ( + if spec ? branch then "refs/heads/${spec.branch}" else + if spec ? tag then "refs/tags/${spec.tag}" else + abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!" + ); + submodules = spec.submodules or false; + submoduleArg = + let + nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0; + emptyArgWithWarning = + if submodules + then + builtins.trace + ( + "The niv input \"${name}\" uses submodules " + + "but your nix's (${builtins.nixVersion}) builtins.fetchGit " + + "does not support them" + ) + { } + else { }; + in + if nixSupportsSubmodules + then { inherit submodules; } + else emptyArgWithWarning; + in + builtins.fetchGit + ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg); + + fetch_local = spec: spec.path; + + fetch_builtin-tarball = name: throw + ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=tarball -a builtin=true''; + + fetch_builtin-url = name: throw + ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=file -a builtin=true''; + + # + # Various helpers + # + + # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 + sanitizeName = name: + ( + concatMapStrings (s: if builtins.isList s then "-" else s) + ( + builtins.split "[^[:alnum:]+._?=-]+" + ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) + ) + ); + + # The set of packages used when specs are fetched using non-builtins. + mkPkgs = sources: system: + let + sourcesNixpkgs = + import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; + hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; + hasThisAsNixpkgsPath = == ./.; + in + if builtins.hasAttr "nixpkgs" sources + then sourcesNixpkgs + else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then + import { } + else + abort + '' + Please specify either (through -I or NIX_PATH=nixpkgs=...) or + add a package called "nixpkgs" to your sources.json. + ''; + + # The actual fetching function. + fetch = pkgs: name: spec: + + if ! builtins.hasAttr "type" spec then + abort "ERROR: niv spec ${name} does not have a 'type' attribute" + else if spec.type == "file" then fetch_file pkgs name spec + else if spec.type == "tarball" then fetch_tarball pkgs name spec + else if spec.type == "git" then fetch_git name spec + else if spec.type == "local" then fetch_local spec + else if spec.type == "builtin-tarball" then fetch_builtin-tarball name + else if spec.type == "builtin-url" then fetch_builtin-url name + else + abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; + + # If the environment variable NIV_OVERRIDE_${name} is set, then use + # the path directly as opposed to the fetched source. + replace = name: drv: + let + saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; + ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; + in + if ersatz == "" then drv else + # this turns the string into an actual Nix path (for both absolute and + # relative paths) + if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; + + # Ports of functions for older nix versions + + # a Nix version of mapAttrs if the built-in doesn't exist + mapAttrs = builtins.mapAttrs or ( + f: set: with builtins; + listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) + ); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 + range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 + stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 + stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); + concatMapStrings = f: list: concatStrings (map f list); + concatStrings = builtins.concatStringsSep ""; + + # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 + optionalAttrs = cond: as: if cond then as else { }; + + # fetchTarball version that is compatible between all the versions of Nix + builtins_fetchTarball = { url, name ? null, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchTarball; + in + if lessThan nixVersion "1.12" then + fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) + else + fetchTarball attrs; + + # fetchurl version that is compatible between all the versions of Nix + builtins_fetchurl = { url, name ? null, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchurl; + in + if lessThan nixVersion "1.12" then + fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) + else + fetchurl attrs; + + # Create the final "sources" from the config + mkSources = config: + mapAttrs + ( + name: spec: + if builtins.hasAttr "outPath" spec + then + abort + "The values in sources.json should not have an 'outPath' attribute" + else + spec // { outPath = replace name (fetch config.pkgs name spec); } + ) + config.sources; + + # The "config" used by the fetchers + mkConfig = + { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null + , sources ? if sourcesFile == null then { } else builtins.fromJSON (builtins.readFile sourcesFile) + , system ? builtins.currentSystem + , pkgs ? mkPkgs sources system + }: rec { + # The sources, i.e. the attribute set of spec name to spec + inherit sources; + + # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers + inherit pkgs; + }; + +in +mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/server.py b/server.py new file mode 100644 index 0000000..5a5a49e --- /dev/null +++ b/server.py @@ -0,0 +1,21 @@ +import http.server +import sys + +class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(directory='web', *args, **kwargs) + + def end_headers(self): + self.send_header('Cross-Origin-Opener-Policy', 'same-origin') + self.send_header('Cross-Origin-Embedder-Policy', 'require-corp') + super().end_headers() + +if __name__ == '__main__': + print(f"Serving HTTP on port 8000 (http://127.0.0.1:8000/) ...") + try: + server_address = ('', 8000) + httpd = http.server.HTTPServer(server_address, CustomHTTPRequestHandler) + httpd.serve_forever() + except KeyboardInterrupt: + print("\nKeyboard interrupt received, exiting.") + sys.exit(0) diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..310add4 --- /dev/null +++ b/shell.nix @@ -0,0 +1,41 @@ +{ system ? builtins.currentSystem }: +let + pkgs = import ./nix/nixpkgs.nix { inherit system; }; + myHaskellPackages = pkgs.haskellPackages.extend + (final: prev: { haskell-wasm-repl = import ./. { inherit system; }; }); + scripts = [ + (pkgs.writeShellScriptBin "clean" "cabal clean") + (pkgs.writeShellScriptBin "compile" + ''cabal build -j -O2 --ghc-options="-j" "$@"'') + (pkgs.writeShellScriptBin "run" + ''cabal run -j -O2 --ghc-options="-j" "$@"'') + (pkgs.writeShellScriptBin "create-static-env-gcroot" '' + nix-store --realise --add-root .gcroots/static-env `nix-instantiate static-env.nix 2> /dev/null` + '') + (pkgs.writeShellScriptBin "build" "nom-build") + (pkgs.writeShellScriptBin "build-static" '' + set -xeuo pipefail; + nom-build --arg static true + create-static-env-gcroot + '') + (pkgs.writeShellScriptBin "build-docker" '' + set -xeuo pipefail; + nom-build docker.nix + create-static-env-gcroot + '') + (pkgs.writeShellScriptBin "build-wasm" '' + set -xeuo pipefail; + nom-build wasm.nix + cp -f result/bin/out.wasm web/ + create-static-env-gcroot + '') + (pkgs.writeShellScriptBin "serve" '' + ${pkgs.python3}/bin/python server.py + '') + ]; +in myHaskellPackages.shellFor { + name = "haskell-shell"; + packages = p: [ p.haskell-wasm-repl ]; + nativeBuildInputs = with pkgs; + [ cabal-install niv nix-output-monitor ] ++ scripts; +} diff --git a/static-env.nix b/static-env.nix new file mode 100644 index 0000000..712b82a --- /dev/null +++ b/static-env.nix @@ -0,0 +1,24 @@ +{ system ? builtins.currentSystem }: +let + pkgs = import ./nix/nixpkgs.nix { + inherit system; + static = true; + }; + haskell-wasm-repl = import ./. { + inherit system; + static = true; + }; + ghc-env = pkgs.haskellPackages.ghc.withPackages + (_: pkgs.haskell.lib.getHaskellBuildInputs haskell-wasm-repl); + static-deps = pkgs.callPackage ./nix/haskell-wasm-repl/static-deps.nix { }; +in pkgs.symlinkJoin { + name = "haskell-wasm-repl-static-env"; + paths = [ + ghc-env + pkgs.cabal2nix-unwrapped + static-deps.gmp6 + static-deps.libffi + static-deps.ncurses + static-deps.zlib + ]; +} diff --git a/wasm.nix b/wasm.nix new file mode 100644 index 0000000..5c10ba1 --- /dev/null +++ b/wasm.nix @@ -0,0 +1,8 @@ +{ system ? builtins.currentSystem }: +let + pkgs = import ./nix/nixpkgs.nix { + system = (builtins.head (builtins.split "-" system)) + "-linux"; + }; +in pkgs.callPackage ./nix/haskell-wasm-repl/wasm.nix { + haskell-wasm-repl-docker = import ./docker.nix { inherit system; }; +} diff --git a/web/browser_wasi_shim/index.js b/web/browser_wasi_shim/index.js new file mode 100755 index 0000000..82a6032 --- /dev/null +++ b/web/browser_wasi_shim/index.js @@ -0,0 +1 @@ +!function(t,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var r=e();for(var s in r)("object"==typeof exports?exports:t)[s]=r[s]}}(self,(()=>(()=>{"use strict";var t={759:(t,e,r)=>{r.d(e,{CLOCKID_MONOTONIC:()=>n,CLOCKID_REALTIME:()=>s,Ciovec:()=>_,Dirent:()=>c,ERRNO_BADF:()=>i,ERRNO_INVAL:()=>f,FILETYPE_DIRECTORY:()=>u,FILETYPE_REGULAR_FILE:()=>h,Fdstat:()=>p,Filestat:()=>b,Iovec:()=>l,OFLAGS_CREAT:()=>y,OFLAGS_DIRECTORY:()=>m,OFLAGS_EXCL:()=>w,OFLAGS_TRUNC:()=>g,Prestat:()=>E,WHENCE_CUR:()=>d,WHENCE_END:()=>o,WHENCE_SET:()=>a});const s=0,n=1,i=8,f=28;class l{static read_bytes(t,e){let r=new l;return r.buf=t.getUint32(e,!0),r.buf_len=t.getUint32(e+4,!0),r}static read_bytes_array(t,e,r){let s=[];for(let n=0;n{for(var s in e)r.o(e,s)&&!r.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:e[s]})},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var s={};return(()=>{r.r(s),r.d(s,{Directory:()=>f,Fd:()=>n,File:()=>i,OpenDirectory:()=>_,OpenFile:()=>l,PreopenDirectory:()=>a,WASI:()=>e,strace:()=>d});var t=r(759);let e=class{start(t){this.inst=t,t.exports._start()}initialize(t){this.inst=t,t.exports._initialize()}constructor(e,r,s){this.args=[],this.env=[],this.fds=[],this.args=e,this.env=r,this.fds=s;let n=this;this.wasiImport={args_sizes_get(t,e){let r=new DataView(n.inst.exports.memory.buffer);r.setUint32(t,n.args.length,!0);let s=0;for(let t of n.args)s+=t.length+1;return r.setUint32(e,s,!0),0},args_get(t,e){let r=new DataView(n.inst.exports.memory.buffer),s=new Uint8Array(n.inst.exports.memory.buffer);for(let i=0;inull!=n.fds[e]?n.fds[e].fd_advise(r,s,i):t.ERRNO_BADF,fd_allocate:(e,r,s)=>null!=n.fds[e]?n.fds[e].fd_allocate(r,s):t.ERRNO_BADF,fd_close(e){if(null!=n.fds[e]){let t=n.fds[e].fd_close();return n.fds[e]=void 0,t}return t.ERRNO_BADF},fd_datasync:e=>null!=n.fds[e]?n.fds[e].fd_datasync():t.ERRNO_BADF,fd_fdstat_get(e,r){if(null!=n.fds[e]){let{ret:t,fdstat:s}=n.fds[e].fd_fdstat_get();return null!=s&&s.write_bytes(new DataView(n.inst.exports.memory.buffer),r),t}return t.ERRNO_BADF},fd_fdstat_set_flags:(e,r)=>null!=n.fds[e]?n.fds[e].fd_fdstat_set_flags(r):t.ERRNO_BADF,fd_fdstat_set_rights:(e,r,s)=>null!=n.fds[e]?n.fds[e].fd_fdstat_set_rights(r,s):t.ERRNO_BADF,fd_filestat_get(e,r){if(null!=n.fds[e]){let{ret:t,filestat:s}=n.fds[e].fd_filestat_get();return null!=s&&s.write_bytes(new DataView(n.inst.exports.memory.buffer),r),t}return t.ERRNO_BADF},fd_filestat_set_size:(e,r)=>null!=n.fds[e]?n.fds[e].fd_filestat_set_size(r):t.ERRNO_BADF,fd_filestat_set_times:(e,r,s,i)=>null!=n.fds[e]?n.fds[e].fd_filestat_set_times(r,s,i):t.ERRNO_BADF,fd_pread(e,r,s,i,f){let l=new DataView(n.inst.exports.memory.buffer),_=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let a=t.Iovec.read_bytes_array(l,r,s),{ret:d,nread:o}=n.fds[e].fd_pread(_,a,i);return l.setUint32(f,o,!0),d}return t.ERRNO_BADF},fd_prestat_get(e,r){let s=new DataView(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let{ret:t,prestat:i}=n.fds[e].fd_prestat_get();return null!=i&&i.write_bytes(s,r),t}return t.ERRNO_BADF},fd_prestat_dir_name(e,r,s){if(null!=n.fds[e]){let{ret:t,prestat_dir_name:s}=n.fds[e].fd_prestat_dir_name();return null!=s&&new Uint8Array(n.inst.exports.memory.buffer).set(s,r),t}return t.ERRNO_BADF},fd_pwrite(e,r,s,i,f){let l=new DataView(n.inst.exports.memory.buffer),_=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let a=t.Ciovec.read_bytes_array(l,r,s),{ret:d,nwritten:o}=n.fds[e].fd_pwrite(_,a,i);return l.setUint32(f,o,!0),d}return t.ERRNO_BADF},fd_read(e,r,s,i){let f=new DataView(n.inst.exports.memory.buffer),l=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let _=t.Iovec.read_bytes_array(f,r,s),{ret:a,nread:d}=n.fds[e].fd_read(l,_);return f.setUint32(i,d,!0),a}return t.ERRNO_BADF},fd_readdir(e,r,s,i,f){let l=new DataView(n.inst.exports.memory.buffer),_=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let t=0;for(;;){let{ret:a,dirent:d}=n.fds[e].fd_readdir_single(i);if(0!=a)return l.setUint32(f,t,!0),a;if(null==d)break;let o=d.length();if(s-tnull!=n.fds[e]?n.fds[e].fd_sync():t.ERRNO_BADF,fd_tell(e,r){let s=new DataView(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let{ret:t,offset:i}=n.fds[e].fd_tell();return s.setUint32(r,i,!0),t}return t.ERRNO_BADF},fd_write(e,r,s,i){let f=new DataView(n.inst.exports.memory.buffer),l=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let _=t.Ciovec.read_bytes_array(f,r,s),{ret:a,nwritten:d}=n.fds[e].fd_write(l,_);return f.setUint32(i,d,!0),a}return t.ERRNO_BADF},path_create_directory(t,e,r){let s=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[t]){let i=new TextDecoder("utf-8").decode(s.slice(e,e+r));return n.fds[t].path_create_directory(i)}},path_filestat_get(e,r,s,i,f){let l=new DataView(n.inst.exports.memory.buffer),_=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let t=new TextDecoder("utf-8").decode(_.slice(s,s+i)),{ret:a,filestat:d}=n.fds[e].path_filestat_get(r,t);return null!=d&&d.write_bytes(l,f),a}return t.ERRNO_BADF},path_filestat_set_times(e,r,s,i,f,l,_){let a=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let t=new TextDecoder("utf-8").decode(a.slice(s,s+i));return n.fds[e].path_filestat_set_times(r,t,f,l,_)}return t.ERRNO_BADF},path_link(e,r,s,i,f,l,_){let a=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[e]&&null!=n.fds[f]){let t=new TextDecoder("utf-8").decode(a.slice(s,s+i)),d=new TextDecoder("utf-8").decode(a.slice(l,l+_));return n.fds[f].path_link(e,r,t,d)}return t.ERRNO_BADF},path_open(e,r,s,i,f,l,_,a,d){let o=new DataView(n.inst.exports.memory.buffer),u=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let t=new TextDecoder("utf-8").decode(u.slice(s,s+i)),{ret:h,fd_obj:c}=n.fds[e].path_open(r,t,f,l,_,a);if(0!=h)return h;n.fds.push(c);let p=n.fds.length-1;return o.setUint32(d,p,!0),0}return t.ERRNO_BADF},path_readlink(e,r,s,i,f,l){let _=new DataView(n.inst.exports.memory.buffer),a=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let d=new TextDecoder("utf-8").decode(a.slice(r,r+s)),{ret:o,data:u}=n.fds[e].path_readlink(d);if(null!=u){if(u.length>f)return _.setUint32(l,0,!0),t.ERRNO_BADF;a.set(u,i),_.setUint32(l,u.length,!0)}return o}return t.ERRNO_BADF},path_remove_directory(e,r,s){let i=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let t=new TextDecoder("utf-8").decode(i.slice(r,r+s));return n.fds[e].path_remove_directory(t)}return t.ERRNO_BADF},path_rename(t,e,r,s,n,i){throw"FIXME what is the best abstraction for this?"},path_symlink(e,r,s,i,f){let l=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[s]){let t=new TextDecoder("utf-8").decode(l.slice(e,e+r)),_=new TextDecoder("utf-8").decode(l.slice(i,i+f));return n.fds[s].path_symlink(t,_)}return t.ERRNO_BADF},path_unlink_file(e,r,s){let i=new Uint8Array(n.inst.exports.memory.buffer);if(null!=n.fds[e]){let t=new TextDecoder("utf-8").decode(i.slice(r,r+s));return n.fds[e].path_unlink_file(t)}return t.ERRNO_BADF},poll_oneoff(t,e,r){throw"async io not supported"},proc_exit(t){throw"exit with exit code "+t},proc_raise(t){throw"raised signal "+t},sched_yield(){},random_get(t,e){let r=new Uint8Array(n.inst.exports.memory.buffer);for(let s=0;s"/"!=t));for(let t=0;tthis.file.size){let t=this.file.data;this.file.data=new Uint8Array(Number(this.file_pos+BigInt(e.byteLength))),this.file.data.set(t)}this.file.data.set(e.slice(0,this.file.size-Number(this.file_pos)),Number(this.file_pos)),this.file_pos+=BigInt(e.byteLength),r+=s.buf_len}return{ret:0,nwritten:r}}fd_filestat_get(){return{ret:0,filestat:this.file.stat()}}constructor(t){super(),this.file_pos=0n,this.file=t}}class _ extends n{fd_fdstat_get(){return{ret:0,fdstat:new t.Fdstat(t.FILETYPE_DIRECTORY,0)}}fd_readdir_single(e){if(e>=BigInt(Object.keys(this.dir.contents).length))return{ret:0,dirent:null};let r=Object.keys(this.dir.contents)[Number(e)],s=this.dir.contents[r];return new TextEncoder("utf-8").encode(r),{ret:0,dirent:new t.Dirent(e+1n,r,s.stat().filetype)}}path_filestat_get(t,e){let r=this.dir.get_entry_for_path(e);return null==r?{ret:-1,filestat:null}:{ret:0,filestat:r.stat()}}path_open(e,r,s,n,a,d){let o=this.dir.get_entry_for_path(r);if(null==o){if((s&t.OFLAGS_CREAT)!=t.OFLAGS_CREAT)return{ret:-1,fd_obj:null};o=this.dir.create_entry_for_path(r)}else if((s&t.OFLAGS_EXCL)==t.OFLAGS_EXCL)return{ret:-1,fd_obj:null};if((s&t.OFLAGS_DIRECTORY)==t.OFLAGS_DIRECTORY&&o.stat().filetype!=t.FILETYPE_DIRECTORY)return{ret:-1,fd_obj:null};if((s&t.OFLAGS_TRUNC)==t.OFLAGS_TRUNC&&o.truncate(),o instanceof i)return{ret:0,fd_obj:new l(o)};if(o instanceof f)return{ret:0,fd_obj:new _(o)};throw"dir entry neither file nor dir"}constructor(t){super(),this.dir=t}}class a extends _{fd_prestat_get(){return{ret:0,prestat:t.Prestat.dir(this.prestat_name.length)}}fd_prestat_dir_name(){return{ret:0,prestat_dir_name:this.prestat_name}}constructor(t,e){super(new f(e)),this.prestat_name=new TextEncoder("utf-8").encode(t)}}function d(t,e){return new Proxy(t,{get(t,r,s){let n=Reflect.get(t,r,s);return e.includes(r)?n:function(...t){return console.log(r,"(",...t,")"),Reflect.apply(n,s,t)}}})}})(),s})())); \ No newline at end of file diff --git a/web/browser_wasi_shim/wasi_defs.js b/web/browser_wasi_shim/wasi_defs.js new file mode 100755 index 0000000..1d7cf70 --- /dev/null +++ b/web/browser_wasi_shim/wasi_defs.js @@ -0,0 +1 @@ +!function(_,R){if("object"==typeof exports&&"object"==typeof module)module.exports=R();else if("function"==typeof define&&define.amd)define([],R);else{var E=R();for(var N in E)("object"==typeof exports?exports:_)[N]=E[N]}}(self,(()=>(()=>{"use strict";var _={d:(R,E)=>{for(var N in E)_.o(E,N)&&!_.o(R,N)&&Object.defineProperty(R,N,{enumerable:!0,get:E[N]})},o:(_,R)=>Object.prototype.hasOwnProperty.call(_,R),r:_=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(_,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(_,"__esModule",{value:!0})}},R={};_.r(R),_.d(R,{ADVICE_DONTNEED:()=>hR,ADVICE_NOREUSE:()=>lR,ADVICE_NORMAL:()=>UR,ADVICE_RANDOM:()=>oR,ADVICE_SEQUENTIAL:()=>HR,ADVICE_WILLNEED:()=>aR,CLOCKID_MONOTONIC:()=>T,CLOCKID_PROCESS_CPUTIME_ID:()=>S,CLOCKID_REALTIME:()=>O,CLOCKID_THREAD_CPUTIME_ID:()=>e,Ciovec:()=>eR,Dirent:()=>PR,ERRNO_2BIG:()=>A,ERRNO_ACCES:()=>i,ERRNO_ADDRINUSE:()=>s,ERRNO_ADDRNOTAVAIL:()=>L,ERRNO_AFNOSUPPORT:()=>n,ERRNO_AGAIN:()=>D,ERRNO_ALREADY:()=>G,ERRNO_BADF:()=>C,ERRNO_BADMSG:()=>r,ERRNO_BUSY:()=>F,ERRNO_CANCELED:()=>P,ERRNO_CHILD:()=>U,ERRNO_CONNABORTED:()=>H,ERRNO_CONNREFUSED:()=>o,ERRNO_CONNRESET:()=>a,ERRNO_DEADLK:()=>h,ERRNO_DESTADDRREQ:()=>l,ERRNO_DOM:()=>c,ERRNO_DQUOT:()=>M,ERRNO_EXIST:()=>d,ERRNO_FAULT:()=>f,ERRNO_FBIG:()=>y,ERRNO_HOSTUNREACH:()=>g,ERRNO_IDRM:()=>b,ERRNO_ILSEQ:()=>Y,ERRNO_INPROGRESS:()=>B,ERRNO_INTR:()=>u,ERRNO_INVAL:()=>K,ERRNO_IO:()=>p,ERRNO_ISCONN:()=>V,ERRNO_ISDIR:()=>m,ERRNO_LOOP:()=>W,ERRNO_MFILE:()=>w,ERRNO_MLINK:()=>v,ERRNO_MSGSIZE:()=>X,ERRNO_MULTIHOP:()=>j,ERRNO_NAMETOOLONG:()=>x,ERRNO_NETDOWN:()=>Q,ERRNO_NETRESET:()=>Z,ERRNO_NETUNREACH:()=>k,ERRNO_NFILE:()=>z,ERRNO_NOBUFS:()=>q,ERRNO_NODEV:()=>J,ERRNO_NOENT:()=>$,ERRNO_NOEXEC:()=>__,ERRNO_NOLCK:()=>R_,ERRNO_NOLINK:()=>E_,ERRNO_NOMEM:()=>N_,ERRNO_NOMSG:()=>t_,ERRNO_NOPROTOOPT:()=>O_,ERRNO_NOSPC:()=>T_,ERRNO_NOSYS:()=>S_,ERRNO_NOTCAPABLE:()=>y_,ERRNO_NOTCONN:()=>e_,ERRNO_NOTDIR:()=>I_,ERRNO_NOTEMPTY:()=>A_,ERRNO_NOTRECOVERABLE:()=>i_,ERRNO_NOTSOCK:()=>s_,ERRNO_NOTSUP:()=>L_,ERRNO_NOTTY:()=>n_,ERRNO_NXIO:()=>D_,ERRNO_OVERFLOW:()=>G_,ERRNO_OWNERDEAD:()=>C_,ERRNO_PERM:()=>r_,ERRNO_PIPE:()=>F_,ERRNO_PROTO:()=>P_,ERRNO_PROTONOSUPPORT:()=>U_,ERRNO_PROTOTYPE:()=>H_,ERRNO_RANGE:()=>o_,ERRNO_ROFS:()=>a_,ERRNO_SPIPE:()=>h_,ERRNO_SRCH:()=>l_,ERRNO_STALE:()=>c_,ERRNO_SUCCESS:()=>I,ERRNO_TIMEDOUT:()=>M_,ERRNO_TXTBSY:()=>d_,ERRNO_XDEV:()=>f_,EVENTRWFLAGS_FD_READWRITE_HANGUP:()=>jR,EVENTTYPE_CLOCK:()=>wR,EVENTTYPE_FD_READ:()=>vR,EVENTTYPE_FD_WRITE:()=>XR,FDFLAGS_APPEND:()=>cR,FDFLAGS_DSYNC:()=>MR,FDFLAGS_NONBLOCK:()=>dR,FDFLAGS_RSYNC:()=>fR,FDFLAGS_SYNC:()=>yR,FD_STDERR:()=>t,FD_STDIN:()=>E,FD_STDOUT:()=>N,FILETYPE_BLOCK_DEVICE:()=>LR,FILETYPE_CHARACTER_DEVICE:()=>nR,FILETYPE_DIRECTORY:()=>DR,FILETYPE_REGULAR_FILE:()=>GR,FILETYPE_SOCKET_DGRAM:()=>CR,FILETYPE_SOCKET_STREAM:()=>rR,FILETYPE_SYMBOLIC_LINK:()=>FR,FILETYPE_UNKNOWN:()=>sR,FSTFLAGS_ATIM:()=>bR,FSTFLAGS_ATIM_NOW:()=>YR,FSTFLAGS_MTIM:()=>BR,FSTFLAGS_MTIM_NOW:()=>uR,Fdstat:()=>gR,Filestat:()=>WR,Iovec:()=>SR,OFLAGS_CREAT:()=>KR,OFLAGS_DIRECTORY:()=>pR,OFLAGS_EXCL:()=>VR,OFLAGS_TRUNC:()=>mR,PREOPENTYPE_DIR:()=>dE,Prestat:()=>yE,PrestatDir:()=>fE,RIFLAGS_RECV_PEEK:()=>aE,RIFLAGS_RECV_WAITALL:()=>hE,RIGHTS_FD_ADVISE:()=>V_,RIGHTS_FD_ALLOCATE:()=>m_,RIGHTS_FD_DATASYNC:()=>g_,RIGHTS_FD_FDSTAT_SET_FLAGS:()=>B_,RIGHTS_FD_FILESTAT_GET:()=>$_,RIGHTS_FD_FILESTAT_SET_SIZE:()=>_R,RIGHTS_FD_FILESTAT_SET_TIMES:()=>RR,RIGHTS_FD_READ:()=>b_,RIGHTS_FD_READDIR:()=>x_,RIGHTS_FD_SEEK:()=>Y_,RIGHTS_FD_SYNC:()=>u_,RIGHTS_FD_TELL:()=>K_,RIGHTS_FD_WRITE:()=>p_,RIGHTS_PATH_CREATE_DIRECTORY:()=>W_,RIGHTS_PATH_CREATE_FILE:()=>w_,RIGHTS_PATH_FILESTAT_GET:()=>z_,RIGHTS_PATH_FILESTAT_SET_SIZE:()=>q_,RIGHTS_PATH_FILESTAT_SET_TIMES:()=>J_,RIGHTS_PATH_LINK_SOURCE:()=>v_,RIGHTS_PATH_LINK_TARGET:()=>X_,RIGHTS_PATH_OPEN:()=>j_,RIGHTS_PATH_READLINK:()=>Q_,RIGHTS_PATH_REMOVE_DIRECTORY:()=>NR,RIGHTS_PATH_RENAME_SOURCE:()=>Z_,RIGHTS_PATH_RENAME_TARGET:()=>k_,RIGHTS_PATH_SYMLINK:()=>ER,RIGHTS_PATH_UNLINK_FILE:()=>tR,RIGHTS_POLL_FD_READWRITE:()=>OR,RIGHTS_SOCK_SHUTDOWN:()=>TR,ROFLAGS_RECV_DATA_TRUNCATED:()=>lE,SDFLAGS_RD:()=>cE,SDFLAGS_WR:()=>ME,SIGNAL_ABRT:()=>$R,SIGNAL_ALRM:()=>SE,SIGNAL_BUS:()=>_E,SIGNAL_CHLD:()=>IE,SIGNAL_CONT:()=>AE,SIGNAL_FPE:()=>RE,SIGNAL_HUP:()=>ZR,SIGNAL_ILL:()=>qR,SIGNAL_INT:()=>kR,SIGNAL_KILL:()=>EE,SIGNAL_NONE:()=>QR,SIGNAL_PIPE:()=>TE,SIGNAL_POLL:()=>UE,SIGNAL_PROF:()=>FE,SIGNAL_PWR:()=>HE,SIGNAL_QUIT:()=>zR,SIGNAL_SEGV:()=>tE,SIGNAL_STOP:()=>iE,SIGNAL_SYS:()=>oE,SIGNAL_TERM:()=>eE,SIGNAL_TRAP:()=>JR,SIGNAL_TSTP:()=>sE,SIGNAL_TTIN:()=>LE,SIGNAL_TTOU:()=>nE,SIGNAL_URG:()=>DE,SIGNAL_USR1:()=>NE,SIGNAL_USR2:()=>OE,SIGNAL_VTALRM:()=>rE,SIGNAL_WINCH:()=>PE,SIGNAL_XCPU:()=>GE,SIGNAL_XFSZ:()=>CE,SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME:()=>xR,WHENCE_CUR:()=>AR,WHENCE_END:()=>iR,WHENCE_SET:()=>IR});const E=0,N=1,t=2,O=0,T=1,S=2,e=3,I=0,A=1,i=2,s=3,L=4,n=5,D=6,G=7,C=8,r=9,F=10,P=11,U=12,H=13,o=14,a=15,h=16,l=17,c=18,M=19,d=20,f=21,y=22,g=23,b=24,Y=25,B=26,u=27,K=28,p=29,V=30,m=31,W=32,w=33,v=34,X=35,j=36,x=37,Q=38,Z=39,k=40,z=41,q=42,J=43,$=44,__=45,R_=46,E_=47,N_=48,t_=49,O_=50,T_=51,S_=52,e_=53,I_=54,A_=55,i_=56,s_=57,L_=58,n_=59,D_=60,G_=61,C_=62,r_=63,F_=64,P_=65,U_=66,H_=67,o_=68,a_=69,h_=70,l_=71,c_=72,M_=73,d_=74,f_=75,y_=76,g_=1,b_=2,Y_=4,B_=8,u_=16,K_=32,p_=64,V_=128,m_=256,W_=512,w_=1024,v_=2048,X_=4096,j_=8192,x_=16384,Q_=32768,Z_=65536,k_=1<<17,z_=1<<18,q_=1<<19,J_=1<<20,$_=1<<21,_R=1<<22,RR=1<<23,ER=1<<24,NR=1<<25,tR=1<<26,OR=1<<27,TR=1<<28;class SR{static read_bytes(_,R){let E=new SR;return E.buf=_.getUint32(R,!0),E.buf_len=_.getUint32(R+4,!0),E}static read_bytes_array(_,R,E){let N=[];for(let t=0;t + + + Haskell WASM REPL + + + + + + + +
+ + + + + + + + diff --git a/web/stack-worker.js b/web/stack-worker.js new file mode 100755 index 0000000..de3411a --- /dev/null +++ b/web/stack-worker.js @@ -0,0 +1,245 @@ +importScripts(location.origin + "/browser_wasi_shim/index.js"); +importScripts(location.origin + "/browser_wasi_shim/wasi_defs.js"); +importScripts(location.origin + "/worker-util.js"); +importScripts(location.origin + "/wasi-util.js"); + +onmessage = (msg) => { + serveIfInitMsg(msg); + var fds = [ + undefined, // 0: stdin + undefined, // 1: stdout + undefined, // 2: stderr + undefined, // 3: receive certificates + undefined, // 4: socket listenfd + undefined, // 5: accepted socket fd (multi-connection is unsupported) + // 6...: used by wasi shim + ]; + var certfd = 3; + var listenfd = 4; + var args = ['arg0', '--certfd='+certfd, '--net-listenfd='+listenfd, '--debug']; + var env = []; + var wasi = new WASI(args, env, fds); + wasiHack(wasi, certfd, 5); + wasiHackSocket(wasi, listenfd, 5); + fetch(getImagename(), { credentials: 'same-origin' }).then((resp) => { + resp['arrayBuffer']().then((wasm) => { + WebAssembly.instantiate(wasm, { + "wasi_snapshot_preview1": wasi.wasiImport, + "env": envHack(wasi), + }).then((inst) => { + wasi.start(inst.instance); + }); + }) + }); +}; + +// definition from wasi-libc https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-19/expected/wasm32-wasi/predefined-macros.txt +const ERRNO_INVAL = 28; +const ERRNO_AGAIN= 6; + +function wasiHack(wasi, certfd, connfd) { + var certbuf = new Uint8Array(0); + var _fd_close = wasi.wasiImport.fd_close; + wasi.wasiImport.fd_close = (fd) => { + if (fd == certfd) { + sendCert(certbuf); + return 0; + } + return _fd_close.apply(wasi.wasiImport, [fd]); + } + var _fd_fdstat_get = wasi.wasiImport.fd_fdstat_get; + wasi.wasiImport.fd_fdstat_get = (fd, fdstat_ptr) => { + if (fd == certfd) { + return 0; + } + return _fd_fdstat_get.apply(wasi.wasiImport, [fd, fdstat_ptr]); + } + wasi.wasiImport.fd_fdstat_set_flags = (fd, fdflags) => { + // TODO + return 0; + } + var _fd_write = wasi.wasiImport.fd_write; + wasi.wasiImport.fd_write = (fd, iovs_ptr, iovs_len, nwritten_ptr) => { + if ((fd == 1) || (fd == 2) || (fd == certfd)) { + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = Ciovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var wtotal = 0 + for (i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + var buf = buffer8.slice(iovec.buf, iovec.buf + iovec.buf_len); + if (buf.length == 0) { + continue; + } + console.log(new TextDecoder().decode(buf)); + if (fd == certfd) { + certbuf = appendData(certbuf, buf); + } + wtotal += buf.length; + } + buffer.setUint32(nwritten_ptr, wtotal, true); + return 0; + } + console.log("fd_write: unknown fd " + fd); + return _fd_write.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nwritten_ptr]); + } + wasi.wasiImport.poll_oneoff = (in_ptr, out_ptr, nsubscriptions, nevents_ptr) => { + if (nsubscriptions == 0) { + return ERRNO_INVAL; + } + let buffer = new DataView(wasi.inst.exports.memory.buffer); + let in_ = Subscription.read_bytes_array(buffer, in_ptr, nsubscriptions); + let isReadPollStdin = false; + let isReadPollConn = false; + let isClockPoll = false; + let pollSubStdin; + let pollSubConn; + let clockSub; + let timeout = Number.MAX_VALUE; + for (let sub of in_) { + if (sub.u.tag.variant == "fd_read") { + if ((sub.u.data.fd != 0) && (sub.u.data.fd != connfd)) { + return ERRNO_INVAL; // only fd=0 and connfd is supported as of now (FIXME) + } + if (sub.u.data.fd == 0) { + isReadPollStdin = true; + pollSubStdin = sub; + } else { + isReadPollConn = true; + pollSubConn = sub; + } + } else if (sub.u.tag.variant == "clock") { + if (sub.u.data.timeout < timeout) { + timeout = sub.u.data.timeout + isClockPoll = true; + clockSub = sub; + } + } else { + return ERRNO_INVAL; // FIXME + } + } + let events = []; + if (isReadPollStdin || isReadPollConn || isClockPoll) { + var sockreadable = sockWaitForReadable(timeout / 1000000000); + if (isReadPollConn) { + if (sockreadable == errStatus) { + return ERRNO_INVAL; + } else if (sockreadable == true) { + let event = new Event(); + event.userdata = pollSubConn.userdata; + event.error = 0; + event.type = new EventType("fd_read"); + events.push(event); + } + } + if (isClockPoll) { + let event = new Event(); + event.userdata = clockSub.userdata; + event.error = 0; + event.type = new EventType("clock"); + events.push(event); + } + } + var len = events.length; + Event.write_bytes_array(buffer, out_ptr, events); + buffer.setUint32(nevents_ptr, len, true); + return 0; + } +} + +function envHack(wasi){ + return { + http_send: function(addressP, addresslen, reqP, reqlen, idP){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var address = new Uint8Array(wasi.inst.exports.memory.buffer, addressP, addresslen); + var req = new Uint8Array(wasi.inst.exports.memory.buffer, reqP, reqlen); + streamCtrl[0] = 0; + postMessage({ + type: "http_send", + address: address, + req: req, + }); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var id = streamStatus[0]; + buffer.setUint32(idP, id, true); + return 0; + }, + http_writebody: function(id, bodyP, bodylen, nwrittenP, isEOF){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var body = new Uint8Array(wasi.inst.exports.memory.buffer, bodyP, bodylen); + streamCtrl[0] = 0; + postMessage({ + type: "http_writebody", + id: id, + body: body, + isEOF: isEOF, + }); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + buffer.setUint32(nwrittenP, bodylen, true); + return 0; + }, + http_isreadable: function(id, isOKP){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + streamCtrl[0] = 0; + postMessage({type: "http_isreadable", id: id}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var readable = 0; + if (streamData[0] == 1) { + readable = 1; + } + buffer.setUint32(isOKP, readable, true); + return 0; + }, + http_recv: function(id, respP, bufsize, respsizeP, isEOFP){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + + streamCtrl[0] = 0; + postMessage({type: "http_recv", id: id, len: bufsize}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var ddlen = streamLen[0]; + var resp = streamData.slice(0, ddlen); + buffer8.set(resp, respP); + buffer.setUint32(respsizeP, ddlen, true); + if (streamStatus[0] == 1) { + buffer.setUint32(isEOFP, 1, true); + } else { + buffer.setUint32(isEOFP, 0, true); + } + return 0; + }, + http_readbody: function(id, bodyP, bufsize, bodysizeP, isEOFP){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + + streamCtrl[0] = 0; + postMessage({type: "http_readbody", id: id, len: bufsize}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var ddlen = streamLen[0]; + var body = streamData.slice(0, ddlen); + buffer8.set(body, bodyP); + buffer.setUint32(bodysizeP, ddlen, true); + if (streamStatus[0] == 1) { + buffer.setUint32(isEOFP, 1, true); + } else { + buffer.setUint32(isEOFP, 0, true); + } + return 0; + } + }; +} diff --git a/web/stack.js b/web/stack.js new file mode 100755 index 0000000..7161173 --- /dev/null +++ b/web/stack.js @@ -0,0 +1,259 @@ +function newStack(worker, workerImageName, stackWorker, stackImageName) { + let p2vbuf = { + buf: new Uint8Array(0) // proxy => vm + }; + let v2pbuf = { + buf: new Uint8Array(0) // vm => proxy + }; + var proxyConn = { + sendbuf: p2vbuf, + recvbuf: v2pbuf + }; + var vmConn = { + sendbuf: v2pbuf, + recvbuf: p2vbuf + } + var proxyShared = new SharedArrayBuffer(12 + 4096); + var certbuf = { + buf: new Uint8Array(0), + done: false + } + stackWorker.onmessage = connect("proxy", proxyShared, proxyConn, certbuf); + stackWorker.postMessage({type: "init", buf: proxyShared, imagename: stackImageName}); + + var vmShared = new SharedArrayBuffer(12 + 4096); + worker.postMessage({type: "init", buf: vmShared, imagename: workerImageName}); + return connect("vm", vmShared, vmConn, certbuf); +} + +function connect(name, shared, conn, certbuf) { + var streamCtrl = new Int32Array(shared, 0, 1); + var streamStatus = new Int32Array(shared, 4, 1); + var streamLen = new Int32Array(shared, 8, 1); + var streamData = new Uint8Array(shared, 12); + var sendbuf = conn.sendbuf; + var recvbuf = conn.recvbuf; + let accepted = false; + var httpConnections = {}; + var curID = 0; + var maxID = 0x7FFFFFFF; // storable in streamStatus(signed 32bits) + function getID() { + var startID = curID; + while (true) { + if (httpConnections[curID] == undefined) { + return curID; + } + if (curID >= maxID) { + curID = 0; + } else { + curID++; + } + if (curID == startID) { + return -1; // exhausted + } + } + return curID; + } + function serveData(data, len) { + var length = len; + if (length > streamData.byteLength) + length = streamData.byteLength; + if (length > data.byteLength) + length = data.byteLength + var buf = data.slice(0, length); + var remain = data.slice(length, data.byteLength); + streamLen[0] = buf.byteLength; + streamData.set(buf, 0); + return remain; + } + return function(msg){ + const req_ = msg.data; + if (typeof req_ == "object" && req_.type) { + switch (req_.type) { + case "accept": + accepted = true; + streamData[0] = 1; // opened + streamStatus[0] = 0; + break; + case "send": + if (!accepted) { + console.log(name + ":" + "cannot send to unaccepted socket"); + streamStatus[0] = -1; + break; + } + sendbuf.buf = appendData(sendbuf.buf, req_.buf); + streamStatus[0] = 0; + break; + case "recv": + if (!accepted) { + console.log(name + ":" + "cannot recv from unaccepted socket"); + streamStatus[0] = -1; + break; + } + recvbuf.buf = serveData(recvbuf.buf, req_.len); + streamStatus[0] = 0; + break; + case "recv-is-readable": + var recvbufP = recvbuf.buf; + if (recvbufP.byteLength > 0) { + streamData[0] = 1; // ready for reading + } else { + if ((req_.timeout != undefined) && (req_.timeout > 0)) { + if (this.timeoutHandler) { + clearTimeout(this.timeoutHandler); + this.timeoutHandler = null; + } + this.timeoutHandler = setTimeout(() => { + if (this.timeoutHandler) { + clearTimeout(this.timeoutHandler); + this.timeoutHandler = null; + } + if (recvbuf.buf.byteLength > 0) { + streamData[0] = 1; // ready for reading + } else { + streamData[0] = 0; // timeout + } + streamStatus[0] = 0; + Atomics.store(streamCtrl, 0, 1); + Atomics.notify(streamCtrl, 0); + }, req_.timeout * 1000); + return; + } + streamData[0] = 0; // timeout + } + streamStatus[0] = 0; + break; + case "http_send": + var reqObj = JSON.parse(new TextDecoder().decode(req_.req)); + reqObj.mode = "cors"; + reqObj.credentials = "omit"; + if (reqObj.headers && reqObj.headers["User-Agent"] != "") { + delete reqObj.headers["User-Agent"]; // Browser will add its own value. + } + var reqID = getID(); + if (reqID < 0) { + console.log(name + ":" + "failed to get id"); + streamStatus[0] = -1; + break; + } + var connObj = { + address: new TextDecoder().decode(req_.address), + request: reqObj, + requestSent: false, + reqBodybuf: new Uint8Array(0), + reqBodyEOF: false, + }; + httpConnections[reqID] = connObj; + streamStatus[0] = reqID; + break; + case "http_writebody": + httpConnections[req_.id].reqBodybuf = appendData(httpConnections[req_.id].reqBodybuf, req_.body) + httpConnections[req_.id].reqBodyEOF = req_.isEOF; + streamStatus[0] = 0; + if (req_.isEOF && !httpConnections[req_.id].requestSent) { + httpConnections[req_.id].requestSent = true; + var connObj = httpConnections[req_.id]; + if ((connObj.request.method != "HEAD") && (connObj.request.method != "GET")) { + connObj.request.body = connObj.reqBodybuf; + } + fetch(connObj.address, connObj.request).then((resp) => { + connObj.response = new TextEncoder().encode(JSON.stringify({ + bodyUsed: resp.bodyUsed, + headers: resp.headers, + redirected: resp.redirected, + status: resp.status, + statusText: resp.statusText, + type: resp.type, + url: resp.url + })), + connObj.done = false; + connObj.respBodybuf = new Uint8Array(0); + if (resp.ok) { + resp.arrayBuffer().then((data) => { + connObj.respBodybuf = new Uint8Array(data); + connObj.done = true; + }).catch((error) => { + connObj.respBodybuf = new Uint8Array(0); + connObj.done = true; + console.log("failed to fetch body: " + error); + }); + } else { + connObj.done = true; + } + }).catch((error) => { + connObj.response = new TextEncoder().encode(JSON.stringify({ + status: 503, + statusText: "Service Unavailable", + })) + connObj.respBodybuf = new Uint8Array(0); + connObj.done = true; + }); + } + break; + case "http_isreadable": + if ((httpConnections[req_.id] != undefined) && (httpConnections[req_.id].response != undefined)) { + streamData[0] = 1; // ready for reading + } else { + streamData[0] = 0; // nothing to read + } + streamStatus[0] = 0; + break; + case "http_recv": + if ((httpConnections[req_.id] == undefined) || (httpConnections[req_.id].response == undefined)) { + console.log(name + ":" + "response is not available"); + streamStatus[0] = -1; + break; + } + httpConnections[req_.id].response = serveData(httpConnections[req_.id].response, req_.len); + streamStatus[0] = 0; + if (httpConnections[req_.id].response.byteLength == 0) { + streamStatus[0] = 1; // isEOF + } + break; + case "http_readbody": + if ((httpConnections[req_.id] == undefined) || (httpConnections[req_.id].response == undefined)) { + console.log(name + ":" + "response body is not available"); + streamStatus[0] = -1; + break; + } + httpConnections[req_.id].respBodybuf = serveData(httpConnections[req_.id].respBodybuf, req_.len); + streamStatus[0] = 0; + if ((httpConnections[req_.id].done) && (httpConnections[req_.id].respBodybuf.byteLength == 0)) { + streamStatus[0] = 1; + delete httpConnections[req_.id]; // connection done + } + break; + case "send_cert": + certbuf.buf = appendData(certbuf.buf, req_.buf); + certbuf.done = true; + streamStatus[0] = 0; + break; + case "recv_cert": + if (!certbuf.done) { + streamStatus[0] = -1; + break; + } + certbuf.buf = serveData(certbuf.buf, req_.len); + streamStatus[0] = 0; + if (certbuf.buf.byteLength == 0) { + streamStatus[0] = 1; // isEOF + } + break; + default: + console.log(name + ":" + "unknown request: " + req_.type) + return; + } + Atomics.store(streamCtrl, 0, 1); + Atomics.notify(streamCtrl, 0); + } else { + console.log("UNKNOWN MSG " + msg); + } + } +} + +function appendData(data1, data2) { + buf2 = new Uint8Array(data1.byteLength + data2.byteLength); + buf2.set(new Uint8Array(data1), 0); + buf2.set(new Uint8Array(data2), data1.byteLength); + return buf2; +} diff --git a/web/wasi-util.js b/web/wasi-util.js new file mode 100755 index 0000000..7714298 --- /dev/null +++ b/web/wasi-util.js @@ -0,0 +1,127 @@ +//////////////////////////////////////////////////////////// +// +// event-related classes adopted from the on-going discussion +// towards poll_oneoff support in browser_wasi_sim project. +// Ref: https://github.com/bjorn3/browser_wasi_shim/issues/14#issuecomment-1450351935 +// +//////////////////////////////////////////////////////////// + +class EventType { + /*:: variant: "clock" | "fd_read" | "fd_write"*/ + + constructor(variant/*: "clock" | "fd_read" | "fd_write"*/) { + this.variant = variant; + } + + static from_u8(data/*: number*/)/*: EventType*/ { + switch (data) { + case EVENTTYPE_CLOCK: + return new EventType("clock"); + case EVENTTYPE_FD_READ: + return new EventType("fd_read"); + case EVENTTYPE_FD_WRITE: + return new EventType("fd_write"); + default: + throw "Invalid event type " + String(data); + } + } + + to_u8()/*: number*/ { + switch (this.variant) { + case "clock": + return EVENTTYPE_CLOCK; + case "fd_read": + return EVENTTYPE_FD_READ; + case "fd_write": + return EVENTTYPE_FD_WRITE; + default: + throw "unreachable"; + } + } +} + +class Event { + /*:: userdata: UserData*/ + /*:: error: number*/ + /*:: type: EventType*/ + /*:: fd_readwrite: EventFdReadWrite | null*/ + + write_bytes(view/*: DataView*/, ptr/*: number*/) { + view.setBigUint64(ptr, this.userdata, true); + view.setUint8(ptr + 8, this.error); + view.setUint8(ptr + 9, 0); + view.setUint8(ptr + 10, this.type.to_u8()); + // if (this.fd_readwrite) { + // this.fd_readwrite.write_bytes(view, ptr + 16); + // } + } + + static write_bytes_array(view/*: DataView*/, ptr/*: number*/, events/*: Array*/) { + for (let i = 0; i < events.length; i++) { + events[i].write_bytes(view, ptr + 32 * i); + } + } +} + +class SubscriptionClock { + /*:: timeout: number*/ + + static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionFdReadWrite*/ { + let self = new SubscriptionClock(); + self.timeout = Number(view.getBigUint64(ptr + 8, true)); + return self; + } +} + +class SubscriptionFdReadWrite { + /*:: fd: number*/ + + static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionFdReadWrite*/ { + let self = new SubscriptionFdReadWrite(); + self.fd = view.getUint32(ptr, true); + return self; + } +} + +class SubscriptionU { + /*:: tag: EventType */ + /*:: data: SubscriptionClock | SubscriptionFdReadWrite */ + + static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionU*/ { + let self = new SubscriptionU(); + self.tag = EventType.from_u8(view.getUint8(ptr)); + switch (self.tag.variant) { + case "clock": + self.data = SubscriptionClock.read_bytes(view, ptr + 8); + break; + case "fd_read": + case "fd_write": + self.data = SubscriptionFdReadWrite.read_bytes(view, ptr + 8); + break; + default: + throw "unreachable"; + } + return self; + } +} + +class Subscription { + /*:: userdata: UserData */ + /*:: u: SubscriptionU */ + + static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: Subscription*/ { + let subscription = new Subscription(); + subscription.userdata = view.getBigUint64(ptr, true); + subscription.u = SubscriptionU.read_bytes(view, ptr + 8); + return subscription; + } + + static read_bytes_array(view/*: DataView*/, ptr/*: number*/, len/*: number*/)/*: Array*/ { + let subscriptions = []; + for (let i = 0; i < len; i++) { + subscriptions.push(Subscription.read_bytes(view, ptr + 48 * i)); + } + return subscriptions; + } +} + diff --git a/web/worker-util.js b/web/worker-util.js new file mode 100755 index 0000000..59f8645 --- /dev/null +++ b/web/worker-util.js @@ -0,0 +1,275 @@ +var streamCtrl; +var streamStatus; +var streamLen; +var streamData; +function registerSocketBuffer(shared){ + streamCtrl = new Int32Array(shared, 0, 1); + streamStatus = new Int32Array(shared, 4, 1); + streamLen = new Int32Array(shared, 8, 1); + streamData = new Uint8Array(shared, 12); +} + +var imagename; +function serveIfInitMsg(msg) { + const req_ = msg.data; + if (typeof req_ == "object"){ + if (req_.type == "init") { + if (req_.buf) + var shared = req_.buf; + registerSocketBuffer(shared); + if (req_.imagename) + imagename = req_.imagename; + return true; + } + } + + return false; +} + +function getImagename() { + return imagename; +} + +const errStatus = { + val: 0, +}; + +function sockAccept(){ + streamCtrl[0] = 0; + postMessage({type: "accept"}); + Atomics.wait(streamCtrl, 0, 0); + return streamData[0] == 1; +} +function sockSend(data){ + streamCtrl[0] = 0; + postMessage({type: "send", buf: data}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + errStatus.val = streamStatus[0] + return errStatus; + } +} +function sockRecv(len){ + streamCtrl[0] = 0; + postMessage({type: "recv", len: len}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + errStatus.val = streamStatus[0] + return errStatus; + } + let ddlen = streamLen[0]; + var res = streamData.slice(0, ddlen); + return res; +} + +function sockWaitForReadable(timeout){ + streamCtrl[0] = 0; + postMessage({type: "recv-is-readable", timeout: timeout}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + errStatus.val = streamStatus[0] + return errStatus; + } + return streamData[0] == 1; +} + +function sendCert(data){ + streamCtrl[0] = 0; + postMessage({type: "send_cert", buf: data}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + errStatus.val = streamStatus[0] + return errStatus; + } +} + +function recvCert(){ + var buf = new Uint8Array(0); + return new Promise((resolve, reject) => { + function getCert(){ + streamCtrl[0] = 0; + postMessage({type: "recv_cert"}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + setTimeout(getCert, 100); + return; + } + var ddlen = streamLen[0]; + buf = appendData(buf, streamData.slice(0, ddlen)); + if (streamStatus[0] == 0) { + resolve(buf); // EOF + } else { + setTimeout(getCert, 0); + return; + } + } + getCert(); + }); +} + +function appendData(data1, data2) { + buf2 = new Uint8Array(data1.byteLength + data2.byteLength); + buf2.set(new Uint8Array(data1), 0); + buf2.set(new Uint8Array(data2), data1.byteLength); + return buf2; +} + +function getCertDir(cert) { + var certDir = new PreopenDirectory("/.wasmenv", { + "proxy.crt": new File(cert) + }); + var _path_open = certDir.path_open; + certDir.path_open = (e, r, s, n, a, d) => { + var ret = _path_open.apply(certDir, [e, r, s, n, a, d]); + if (ret.fd_obj != null) { + var o = ret.fd_obj; + ret.fd_obj.fd_pread = (view8, iovs, offset) => { + var old_offset = o.file_pos; + var r = o.fd_seek(offset, WHENCE_SET); + if (r.ret != 0) { + return { ret: -1, nread: 0 }; + } + var read_ret = o.fd_read(view8, iovs); + r = o.fd_seek(old_offset, WHENCE_SET); + if (r.ret != 0) { + return { ret: -1, nread: 0 }; + } + return read_ret; + } + } + return ret; + } + certDir.dir.contents["."] = certDir.dir; + return certDir; +} + +function wasiHackSocket(wasi, listenfd, connfd) { + // definition from wasi-libc https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-19/expected/wasm32-wasi/predefined-macros.txt + const ERRNO_INVAL = 28; + const ERRNO_AGAIN= 6; + var connfdUsed = false; + var connbuf = new Uint8Array(0); + var _fd_close = wasi.wasiImport.fd_close; + wasi.wasiImport.fd_close = (fd) => { + if (fd == connfd) { + connfdUsed = false; + return 0; + } + return _fd_close.apply(wasi.wasiImport, [fd]); + } + var _fd_read = wasi.wasiImport.fd_read; + wasi.wasiImport.fd_read = (fd, iovs_ptr, iovs_len, nread_ptr) => { + if (fd == connfd) { + return wasi.wasiImport.sock_recv(fd, iovs_ptr, iovs_len, 0, nread_ptr, 0); + } + return _fd_read.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nread_ptr]); + } + var _fd_write = wasi.wasiImport.fd_write; + wasi.wasiImport.fd_write = (fd, iovs_ptr, iovs_len, nwritten_ptr) => { + if (fd == connfd) { + return wasi.wasiImport.sock_send(fd, iovs_ptr, iovs_len, 0, nwritten_ptr); + } + return _fd_write.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nwritten_ptr]); + } + var _fd_fdstat_get = wasi.wasiImport.fd_fdstat_get; + wasi.wasiImport.fd_fdstat_get = (fd, fdstat_ptr) => { + if ((fd == listenfd) || (fd == connfd) && connfdUsed){ + let buffer = new DataView(wasi.inst.exports.memory.buffer); + // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fdstat-struct + buffer.setUint8(fdstat_ptr, 6); // filetype = 6 (socket_stream) + buffer.setUint8(fdstat_ptr + 1, 2); // fdflags = 2 (nonblock) + return 0; + } + return _fd_fdstat_get.apply(wasi.wasiImport, [fd, fdstat_ptr]); + } + var _fd_prestat_get = wasi.wasiImport.fd_prestat_get; + wasi.wasiImport.fd_prestat_get = (fd, prestat_ptr) => { + if ((fd == listenfd) || (fd == connfd)){ // reserve socket-related fds + let buffer = new DataView(wasi.inst.exports.memory.buffer); + buffer.setUint8(prestat_ptr, 1); + return 0; + } + return _fd_prestat_get.apply(wasi.wasiImport, [fd, prestat_ptr]); + } + wasi.wasiImport.sock_accept = (fd, flags, fd_ptr) => { + if (fd != listenfd) { + console.log("sock_accept: unknown fd " + fd); + return ERRNO_INVAL; + } + if (connfdUsed) { + console.log("sock_accept: multi-connection is unsupported"); + return ERRNO_INVAL; + } + if (!sockAccept()) { + return ERRNO_AGAIN; + } + connfdUsed = true; + var buffer = new DataView(wasi.inst.exports.memory.buffer); + buffer.setUint32(fd_ptr, connfd, true); + return 0; + } + wasi.wasiImport.sock_send = (fd, iovs_ptr, iovs_len, si_flags/*not defined*/, nwritten_ptr) => { + if (fd != connfd) { + console.log("sock_send: unknown fd " + fd); + return ERRNO_INVAL; + } + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = Ciovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var wtotal = 0 + for (i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + var buf = buffer8.slice(iovec.buf, iovec.buf + iovec.buf_len); + if (buf.length == 0) { + continue; + } + var ret = sockSend(buf.buffer.slice(0, iovec.buf_len)); + if (ret == errStatus) { + return ERRNO_INVAL; + } + wtotal += buf.length; + } + buffer.setUint32(nwritten_ptr, wtotal, true); + return 0; + } + wasi.wasiImport.sock_recv = (fd, iovs_ptr, iovs_len, ri_flags, nread_ptr, ro_flags_ptr) => { + if (ri_flags != 0) { + console.log("ri_flags are unsupported"); // TODO + } + if (fd != connfd) { + console.log("sock_recv: unknown fd " + fd); + return ERRNO_INVAL; + } + var sockreadable = sockWaitForReadable(); + if (sockreadable == errStatus) { + return ERRNO_INVAL; + } else if (sockreadable == false) { + return ERRNO_AGAIN; + } + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = Iovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var nread = 0; + for (i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + if (iovec.buf_len == 0) { + continue; + } + var data = sockRecv(iovec.buf_len); + if (data == errStatus) { + return ERRNO_INVAL; + } + buffer8.set(data, iovec.buf); + nread += data.length; + } + buffer.setUint32(nread_ptr, nread, true); + // TODO: support ro_flags_ptr + return 0; + } + wasi.wasiImport.sock_shutdown = (fd, sdflags) => { + if (fd == connfd) { + connfdUsed = false; + } + return 0; + } +} diff --git a/web/worker.js b/web/worker.js new file mode 100755 index 0000000..fff503a --- /dev/null +++ b/web/worker.js @@ -0,0 +1,212 @@ +importScripts("https://cdn.jsdelivr.net/npm/xterm-pty@0.9.4/workerTools.js"); +importScripts(location.origin + "/browser_wasi_shim/index.js"); +importScripts(location.origin + "/browser_wasi_shim/wasi_defs.js"); +importScripts(location.origin + "/worker-util.js"); +importScripts(location.origin + "/wasi-util.js"); + +onmessage = (msg) => { + if (serveIfInitMsg(msg)) { + return; + } + var ttyClient = new TtyClient(msg.data); + var args = []; + var env = []; + var fds = []; + var netParam = null; + var listenfd = 3; + fetch(getImagename(), { credentials: 'same-origin' }).then((resp) => { + resp['arrayBuffer']().then((wasm) => { + if (netParam) { + if (netParam.mode == 'delegate') { + args = ['arg0', '--net=socket', '--mac', genmac()]; + } else if (netParam.mode == 'browser') { + recvCert().then((cert) => { + var certDir = getCertDir(cert); + fds = [ + undefined, // 0: stdin + undefined, // 1: stdout + undefined, // 2: stderr + certDir, // 3: certificates dir + undefined, // 4: socket listenfd + undefined, // 5: accepted socket fd (multi-connection is unsupported) + // 6...: used by wasi shim + ]; + args = ['arg0', '--net=socket=listenfd=4', '--mac', genmac()]; + env = [ + "SSL_CERT_FILE=/.wasmenv/proxy.crt", + "https_proxy=http://192.168.127.253:80", + "http_proxy=http://192.168.127.253:80", + "HTTPS_PROXY=http://192.168.127.253:80", + "HTTP_PROXY=http://192.168.127.253:80" + ]; + listenfd = 4; + startWasi(wasm, ttyClient, args, env, fds, listenfd, 5); + }); + return; + } + } + startWasi(wasm, ttyClient, args, env, fds, listenfd, 5); + }) + }); +}; + +function startWasi(wasm, ttyClient, args, env, fds, listenfd, connfd) { + var wasi = new WASI(args, env, fds); + wasiHack(wasi, ttyClient, connfd); + wasiHackSocket(wasi, listenfd, connfd); + WebAssembly.instantiate(wasm, { + "wasi_snapshot_preview1": wasi.wasiImport, + }).then((inst) => { + wasi.start(inst.instance); + }); +} + +// wasiHack patches wasi object for integrating it to xterm-pty. +function wasiHack(wasi, ttyClient, connfd) { + // definition from wasi-libc https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-19/expected/wasm32-wasi/predefined-macros.txt + const ERRNO_INVAL = 28; + const ERRNO_AGAIN= 6; + var _fd_read = wasi.wasiImport.fd_read; + wasi.wasiImport.fd_read = (fd, iovs_ptr, iovs_len, nread_ptr) => { + if (fd == 0) { + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = Iovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var nread = 0; + for (i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + if (iovec.buf_len == 0) { + continue; + } + var data = ttyClient.onRead(iovec.buf_len); + buffer8.set(data, iovec.buf); + nread += data.length; + } + buffer.setUint32(nread_ptr, nread, true); + return 0; + } else { + console.log("fd_read: unknown fd " + fd); + return _fd_read.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nread_ptr]); + } + return ERRNO_INVAL; + } + var _fd_write = wasi.wasiImport.fd_write; + wasi.wasiImport.fd_write = (fd, iovs_ptr, iovs_len, nwritten_ptr) => { + if ((fd == 1) || (fd == 2)) { + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = Ciovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var wtotal = 0 + for (i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + var buf = buffer8.slice(iovec.buf, iovec.buf + iovec.buf_len); + if (buf.length == 0) { + continue; + } + ttyClient.onWrite(Array.from(buf)); + wtotal += buf.length; + } + buffer.setUint32(nwritten_ptr, wtotal, true); + return 0; + } else { + console.log("fd_write: unknown fd " + fd); + return _fd_write.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nwritten_ptr]); + } + return ERRNO_INVAL; + } + wasi.wasiImport.poll_oneoff = (in_ptr, out_ptr, nsubscriptions, nevents_ptr) => { + if (nsubscriptions == 0) { + return ERRNO_INVAL; + } + let buffer = new DataView(wasi.inst.exports.memory.buffer); + let in_ = Subscription.read_bytes_array(buffer, in_ptr, nsubscriptions); + let isReadPollStdin = false; + let isReadPollConn = false; + let isClockPoll = false; + let pollSubStdin; + let pollSubConn; + let clockSub; + let timeout = Number.MAX_VALUE; + for (let sub of in_) { + if (sub.u.tag.variant == "fd_read") { + if ((sub.u.data.fd != 0) && (sub.u.data.fd != connfd)) { + console.log("poll_oneoff: unknown fd " + sub.u.data.fd); + return ERRNO_INVAL; // only fd=0 and connfd is supported as of now (FIXME) + } + if (sub.u.data.fd == 0) { + isReadPollStdin = true; + pollSubStdin = sub; + } else { + isReadPollConn = true; + pollSubConn = sub; + } + } else if (sub.u.tag.variant == "clock") { + if (sub.u.data.timeout < timeout) { + timeout = sub.u.data.timeout + isClockPoll = true; + clockSub = sub; + } + } else { + console.log("poll_oneoff: unknown variant " + sub.u.tag.variant); + return ERRNO_INVAL; // FIXME + } + } + let events = []; + if (isReadPollStdin || isReadPollConn || isClockPoll) { + var readable = false; + if (isReadPollStdin || (isClockPoll && timeout > 0)) { + readable = ttyClient.onWaitForReadable(timeout / 1000000000); + } + if (readable && isReadPollStdin) { + let event = new Event(); + event.userdata = pollSubStdin.userdata; + event.error = 0; + event.type = new EventType("fd_read"); + events.push(event); + } + if (isReadPollConn) { + var sockreadable = sockWaitForReadable(); + if (sockreadable == errStatus) { + return ERRNO_INVAL; + } else if (sockreadable == true) { + let event = new Event(); + event.userdata = pollSubConn.userdata; + event.error = 0; + event.type = new EventType("fd_read"); + events.push(event); + } + } + if (isClockPoll) { + let event = new Event(); + event.userdata = clockSub.userdata; + event.error = 0; + event.type = new EventType("clock"); + events.push(event); + } + } + var len = events.length; + Event.write_bytes_array(buffer, out_ptr, events); + buffer.setUint32(nevents_ptr, len, true); + return 0; + } +} + +function getNetParam() { + var vars = location.search.substring(1).split('&'); + for (var i = 0; i < vars.length; i++) { + var kv = vars[i].split('='); + if (decodeURIComponent(kv[0]) == 'net') { + return { + mode: kv[1], + param: kv[2], + }; + } + } + return null; +} + +function genmac(){ + return "02:XX:XX:XX:XX:XX".replace(/X/g, function() { + return "0123456789ABCDEF".charAt(Math.floor(Math.random() * 16)) + }); +} diff --git a/web/ws-delegate.js b/web/ws-delegate.js new file mode 100755 index 0000000..199a873 --- /dev/null +++ b/web/ws-delegate.js @@ -0,0 +1,105 @@ +function delegate(worker, workerImageName, address) { + var shared = new SharedArrayBuffer(8 + 4096); + var streamCtrl = new Int32Array(shared, 0, 1); + var streamStatus = new Int32Array(shared, 4, 1); + var streamLen = new Int32Array(shared, 8, 1); + var streamData = new Uint8Array(shared, 12); + worker.postMessage({type: "init", buf: shared, imagename: workerImageName}); + + var opts = 'binary'; + var ongoing = false; + var opened = false; + var accepted = false; + var wsconn; + var connbuf = new Uint8Array(0); + return function(msg) { + const req_ = msg.data; + if (typeof req_ == "object" && req_.type) { + switch (req_.type) { + case "accept": + if (opened) { + streamData[0] = 1; // opened + accepted = true; + } else { + streamData[0] = 0; // not opened + if (!ongoing) { + ongoing = true; + wsconn = new WebSocket(address, opts); + wsconn.binaryType = 'arraybuffer'; + wsconn.onmessage = function(event) { + buf2 = new Uint8Array(connbuf.length + event.data.byteLength); + var o = connbuf.length; + buf2.set(connbuf, 0); + buf2.set(new Uint8Array(event.data), o); + connbuf = buf2; + }; + wsconn.onclose = function(event) { + console.log("websocket closed" + event.code + " " + event.reason + " " + event.wasClean); + opened = false; + accepted = false; + ongoing = false; + }; + wsconn.onopen = function(event) { + opened = true; + accepted = false; + ongoing = false; + }; + wsconn.onerror = function(error) { + console.log("websocket error: "+error.data); + opened = false; + accepted = false; + ongoing = false; + }; + } + } + streamStatus[0] = 0; + break; + case "send": + if (!accepted) { + console.log("ERROR: cannot send to unaccepted websocket"); + streamStatus[0] = -1; + break; + } + wsconn.send(req_.buf); + streamStatus[0] = 0; + break; + case "recv": + if (!accepted) { + console.log("ERROR: cannot receive from unaccepted websocket"); + streamStatus[0] = -1; + break; + } + var length = req_.len; + if (length > streamData.length) + length = streamData.length; + if (length > connbuf.length) + length = connbuf.length + var buf = connbuf.slice(0, length); + var remain = connbuf.slice(length, connbuf.length); + connbuf = remain; + streamLen[0] = buf.length; + streamData.set(buf, 0); + streamStatus[0] = 0; + break; + case "recv-is-readable": + if (!accepted) { + console.log("ERROR: cannot poll unaccepted websocket"); + streamStatus[0] = -1; + break; + } + if (connbuf.length > 0) { + streamData[0] = 1; // ready for reading + } else { + streamData[0] = 0; // timeout + } + streamStatus[0] = 0; + break; + default: + console.log("unknown request: " + req_.type) + return; + } + Atomics.store(streamCtrl, 0, 1); + Atomics.notify(streamCtrl, 0); + } + } +}