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