Initial commit
This commit is contained in:
commit
57f821b30b
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
dist-newstyle
|
||||
result
|
||||
.gcroots/
|
||||
web/*.wasm
|
|
@ -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
|
|
@ -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"
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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"
|
||||
])
|
||||
])
|
|
@ -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";
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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
|
|
@ -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/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
# This file has been generated by Niv.
|
||||
|
||||
let
|
||||
|
||||
#
|
||||
# The fetchers. fetch_<type> fetches specs of type <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 = <nixpkgs> == ./.;
|
||||
in
|
||||
if builtins.hasAttr "nixpkgs" sources
|
||||
then sourcesNixpkgs
|
||||
else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
|
||||
import <nixpkgs> { }
|
||||
else
|
||||
abort
|
||||
''
|
||||
Please specify either <nixpkgs> (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); }
|
|
@ -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)
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
];
|
||||
}
|
|
@ -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; };
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,58 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Haskell WASM REPL</title>
|
||||
<link rel="preload" href="out.wasm" as="fetch">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.17.0/css/xterm.css">
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='34' height='23'%3E%3Cpath d='M 13.926758 17.000000 L 11.281250 17.000000 L 14.665039 7.718750 L 14.401367 7.068359 Q 14.287109 6.778320 14.124512 6.483887 Q 13.961914 6.189453 13.746582 5.952148 Q 13.531250 5.714844 13.271973 5.569824 Q 13.012695 5.424805 12.696289 5.424805 Q 12.546875 5.424805 12.371094 5.442383 Q 12.195312 5.459961 12.019531 5.477539 L 11.966797 3.649414 Q 12.107422 3.614258 12.283203 3.574707 Q 12.458984 3.535156 12.647949 3.508789 Q 12.836914 3.482422 13.025879 3.464844 Q 13.214844 3.447266 13.390625 3.447266 Q 14.032227 3.447266 14.528809 3.693359 Q 15.025391 3.939453 15.390137 4.299805 Q 15.754883 4.660156 15.996582 5.086426 Q 16.238281 5.512695 16.387695 5.864258 L 19.437500 13.633789 Q 19.525391 13.906250 19.648438 14.183105 Q 19.771484 14.459961 19.934082 14.679688 Q 20.096680 14.899414 20.294434 15.035645 Q 20.492188 15.171875 20.720703 15.171875 Q 20.791016 15.171875 20.830566 15.171875 Q 20.870117 15.171875 20.905273 15.167480 Q 20.940430 15.163086 20.984375 15.158691 Q 21.028320 15.154297 21.107422 15.145508 L 21.054688 17.061523 Q 20.878906 17.114258 20.681152 17.136230 Q 20.483398 17.158203 20.307617 17.158203 Q 19.648438 17.158203 19.138672 16.947266 Q 18.628906 16.736328 18.250977 16.380371 Q 17.873047 16.024414 17.604980 15.562988 Q 17.336914 15.101562 17.161133 14.609375 L 15.772461 10.733398 L 15.517578 11.955078 Z ' fill='rgb(255, 147, 0)' fill-opacity='1.000000' /%3E%3C/svg%3E" />
|
||||
<style>
|
||||
body {
|
||||
height: calc(100vh - 2rem);
|
||||
width: calc(100% - 2rem);
|
||||
margin: 1rem;
|
||||
background-color: #000;
|
||||
}
|
||||
#terminal {
|
||||
height: 100%;
|
||||
}
|
||||
.xterm {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript style="color: lightgreen;">Please enable JavaScript to use this program.</noscript>
|
||||
<div id="terminal"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@4.17.0/lib/xterm.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-pty@0.9.4/index.js"></script>
|
||||
<script src=" https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.min.js "></script>
|
||||
<script src="./stack.js"></script>
|
||||
<script src="./ws-delegate.js"></script>
|
||||
<script>
|
||||
const xterm = new Terminal();
|
||||
const fitAddon = new FitAddon.FitAddon();
|
||||
xterm.loadAddon(fitAddon);
|
||||
xterm.open(document.getElementById("terminal"));
|
||||
fitAddon.fit();
|
||||
xterm.options.fontFamily = "'Fira mono', 'Source code pro', ui-monospace, 'Cascadia Mono', 'Segoe UI Mono', 'Ubuntu Mono', 'Roboto Mono', Menlo, Monaco, Consolas, monospace";
|
||||
|
||||
const { master, slave } = openpty();
|
||||
|
||||
termios = slave.ioctl("TCGETS");
|
||||
termios.iflag &= ~(/*IGNBRK | BRKINT | PARMRK |*/ ISTRIP | INLCR | IGNCR | ICRNL | IXON);
|
||||
termios.oflag &= ~(OPOST);
|
||||
termios.lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
|
||||
//termios.cflag &= ~(CSIZE | PARENB);
|
||||
//termios.cflag |= CS8;
|
||||
slave.ioctl("TCSETS", new Termios(termios.iflag, termios.oflag, termios.cflag, termios.lflag, termios.cc));
|
||||
xterm.loadAddon(master);
|
||||
slave.write('\x1B[1;32mLoading Haskell WASM REPL. Please wait for ~10 seconds.\x1B[0m\n');
|
||||
slave.write('\x1B[1E');
|
||||
|
||||
const worker = new Worker("./worker.js"+location.search);
|
||||
var workerImage = location.origin + "/out.wasm";
|
||||
worker.postMessage({type: "init", imagename: workerImage});
|
||||
new TtyServer(slave).start(worker, null);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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<Event>*/) {
|
||||
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<Subscription>*/ {
|
||||
let subscriptions = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
subscriptions.push(Subscription.read_bytes(view, ptr + 48 * i));
|
||||
}
|
||||
return subscriptions;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue