├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── README.md ├── examples ├── 1-directly-in-the-browser │ ├── README.md │ ├── binaries │ ├── index.html │ ├── package-lock.json │ └── package.json ├── 2-nodejs-with-parcel │ ├── .parcelrc │ ├── .proxyrc.json │ ├── README.md │ ├── index.html │ ├── index.js │ ├── package-lock.json │ └── package.json ├── 3-react-with-webpack │ ├── README.md │ ├── index.css │ ├── index.html │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── webpack.config.js │ └── xterm-for-react.js ├── README.md └── binaries │ ├── base32.lnk │ ├── base64.lnk │ ├── basename.lnk │ ├── cat.lnk │ ├── coreutils.wasm │ ├── dirname.lnk │ ├── echo.lnk │ ├── env.lnk │ ├── ls.lnk │ ├── mkdir.lnk │ ├── mv.lnk │ ├── openssl.js │ ├── openssl.wasm │ ├── printf.lnk │ ├── pwd.lnk │ ├── sum.lnk │ └── wc.lnk ├── package-lock.json ├── package.json ├── src ├── History.js ├── LineBuffer.js ├── WapmFetchUtil.js ├── WasmWebTerm.js ├── WasmWebTerm.md ├── runnables │ ├── EmscriptenRunnable.js │ └── WasmerRunnable.js └── runners │ ├── ImportInjectedModules.js │ ├── WasmRunner.js │ └── WasmWorker.js ├── webpack.config.js ├── webterm.bundle.js ├── webterm.bundle.js.map └── worker.loader.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist 4 | .DS_Store 5 | .parcel-cache 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .github 3 | .git 4 | examples/ 5 | webterm.bundle.js* 6 | *.md 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": false, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "always", 11 | "proseWrap": "always", 12 | "endOfLine": "lf", 13 | "embeddedLanguageFormatting": "off" 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

WebAssembly WebTerm

4 | 🚀 Live Demo    5 | ⚛️ React Example    6 | 🔐 OpenSSL 7 |
8 |   9 | 10 | Run your [WebAssembly](https://webassembly.org) binaries on a terminal/tty emulation in your browser. [Emscripten](https://emscripten.org) and [WASI](https://github.com/WebAssembly/WASI) are supported. This project is developed as an addon for [xterm.js](https://github.com/xtermjs/xterm.js) v4, so you can easily use it in your own projects. 11 | 12 | > It originated from the [CrypTool project](https://www.cryptool.org) in 2022 for [running OpenSSL v3 in a browser](https://github.com/cryptool-org/openssl-webterm). 13 | 14 | Please note that xterm.js and this addon need a browser to run. 15 | 16 | 17 | 18 | 19 | ----- 20 | 21 | 22 | ## Readme Contents 23 | 24 | * [Installation](#installation) and [Usage](#usage) (via [`script` tag](#variant-1-load-via-plain-js-script-tag), [Node.js](#variant-2-import-as-nodejs-module-and-use-a-web-bundler), or [React](#variant-3-using-react-and-a-web-bundler)) 25 | * [Binaries](#binaries) ([Predelivery](#predelivering-binaries), [Compiling C/C++](#compiling-cc-to-wasm-binaries), and [Compiling Rust](#compiling-rust-to-wasm-binaries)) 26 | * [Internal workings](#internal-procedure-flow), [JS commands](#defining-custom-js-commands), and [`WasmWebTerm.js` Code API](#wasmwebtermjs-code-api) 27 | * [Contributing](#contributing) and [License](#license) 28 | 29 | 30 | ----- 31 | 32 | 33 | ## Installation 34 | 35 | First, [install Node.js and npm](https://nodejs.org). Then install xterm.js and wasm-webterm: 36 | 37 | ```shell 38 | npm install xterm cryptool-org/wasm-webterm --save 39 | ``` 40 | 41 | ## Usage 42 | 43 | JavaScript can be written for browsers or nodes, but this addon needs a browser to run (or at least a DOM and Workers or a `window` object). So if you use Node.js, you have to also use a web bundler like [Webpack](https://webpack.js.org) or [Parcel](https://parceljs.org). Using plain JS does not require a bundler. 44 | 45 | > Please note: To make use of WebWorkers you will need to configure your server or web bundler to use [custom HTTPS headers for cross-origin isolation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements). You can find an [example using Webpack in the examples folder](./examples/3-react-with-webpack/webpack.config.js#L14-L18). 46 | 47 | Choose the variant that works best for your existing setup: 48 | 49 | 50 | ### Variant 1: Load via plain JS ` 60 | 61 | 62 | 63 | 64 | 65 |
66 | 67 | 72 | 73 | 77 | 78 | 79 | ``` 80 | 81 | > Please note that the plain JS version uses `new WasmWebTerm.default()` \[containing **.default**] instead of just `new WasmWebTerm()` like in the Node.js examples. 82 | 83 | 84 | ### Variant 2: Import as Node.js module and use a web bundler 85 | 86 | If you are writing a Node.js module and use a web bundler to make it runnable in web browsers, here's how you could include this project: 87 | 88 | > You can also see [example 2 in the examples folder](./examples/2-nodejs-with-parcel). We used Parcel as an example, but any other bundler would work too. 89 | 90 | 1) Create a JS file (let's say `index.js`) 91 | 92 | ```js 93 | import { Terminal } from "xterm" 94 | import WasmWebTerm from "wasm-webterm" 95 | 96 | let term = new Terminal() 97 | term.loadAddon(new WasmWebTerm()) 98 | term.open(document.getElementById("terminal")) 99 | ``` 100 | 101 | 2) Create an HTML file (let's say `index.html`) 102 | 103 | ```html 104 | 105 | 106 | 107 | 108 | 109 |
110 | 111 | 112 | 116 | 117 | 118 | ``` 119 | 120 | 3) Use a web bundler to make it run in a browser 121 | 122 | ```shell 123 | npm install -g parcel-bundler 124 | parcel index.html 125 | ``` 126 | 127 | 128 | ### Variant 3: Using React and a web bundler 129 | 130 | If you are using React, [example 3 in the examples folder](./examples/3-react-with-webpack) includes a React wrapper for xterm.js that was taken from [xterm-for-react](https://github.com/robert-harbison/xterm-for-react). We can use this to pass our addon. 131 | 132 | The following code is not complete (you'd also need an HTML spawnpoint and a web bundler like Webpack) and we recommend to [see the React example](./examples/3-react-with-webpack). 133 | 134 | ```js 135 | import ReactDOM from "react-dom" 136 | import XTerm from "./examples/3-react-with-webpack/xterm-for-react" 137 | import WasmWebTerm from "wasm-webterm" 138 | 139 | ReactDOM.render(, 140 | document.getElementById("terminal")) 141 | ``` 142 | 143 | 144 | ----- 145 | 146 | 147 | ## Binaries 148 | 149 | This addon executes [WebAssembly](https://en.wikipedia.org/wiki/WebAssembly) binaries. They are compiled from native languages like C, C++, Rust, etc. 150 | 151 | WebAssembly binaries are files ending on `.wasm` and can either be [predelivered by you](#predelivering-binaries) (shipping them with your application) or added live via drag and drop by users. If no binary was found locally, [wapm.io](https://wapm.io/explore) is fetched. 152 | 153 | 154 |
155 | What is a runtime and why do we need it? 156 | 157 | > "WebAssembly is an assembly language for a conceptual machine, not a physical one. This is why it can be run across a variety of different machine architectures." [(source)](https://hacks.mozilla.org/2019/03/standardizing-wasi-a-webassembly-system-interface/) 158 | 159 | To run programs intended to run in an OS like Linux, the "machine architecture" (your browser which is running JS) needs to initialize a runtime environment. It provides a virtual memory filesystem, handles system-calls, etc. 160 | 161 | When using [WASI](https://github.com/WebAssembly/WASI) (a standard) this is handled by [WASI from `wasmer-js` v0.12](https://github.com/wasmerio/wasmer-js/tree/0.x/packages/wasi). You can alternatively use compilers like Emscripten, which will generate a specific `.js` file containing the JS runtime for your wasm binary. 162 | 163 | > If you provide a `.js` file with the same name than your `.wasm` file (for example drop or ship `test.wasm` and `test.js` together), the `.wasm` binary will be interpreted as compiled with Emscripten and use the `.js` file as its runtime. If you just drop a `.wasm` file, it's interpreted as WASI. 164 | 165 |
166 | 167 | 168 | ### Predelivering binaries 169 | 170 | When you host your webterm instance somewhere, you might want to deliver some precompiled wasm binaries for your users to use. For example, [we compiled OpenSSL with Emscripten to run it in the webterm](https://github.com/cryptool-org/openssl-webterm). 171 | 172 | [See below](#compiling-cc-to-wasm-binaries) how to compile them. Then copy your binaries (`.wasm` and optionally `.js` files) into a folder, let's say `./binaries`. Make sure, that your web bundler (or however you're serving your project) also delivers these binaries, so that they're available when running the webterm. [We used Webpack's CopyPlugin in our React example](./examples/3-react-with-webpack/webpack.config.js#L34-L36). 173 | 174 | Then pass their path to the [`WasmWebTerm`](./src/WasmWebTerm.js) instance: 175 | 176 | ```js 177 | let wasmterm = new WasmWebTerm("./binaries") 178 | ``` 179 | 180 | When executing a command on the webterm, it will fetch `/.wasm` and validate if it's WebAssembly. So make sure, that the file name of your wasm binary matches the command name. If it's available, it'll also try to fetch `/.js` and thereby determine if WASI or Emscripten. 181 | 182 | 183 | ### Compiling C/C++ to `.wasm` binaries 184 | 185 | C or C++ code can be compiled to WebAssembly using [Emscripten](https://emscripten.org/docs/compiling/Building-Projects.html) or a [WASI compliant](https://github.com/WebAssembly/WASI) compiler like [WASI CC](https://github.com/wasienv/wasienv). 186 | 187 | In both following examples we will use this little C program and put it in a file named `test.c`. 188 | 189 | ```c 190 | #include 191 | 192 | int main() 193 | { 194 | char name[200]; 195 | fgets(name, 200, stdin); 196 | printf("You entered: %s", name); 197 | return 0; 198 | } 199 | ``` 200 | 201 | #### Example 1: Compile with Emscripten 202 | 203 | First, [install the Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html). It supplies `emcc` and tools like `emconfigure` and `emmake` for [building projects](https://emscripten.org/docs/compiling/Building-Projects.html). 204 | 205 | Running the following command will create two files: `test.wasm` (containing the WebAssembly binary) and `test.js` (containing a JS runtime for that specific wasm binary). The flags are used to configure the JS runtime: 206 | 207 | ```shell 208 | $ emcc test.c -o test.js -s EXPORT_NAME='EmscrJSR_test' -s ENVIRONMENT=web,worker -s FILESYSTEM=1 -s MODULARIZE=1 -s EXPORTED_RUNTIME_METHODS=callMain,FS,TTY -s INVOKE_RUN=0 -s EXIT_RUNTIME=1 -s EXPORT_ES6=0 -s USE_ES6_IMPORT_META=0 -s ALLOW_MEMORY_GROWTH=1 209 | ``` 210 | 211 |
212 | Explain these flags to me 213 | 214 | You can also use other Emscripten flags, as long as they don't interfere with the flags we've used here. These are essential. Here's what they mean: 215 | 216 | | Flag | Value | Description | 217 | |---------------------------|---------------------------|--------------------------------------------------------| 218 | | EXPORT_NAME | EmscrJSR_\ | FIXED name for Module, needs to match exactly to work! | 219 | | ENVIRONMENT | web,worker | Specifies we don't need Node.js (only web and worker) | 220 | | FILESYSTEM | 1 | Make sure Emscripten inits a memory filesystem (MemFS) | 221 | | MODULARIZE | 1 | Use a Module factory so we can create custom instances | 222 | | EXPORTED_RUNTIME_METHODS | callMain,FS,TTY | Export Filesystem, Teletypewriter, and our main method | 223 | | INVOKE_RUN | 0 | Do not run immediatly when instanciated (but manually) | 224 | | EXIT_RUNTIME | 1 | Exit JS runtime after wasm, will be re-init by webterm | 225 | | EXPORT_ES6 | 0 | Do not export as ES6 module so we can load in browser | 226 | | USE_ES6_IMPORT_META | 0 | Also do not import via ES6 to easily run in a browser | 227 | | ALLOW_MEMORY_GROWTH | 1 | Allow the memory to grow (allocate more memory space) | 228 | 229 | > ℹ️ The _fixed Emscripten Module_ name is a todo! If you have ideas for an elegant solution, please let us now :) 230 | 231 |
232 | 233 | Then copy the created files `test.wasm` and `test.js` into your predelivery folder or drag&drop them into the terminal window. You can now execute the command "test" in the terminal and it should ask you for input. 234 | 235 | #### Example 2: Compile with WASI CC 236 | 237 | First, [install wasienv](https://github.com/wasienv/wasienv). It includes `wasicc` and tools like `wasiconfigure` and `wasimake`. 238 | 239 | You can then compile `test.c` with the following line: 240 | 241 | ```shell 242 | $ wasicc test.c -o test.wasm 243 | ``` 244 | 245 | > There is no need for lots of flags here, because WASI is a standard interface and uses a standardized JS runtime for all binaries. 246 | 247 | Then copy the created file `test.wasm` into your predelivery folder or drag&drop it into the terminal window. You can now execute the command "test" in the terminal and it should ask you for input. 248 | 249 | 250 | ### Compiling Rust to `.wasm` binaries 251 | 252 | Rust code can be compiled to target `wasm32-wasi` which can be executed by this addon. You can either compile it directly with `rustc` or by using Rust's build tool `cargo`. 253 | 254 | If you haven't already, [install Rust](https://www.rust-lang.org/tools/install). Then install the `wasm32-wasi` target: 255 | 256 | ```shell 257 | $ rustup target add wasm32-wasi 258 | ``` 259 | 260 | #### Example 1: Using `rustc` 261 | 262 | Take some Rust source code, let's say in a file named `test.rs` 263 | 264 | ```rust 265 | fn main() { 266 | println!("Hello, world!"); 267 | } 268 | ``` 269 | 270 | and compile it with 271 | 272 | ```shell 273 | $ rustc test.rs --target wasm32-wasi 274 | ``` 275 | 276 | Then copy the created file `test.wasm` into your predelivery folder or drag&drop it into the terminal window. You can now execute the command "test" in the terminal and it should print `Hello, world!` to you. 277 | 278 | #### Example 2: Using `cargo` 279 | 280 | Create a new project 281 | 282 | ```shell 283 | $ cargo new 284 | $ cd 285 | ``` 286 | 287 | and build it to `wasm32-wasi` 288 | 289 | ```shell 290 | $ cargo build --target=wasm32-wasi 291 | ``` 292 | 293 | You should find the binary `.wasm` in the folder `/target/wasm32-wasi/debug`. 294 | 295 | Copy it into your predelivery folder or drag&drop it into the terminal window. You can now execute the command "\" in the terminal. 296 | 297 | 298 | ----- 299 | 300 | 301 | ## Internal procedure flow 302 | 303 | 304 | 305 | When a user visits your page, it loads xterm.js and attaches our addon. [See the upper code examples](#variant-2-import-as-nodejs-module-and-use-a-web-bundler). That calls the xterm.js life cycle method [`activate(xterm)`](./src/WasmWebTerm.md#async-activatexterm) in [`WasmWebTerm.js`](./src/WasmWebTerm.js) which starts the [REPL](#async-repl). 306 | 307 | The REPL waits for the user to enter a line (any string, usually commands) into the terminal. This line is then evaluated by [`runLine(line)`](./src/WasmWebTerm.md#async-runlineline). If there is a predefined JS command, it'll execute it. If not, it'll delegate to [`runWasmCommand(..)`](./src/WasmWebTerm.md#runwasmcommandprogramname-argv-stdinpreset-onfinishcallback) (or [`runWasmCommandHeadless(..)`](./src/WasmWebTerm.md#runwasmcommandheadlessprogramname-argv-stdinpreset-onfinishcallback) when piping). 308 | 309 | This then calls [`_getOrFetchWasmModule(..)`](./src/WasmWebTerm.md#_getorfetchwasmmoduleprogramname). It will search for a WebAssembly binary with the name of the command in the [predelivery folder](#predelivering-binaries). If none is found, it'll fetch [wapm.io](https://wapm.io/explore). 310 | 311 | The binary will then be passed to an instance of [`WasmRunner`](./src/runners/WasmRunner.js). If it receives both a wasm binary and a JS runtime, it'll instanciate an [`EmscrWasmRunnable`](./src/runnables/EmscriptenRunnable.js). If it only received a wasm binary, it'll instanciate a [`WasmerRunnable`](./src/runnables/WasmerRunnable.js). Both runnables setup the runtime required for the wasm execution and start the execution. 312 | 313 | > If WebWorker support is available (including [SharedArrayBuffer](https://caniuse.com/sharedarraybuffer)s and [Atomics](https://caniuse.com/mdn-javascript_builtins_atomics)), this will be wrapped into a [Worker thread](https://en.wikipedia.org/wiki/Web_worker) (see [`WasmWorker.js`](./src/runners/WasmWorker.js)) using [Comlink](https://github.com/GoogleChromeLabs/comlink). This is done using a [Blob](https://en.wikipedia.org/wiki/Binary_large_object) instead of delivering a separate Worker JS file: When [importing `WasmWorker.js`](./src/WasmWebTerm.js#L6), Webpack will prebuild/bundle all its dependencies and return it as `"asset/source"` (plain text) instead of a instantiable class. This is done using a [Webpack loader](./worker.loader.js). 314 | 315 | Communication between the `WasmRunner` and the xterm.js window is done trough [Comlink proxy callbacks](https://github.com/GoogleChromeLabs/comlink#callbacks), as they might be on different threads. For example, if the wasm binary asks for Stdin (while running on the worker thread), it'll be paused, the Comlink proxy [`_stdinProxy`](./src/WasmWebTerm.md#_stdinproxymessage) is called, and the execution resumes after the proxy has finished. 316 | 317 | > This pausing on the worker thread is done by using Atomics. That's why we rely on that browser support. The fallback (prompts) pauses the browser main thread by calling `window.prompt()`, which also blocks execution. 318 | 319 | When the execution has finished, the respective `onFinish(..)` callback is called and the REPL starts again. 320 | 321 | 322 | ----- 323 | 324 | 325 | ## [`WasmWebTerm.js`](./src/WasmWebTerm.js) Code API 326 | 327 | The code API of the main class `WasmWebterm` is documented in [src/WasmWebTerm.md](./src/WasmWebTerm.md). 328 | 329 | 330 | ----- 331 | 332 | 333 | ## Defining custom JS commands 334 | 335 | In addition to running WebAssembly, you can also run JS commands on the terminal. You can register them with [`registerJsCommand(name, callback)`](./src/WasmWebTerm.md#registerjscommandname-callback-autocomplete). When typing `name` into the terminal, the `callback` function is called. 336 | 337 | The `callback` function will receive `argv` (array) and `stdinPreset` (string) as input parameters. Output can be `return`ed, `resolve()`d or `yield`ed. 338 | 339 | > todo: stderr and file system access are not implemented yet 340 | 341 | Simple `echo` examples: 342 | 343 | ```js 344 | wasmterm.registerJsCommand("echo1", (argv) => { 345 | return argv.join(" ") // sync and normal return 346 | }) 347 | 348 | wasmterm.registerJsCommand("echo2", async (argv) => { 349 | return argv.join(" ") // async function return 350 | }) 351 | 352 | wasmterm.registerJsCommand("echo3", async (argv) => { 353 | return new Promise(resolve => resolve(argv.join(" "))) // promise resolve() 354 | }) 355 | 356 | wasmterm.registerJsCommand("echo4", async function*(argv) { 357 | for(const char of argv.join(" ")) yield char // generator yield 358 | }) 359 | ``` 360 | 361 | 362 | ----- 363 | 364 | 365 | ## Contributing 366 | 367 | Any contributions are greatly appreciated. If you have a suggestion that would make this better, please open an issue or fork the repository and create a pull request. 368 | 369 | 370 | ## License 371 | 372 | Distributed under the [`Apache-2.0`](https://www.apache.org/licenses/LICENSE-2.0) License. See [`LICENSE`](./LICENSE) for more information. 373 | -------------------------------------------------------------------------------- /examples/1-directly-in-the-browser/README.md: -------------------------------------------------------------------------------- 1 | # Example: Run `wasm-webterm` directly in the browser with `http-server` 2 | 3 | This example does not use a bundler but includes xterm.js and the prebundled file `webterm.bundle.js` directly. It uses `http-server` as a static HTTP server. You could also use this example with Apache or Nginx etc. 4 | 5 | Run the following commands **inside of this folder** to execute. 6 | 7 | ``` 8 | npm install 9 | ``` 10 | 11 | ``` 12 | npm run http-server 13 | ``` 14 | 15 | You can now visit http://localhost:8080/index.html 16 | 17 | 18 | ----- 19 | 20 | 21 | ## Notes 22 | 23 | > Because `http-server` sadly does not support serving multiple directories yet (there's an open pull request though), we used a symbolic link to link the binaries into this folder. This may not work on Windows. 24 | 25 | > You could also set the required HTTPS headers to enable web workers by generating an SSL certificate (plus a key) and extending the `http-server` command with `--ssl --cors='Cross-Origin-Embedder-Policy:require-corp,Cross-Origin-Opener-Policy:same-origin'` 26 | -------------------------------------------------------------------------------- /examples/1-directly-in-the-browser/binaries: -------------------------------------------------------------------------------- 1 | ../binaries -------------------------------------------------------------------------------- /examples/1-directly-in-the-browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 19 | 20 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/1-directly-in-the-browser/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm-webterm-plainjs-example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "wasm-webterm-plainjs-example", 9 | "version": "1.0.0", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "wasm-webterm": "file:../..", 13 | "xterm": "<=4.19.0" 14 | }, 15 | "devDependencies": { 16 | "http-server": "^14.1.1" 17 | } 18 | }, 19 | "../..": { 20 | "version": "0.0.8", 21 | "license": "Apache-2.0", 22 | "dependencies": { 23 | "@wasmer/wasi": "^0.12.0", 24 | "@wasmer/wasm-transformer": "^0.12.0", 25 | "@wasmer/wasmfs": "^0.12.0", 26 | "comlink": "^4.4.1", 27 | "js-untar": "^2.0.0", 28 | "local-echo": "github:wavesoft/local-echo", 29 | "pako": "^2.1.0", 30 | "shell-quote": "^1.8.1", 31 | "xterm-addon-fit": "^0.5.0" 32 | }, 33 | "devDependencies": { 34 | "memfs": "^4.9.2", 35 | "prettier": "^3.3.0", 36 | "process": "^0.11.10", 37 | "webpack": "^5.91.0", 38 | "webpack-cli": "^5.1.4" 39 | }, 40 | "peerDependencies": { 41 | "xterm": ">4.9.0 <=4.19.0" 42 | } 43 | }, 44 | "node_modules/ansi-styles": { 45 | "version": "4.3.0", 46 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 47 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 48 | "dev": true, 49 | "license": "MIT", 50 | "dependencies": { 51 | "color-convert": "^2.0.1" 52 | }, 53 | "engines": { 54 | "node": ">=8" 55 | }, 56 | "funding": { 57 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 58 | } 59 | }, 60 | "node_modules/async": { 61 | "version": "2.6.4", 62 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", 63 | "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", 64 | "dev": true, 65 | "license": "MIT", 66 | "dependencies": { 67 | "lodash": "^4.17.14" 68 | } 69 | }, 70 | "node_modules/basic-auth": { 71 | "version": "2.0.1", 72 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", 73 | "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", 74 | "dev": true, 75 | "license": "MIT", 76 | "dependencies": { 77 | "safe-buffer": "5.1.2" 78 | }, 79 | "engines": { 80 | "node": ">= 0.8" 81 | } 82 | }, 83 | "node_modules/call-bind": { 84 | "version": "1.0.7", 85 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 86 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 87 | "dev": true, 88 | "license": "MIT", 89 | "dependencies": { 90 | "es-define-property": "^1.0.0", 91 | "es-errors": "^1.3.0", 92 | "function-bind": "^1.1.2", 93 | "get-intrinsic": "^1.2.4", 94 | "set-function-length": "^1.2.1" 95 | }, 96 | "engines": { 97 | "node": ">= 0.4" 98 | }, 99 | "funding": { 100 | "url": "https://github.com/sponsors/ljharb" 101 | } 102 | }, 103 | "node_modules/chalk": { 104 | "version": "4.1.2", 105 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 106 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 107 | "dev": true, 108 | "license": "MIT", 109 | "dependencies": { 110 | "ansi-styles": "^4.1.0", 111 | "supports-color": "^7.1.0" 112 | }, 113 | "engines": { 114 | "node": ">=10" 115 | }, 116 | "funding": { 117 | "url": "https://github.com/chalk/chalk?sponsor=1" 118 | } 119 | }, 120 | "node_modules/color-convert": { 121 | "version": "2.0.1", 122 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 123 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 124 | "dev": true, 125 | "license": "MIT", 126 | "dependencies": { 127 | "color-name": "~1.1.4" 128 | }, 129 | "engines": { 130 | "node": ">=7.0.0" 131 | } 132 | }, 133 | "node_modules/color-name": { 134 | "version": "1.1.4", 135 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 136 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 137 | "dev": true, 138 | "license": "MIT" 139 | }, 140 | "node_modules/corser": { 141 | "version": "2.0.1", 142 | "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", 143 | "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", 144 | "dev": true, 145 | "license": "MIT", 146 | "engines": { 147 | "node": ">= 0.4.0" 148 | } 149 | }, 150 | "node_modules/debug": { 151 | "version": "3.2.7", 152 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 153 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 154 | "dev": true, 155 | "license": "MIT", 156 | "dependencies": { 157 | "ms": "^2.1.1" 158 | } 159 | }, 160 | "node_modules/define-data-property": { 161 | "version": "1.1.4", 162 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 163 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 164 | "dev": true, 165 | "license": "MIT", 166 | "dependencies": { 167 | "es-define-property": "^1.0.0", 168 | "es-errors": "^1.3.0", 169 | "gopd": "^1.0.1" 170 | }, 171 | "engines": { 172 | "node": ">= 0.4" 173 | }, 174 | "funding": { 175 | "url": "https://github.com/sponsors/ljharb" 176 | } 177 | }, 178 | "node_modules/es-define-property": { 179 | "version": "1.0.0", 180 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 181 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 182 | "dev": true, 183 | "license": "MIT", 184 | "dependencies": { 185 | "get-intrinsic": "^1.2.4" 186 | }, 187 | "engines": { 188 | "node": ">= 0.4" 189 | } 190 | }, 191 | "node_modules/es-errors": { 192 | "version": "1.3.0", 193 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 194 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 195 | "dev": true, 196 | "license": "MIT", 197 | "engines": { 198 | "node": ">= 0.4" 199 | } 200 | }, 201 | "node_modules/eventemitter3": { 202 | "version": "4.0.7", 203 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", 204 | "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", 205 | "dev": true, 206 | "license": "MIT" 207 | }, 208 | "node_modules/follow-redirects": { 209 | "version": "1.15.9", 210 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", 211 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 212 | "dev": true, 213 | "funding": [ 214 | { 215 | "type": "individual", 216 | "url": "https://github.com/sponsors/RubenVerborgh" 217 | } 218 | ], 219 | "license": "MIT", 220 | "engines": { 221 | "node": ">=4.0" 222 | }, 223 | "peerDependenciesMeta": { 224 | "debug": { 225 | "optional": true 226 | } 227 | } 228 | }, 229 | "node_modules/function-bind": { 230 | "version": "1.1.2", 231 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 232 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 233 | "dev": true, 234 | "license": "MIT", 235 | "funding": { 236 | "url": "https://github.com/sponsors/ljharb" 237 | } 238 | }, 239 | "node_modules/get-intrinsic": { 240 | "version": "1.2.4", 241 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 242 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 243 | "dev": true, 244 | "license": "MIT", 245 | "dependencies": { 246 | "es-errors": "^1.3.0", 247 | "function-bind": "^1.1.2", 248 | "has-proto": "^1.0.1", 249 | "has-symbols": "^1.0.3", 250 | "hasown": "^2.0.0" 251 | }, 252 | "engines": { 253 | "node": ">= 0.4" 254 | }, 255 | "funding": { 256 | "url": "https://github.com/sponsors/ljharb" 257 | } 258 | }, 259 | "node_modules/gopd": { 260 | "version": "1.0.1", 261 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 262 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 263 | "dev": true, 264 | "license": "MIT", 265 | "dependencies": { 266 | "get-intrinsic": "^1.1.3" 267 | }, 268 | "funding": { 269 | "url": "https://github.com/sponsors/ljharb" 270 | } 271 | }, 272 | "node_modules/has-flag": { 273 | "version": "4.0.0", 274 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 275 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 276 | "dev": true, 277 | "license": "MIT", 278 | "engines": { 279 | "node": ">=8" 280 | } 281 | }, 282 | "node_modules/has-property-descriptors": { 283 | "version": "1.0.2", 284 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 285 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 286 | "dev": true, 287 | "license": "MIT", 288 | "dependencies": { 289 | "es-define-property": "^1.0.0" 290 | }, 291 | "funding": { 292 | "url": "https://github.com/sponsors/ljharb" 293 | } 294 | }, 295 | "node_modules/has-proto": { 296 | "version": "1.0.3", 297 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 298 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 299 | "dev": true, 300 | "license": "MIT", 301 | "engines": { 302 | "node": ">= 0.4" 303 | }, 304 | "funding": { 305 | "url": "https://github.com/sponsors/ljharb" 306 | } 307 | }, 308 | "node_modules/has-symbols": { 309 | "version": "1.0.3", 310 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 311 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 312 | "dev": true, 313 | "license": "MIT", 314 | "engines": { 315 | "node": ">= 0.4" 316 | }, 317 | "funding": { 318 | "url": "https://github.com/sponsors/ljharb" 319 | } 320 | }, 321 | "node_modules/hasown": { 322 | "version": "2.0.2", 323 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 324 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 325 | "dev": true, 326 | "license": "MIT", 327 | "dependencies": { 328 | "function-bind": "^1.1.2" 329 | }, 330 | "engines": { 331 | "node": ">= 0.4" 332 | } 333 | }, 334 | "node_modules/he": { 335 | "version": "1.2.0", 336 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 337 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 338 | "dev": true, 339 | "license": "MIT", 340 | "bin": { 341 | "he": "bin/he" 342 | } 343 | }, 344 | "node_modules/html-encoding-sniffer": { 345 | "version": "3.0.0", 346 | "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", 347 | "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", 348 | "dev": true, 349 | "license": "MIT", 350 | "dependencies": { 351 | "whatwg-encoding": "^2.0.0" 352 | }, 353 | "engines": { 354 | "node": ">=12" 355 | } 356 | }, 357 | "node_modules/http-proxy": { 358 | "version": "1.18.1", 359 | "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", 360 | "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", 361 | "dev": true, 362 | "license": "MIT", 363 | "dependencies": { 364 | "eventemitter3": "^4.0.0", 365 | "follow-redirects": "^1.0.0", 366 | "requires-port": "^1.0.0" 367 | }, 368 | "engines": { 369 | "node": ">=8.0.0" 370 | } 371 | }, 372 | "node_modules/http-server": { 373 | "version": "14.1.1", 374 | "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", 375 | "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", 376 | "dev": true, 377 | "license": "MIT", 378 | "dependencies": { 379 | "basic-auth": "^2.0.1", 380 | "chalk": "^4.1.2", 381 | "corser": "^2.0.1", 382 | "he": "^1.2.0", 383 | "html-encoding-sniffer": "^3.0.0", 384 | "http-proxy": "^1.18.1", 385 | "mime": "^1.6.0", 386 | "minimist": "^1.2.6", 387 | "opener": "^1.5.1", 388 | "portfinder": "^1.0.28", 389 | "secure-compare": "3.0.1", 390 | "union": "~0.5.0", 391 | "url-join": "^4.0.1" 392 | }, 393 | "bin": { 394 | "http-server": "bin/http-server" 395 | }, 396 | "engines": { 397 | "node": ">=12" 398 | } 399 | }, 400 | "node_modules/iconv-lite": { 401 | "version": "0.6.3", 402 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 403 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 404 | "dev": true, 405 | "license": "MIT", 406 | "dependencies": { 407 | "safer-buffer": ">= 2.1.2 < 3.0.0" 408 | }, 409 | "engines": { 410 | "node": ">=0.10.0" 411 | } 412 | }, 413 | "node_modules/lodash": { 414 | "version": "4.17.21", 415 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 416 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", 417 | "dev": true, 418 | "license": "MIT" 419 | }, 420 | "node_modules/mime": { 421 | "version": "1.6.0", 422 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 423 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 424 | "dev": true, 425 | "license": "MIT", 426 | "bin": { 427 | "mime": "cli.js" 428 | }, 429 | "engines": { 430 | "node": ">=4" 431 | } 432 | }, 433 | "node_modules/minimist": { 434 | "version": "1.2.8", 435 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 436 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 437 | "dev": true, 438 | "license": "MIT", 439 | "funding": { 440 | "url": "https://github.com/sponsors/ljharb" 441 | } 442 | }, 443 | "node_modules/mkdirp": { 444 | "version": "0.5.6", 445 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", 446 | "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", 447 | "dev": true, 448 | "license": "MIT", 449 | "dependencies": { 450 | "minimist": "^1.2.6" 451 | }, 452 | "bin": { 453 | "mkdirp": "bin/cmd.js" 454 | } 455 | }, 456 | "node_modules/ms": { 457 | "version": "2.1.3", 458 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 459 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 460 | "dev": true, 461 | "license": "MIT" 462 | }, 463 | "node_modules/object-inspect": { 464 | "version": "1.13.2", 465 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", 466 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", 467 | "dev": true, 468 | "license": "MIT", 469 | "engines": { 470 | "node": ">= 0.4" 471 | }, 472 | "funding": { 473 | "url": "https://github.com/sponsors/ljharb" 474 | } 475 | }, 476 | "node_modules/opener": { 477 | "version": "1.5.2", 478 | "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", 479 | "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", 480 | "dev": true, 481 | "license": "(WTFPL OR MIT)", 482 | "bin": { 483 | "opener": "bin/opener-bin.js" 484 | } 485 | }, 486 | "node_modules/portfinder": { 487 | "version": "1.0.32", 488 | "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", 489 | "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", 490 | "dev": true, 491 | "license": "MIT", 492 | "dependencies": { 493 | "async": "^2.6.4", 494 | "debug": "^3.2.7", 495 | "mkdirp": "^0.5.6" 496 | }, 497 | "engines": { 498 | "node": ">= 0.12.0" 499 | } 500 | }, 501 | "node_modules/qs": { 502 | "version": "6.13.0", 503 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 504 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 505 | "dev": true, 506 | "license": "BSD-3-Clause", 507 | "dependencies": { 508 | "side-channel": "^1.0.6" 509 | }, 510 | "engines": { 511 | "node": ">=0.6" 512 | }, 513 | "funding": { 514 | "url": "https://github.com/sponsors/ljharb" 515 | } 516 | }, 517 | "node_modules/requires-port": { 518 | "version": "1.0.0", 519 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", 520 | "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", 521 | "dev": true, 522 | "license": "MIT" 523 | }, 524 | "node_modules/safe-buffer": { 525 | "version": "5.1.2", 526 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 527 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 528 | "dev": true, 529 | "license": "MIT" 530 | }, 531 | "node_modules/safer-buffer": { 532 | "version": "2.1.2", 533 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 534 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 535 | "dev": true, 536 | "license": "MIT" 537 | }, 538 | "node_modules/secure-compare": { 539 | "version": "3.0.1", 540 | "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", 541 | "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", 542 | "dev": true, 543 | "license": "MIT" 544 | }, 545 | "node_modules/set-function-length": { 546 | "version": "1.2.2", 547 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 548 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 549 | "dev": true, 550 | "license": "MIT", 551 | "dependencies": { 552 | "define-data-property": "^1.1.4", 553 | "es-errors": "^1.3.0", 554 | "function-bind": "^1.1.2", 555 | "get-intrinsic": "^1.2.4", 556 | "gopd": "^1.0.1", 557 | "has-property-descriptors": "^1.0.2" 558 | }, 559 | "engines": { 560 | "node": ">= 0.4" 561 | } 562 | }, 563 | "node_modules/side-channel": { 564 | "version": "1.0.6", 565 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 566 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 567 | "dev": true, 568 | "license": "MIT", 569 | "dependencies": { 570 | "call-bind": "^1.0.7", 571 | "es-errors": "^1.3.0", 572 | "get-intrinsic": "^1.2.4", 573 | "object-inspect": "^1.13.1" 574 | }, 575 | "engines": { 576 | "node": ">= 0.4" 577 | }, 578 | "funding": { 579 | "url": "https://github.com/sponsors/ljharb" 580 | } 581 | }, 582 | "node_modules/supports-color": { 583 | "version": "7.2.0", 584 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 585 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 586 | "dev": true, 587 | "license": "MIT", 588 | "dependencies": { 589 | "has-flag": "^4.0.0" 590 | }, 591 | "engines": { 592 | "node": ">=8" 593 | } 594 | }, 595 | "node_modules/union": { 596 | "version": "0.5.0", 597 | "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", 598 | "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", 599 | "dev": true, 600 | "dependencies": { 601 | "qs": "^6.4.0" 602 | }, 603 | "engines": { 604 | "node": ">= 0.8.0" 605 | } 606 | }, 607 | "node_modules/url-join": { 608 | "version": "4.0.1", 609 | "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", 610 | "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", 611 | "dev": true, 612 | "license": "MIT" 613 | }, 614 | "node_modules/wasm-webterm": { 615 | "resolved": "../..", 616 | "link": true 617 | }, 618 | "node_modules/whatwg-encoding": { 619 | "version": "2.0.0", 620 | "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", 621 | "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", 622 | "dev": true, 623 | "license": "MIT", 624 | "dependencies": { 625 | "iconv-lite": "0.6.3" 626 | }, 627 | "engines": { 628 | "node": ">=12" 629 | } 630 | }, 631 | "node_modules/xterm": { 632 | "version": "4.19.0", 633 | "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.19.0.tgz", 634 | "integrity": "sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==", 635 | "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", 636 | "license": "MIT" 637 | } 638 | } 639 | } 640 | -------------------------------------------------------------------------------- /examples/1-directly-in-the-browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm-webterm-plainjs-example", 3 | "version": "1.0.0", 4 | "description": "Example for how to use wasm-webterm with plain JS in a browser", 5 | "scripts": { 6 | "http-server": "http-server ." 7 | }, 8 | "author": "cryptool.org", 9 | "license": "Apache-2.0", 10 | "dependencies": { 11 | "wasm-webterm": "file:../..", 12 | "xterm": "<=4.19.0" 13 | }, 14 | "devDependencies": { 15 | "http-server": "^14.1.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/2-nodejs-with-parcel/.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "transformers": { 4 | "*.lnk": ["@parcel/transformer-raw"], 5 | "*.wasm": ["@parcel/transformer-raw"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/2-nodejs-with-parcel/.proxyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "/": { 3 | "target": "https://localhost:1234", 4 | "headers": { 5 | "Cross-Origin-Embedder-Policy": "require-corp", 6 | "Cross-Origin-Opener-Policy": "same-origin" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/2-nodejs-with-parcel/README.md: -------------------------------------------------------------------------------- 1 | # Example: Run `wasm-webterm` with Node.js using Parcel 2 | 3 | This still requires a browser. Parcel will bundle the example for you. 4 | 5 | Run the following commands **inside of this folder** to execute. 6 | 7 | ``` 8 | npm install 9 | ``` 10 | 11 | ``` 12 | npm run parcel 13 | ``` 14 | 15 | You can now visit https://localhost:1234/index.html 16 | 17 | 18 | ----- 19 | 20 | 21 | ## Notes 22 | 23 | > You could theoretically also set the required HTTPS headers to make web workers work by using `.proxyrc.json`. But it unfortunately did not work for me while writing this. 24 | -------------------------------------------------------------------------------- /examples/2-nodejs-with-parcel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/2-nodejs-with-parcel/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example on how to use wasm-webterm with Node.js 3 | */ 4 | 5 | // import xterm.js 6 | import { Terminal } from "xterm" 7 | 8 | // import the prebundled webterm addon 9 | import WasmWebTerm from "wasm-webterm" 10 | 11 | // instantiate xterm.js 12 | let term = new Terminal() 13 | 14 | // load the wasm-webterm addon 15 | // and pass it the path to your wasm binaries (optionally) 16 | term.loadAddon(new WasmWebTerm("./binaries")) 17 | 18 | // spawn the terminal into index.html 19 | term.open(document.getElementById("terminal")) 20 | -------------------------------------------------------------------------------- /examples/2-nodejs-with-parcel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm-webterm-nodejs-example", 3 | "version": "1.0.0", 4 | "description": "Example for how to use wasm-webterm in Node.js with Parcel", 5 | "scripts": { 6 | "parcel": "parcel ./index.html ../binaries/**/* --https && rm -rf dist .cache ../../.parcel-cache" 7 | }, 8 | "author": "cryptool.org", 9 | "license": "Apache-2.0", 10 | "dependencies": { 11 | "wasm-webterm": "file:../..", 12 | "xterm": "<=4.19.0" 13 | }, 14 | "devDependencies": { 15 | "parcel": "^2.12.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/3-react-with-webpack/README.md: -------------------------------------------------------------------------------- 1 | # Example: Run `wasm-webterm` with React using Webpack 2 | 3 | This still requires a browser. Webpack will bundle the example for you. It uses the required HTTPS headers to enable web workers. 4 | 5 | Run the following commands **inside of this folder** to execute. 6 | 7 | ``` 8 | npm install 9 | ``` 10 | 11 | ``` 12 | npm run webpack 13 | ``` 14 | 15 | You can now visit https://localhost:4201 16 | -------------------------------------------------------------------------------- /examples/3-react-with-webpack/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .terminal.xterm { 7 | height: calc(100vh - 2rem); 8 | width: calc(100vw - 2rem); 9 | padding: 1rem; 10 | } 11 | -------------------------------------------------------------------------------- /examples/3-react-with-webpack/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/3-react-with-webpack/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | 4 | import XTerm from "./xterm-for-react" 5 | import WasmWebTerm from "wasm-webterm" 6 | 7 | // instantiate wasm-webterm addon 8 | let wasmterm = new WasmWebTerm("./binaries") 9 | 10 | // create component containing xterm and the addon 11 | class WebtermComponent extends React.Component { 12 | render() { 13 | return 17 | } 18 | } 19 | 20 | // load stylesheet 21 | require("./index.css") 22 | 23 | // initialize wasm webterm component 24 | ReactDOM.render(, document.getElementById("wasm-webterm")) 25 | -------------------------------------------------------------------------------- /examples/3-react-with-webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm-webterm-react", 3 | "version": "1.0.0", 4 | "description": "React app to launch wasm-webterm", 5 | "scripts": { 6 | "webpack": "webpack serve --mode development", 7 | "export": "rm -rf dist && webpack --mode production" 8 | }, 9 | "dependencies": { 10 | "@babel/core": "^7.25.2", 11 | "@babel/preset-react": "^7.24.7", 12 | "babel-loader": "^8.4.1", 13 | "copy-webpack-plugin": "^10.2.4", 14 | "css-loader": "^6.11.0", 15 | "html-webpack-plugin": "^5.6.0", 16 | "react": "^17.0.2", 17 | "react-dom": "^17.0.2", 18 | "style-loader": "^3.3.4", 19 | "wasm-webterm": "file:../..", 20 | "webpack": "^5.95.0", 21 | "webpack-cli": "^4.10.0", 22 | "webpack-dev-server": "^4.15.2", 23 | "xterm": "<=4.19.0" 24 | }, 25 | "babel": { 26 | "presets": [ 27 | "@babel/preset-react" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/3-react-with-webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require("html-webpack-plugin") 2 | const CopyWebpackPlugin = require("copy-webpack-plugin") 3 | 4 | module.exports = { 5 | entry: { 6 | "main": "./index.js" 7 | }, 8 | output: { 9 | path: __dirname + "/dist", 10 | filename: "[name].js" 11 | }, 12 | devServer: { 13 | port: 4201, 14 | headers: { 15 | "Cross-Origin-Embedder-Policy": "require-corp", 16 | "Cross-Origin-Opener-Policy": "same-origin" 17 | }, 18 | server: "https" 19 | }, 20 | module: { 21 | rules: [{ 22 | test: /\.(js|jsx)$/, 23 | exclude: ["/node_modules/", "/bin/"], 24 | use: ["babel-loader"] 25 | }, { 26 | test: /\.css$/, 27 | use: ["style-loader", "css-loader"] 28 | }] 29 | }, 30 | plugins: [ 31 | new HtmlWebPackPlugin({ 32 | template: "./index.html", inject: true 33 | }), 34 | new CopyWebpackPlugin({ patterns: [ 35 | { from: "../binaries", to: "binaries" } 36 | ]}) 37 | ], 38 | /* optimization: { 39 | concatenateModules: false, 40 | minimize: false 41 | } */ 42 | } 43 | -------------------------------------------------------------------------------- /examples/3-react-with-webpack/xterm-for-react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React wrapper for xterm.js 3 | * Taken from https://github.com/robert-harbison/xterm-for-react 4 | * 5 | * (included manually bc the original was limited to React v16) 6 | */ 7 | 8 | 9 | import * as React from 'react' 10 | 11 | import 'xterm/css/xterm.css' 12 | 13 | // We are using these as types. 14 | // eslint-disable-next-line no-unused-vars 15 | import { Terminal, ITerminalOptions, ITerminalAddon } from 'xterm' 16 | 17 | export default class Xterm extends React.Component { 18 | /** 19 | * The ref for the containing element. 20 | */ 21 | terminalRef 22 | 23 | /** 24 | * XTerm.js Terminal object. 25 | */ 26 | terminal // This is assigned in the setupTerminal() which is called from the constructor 27 | 28 | constructor(props) { 29 | super(props) 30 | 31 | this.terminalRef = React.createRef() 32 | 33 | // Bind Methods 34 | this.onData = this.onData.bind(this) 35 | this.onCursorMove = this.onCursorMove.bind(this) 36 | this.onKey = this.onKey.bind(this) 37 | this.onBinary = this.onBinary.bind(this) 38 | this.onLineFeed = this.onLineFeed.bind(this) 39 | this.onScroll = this.onScroll.bind(this) 40 | this.onSelectionChange = this.onSelectionChange.bind(this) 41 | this.onRender = this.onRender.bind(this) 42 | this.onResize = this.onResize.bind(this) 43 | this.onTitleChange = this.onTitleChange.bind(this) 44 | 45 | this.setupTerminal() 46 | } 47 | 48 | setupTerminal() { 49 | // Setup the XTerm terminal. 50 | this.terminal = new Terminal(this.props.options) 51 | 52 | // Load addons if the prop exists. 53 | if (this.props.addons) { 54 | this.props.addons.forEach((addon) => { 55 | this.terminal.loadAddon(addon) 56 | }) 57 | } 58 | 59 | // Create Listeners 60 | this.terminal.onBinary(this.onBinary) 61 | this.terminal.onCursorMove(this.onCursorMove) 62 | this.terminal.onData(this.onData) 63 | this.terminal.onKey(this.onKey) 64 | this.terminal.onLineFeed(this.onLineFeed) 65 | this.terminal.onScroll(this.onScroll) 66 | this.terminal.onSelectionChange(this.onSelectionChange) 67 | this.terminal.onRender(this.onRender) 68 | this.terminal.onResize(this.onResize) 69 | this.terminal.onTitleChange(this.onTitleChange) 70 | 71 | // Add Custom Key Event Handler 72 | if (this.props.customKeyEventHandler) { 73 | this.terminal.attachCustomKeyEventHandler(this.props.customKeyEventHandler) 74 | } 75 | } 76 | 77 | componentDidMount() { 78 | if (this.terminalRef.current) { 79 | // Creates the terminal within the container element. 80 | this.terminal.open(this.terminalRef.current) 81 | } 82 | } 83 | 84 | componentWillUnmount() { 85 | // When the component unmounts dispose of the terminal and all of its listeners. 86 | this.terminal.dispose() 87 | } 88 | 89 | onBinary(data) { 90 | if (this.props.onBinary) this.props.onBinary(data) 91 | } 92 | 93 | onCursorMove() { 94 | if (this.props.onCursorMove) this.props.onCursorMove() 95 | } 96 | 97 | onData(data) { 98 | if (this.props.onData) this.props.onData(data) 99 | } 100 | 101 | onKey(event) { 102 | if (this.props.onKey) this.props.onKey(event) 103 | } 104 | 105 | onLineFeed() { 106 | if (this.props.onLineFeed) this.props.onLineFeed() 107 | } 108 | 109 | onScroll(newPosition) { 110 | if (this.props.onScroll) this.props.onScroll(newPosition) 111 | } 112 | 113 | onSelectionChange() { 114 | if (this.props.onSelectionChange) this.props.onSelectionChange() 115 | } 116 | 117 | onRender(event) { 118 | if (this.props.onRender) this.props.onRender(event) 119 | } 120 | 121 | onResize(event) { 122 | if (this.props.onResize) this.props.onResize(event) 123 | } 124 | 125 | onTitleChange(newTitle) { 126 | if (this.props.onTitleChange) this.props.onTitleChange(newTitle) 127 | } 128 | 129 | render() { 130 | return
131 | } 132 | } 133 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This folder contains examples on how you could use `wasm-webterm` in your project. 4 | It also includes WebAssembly binaries for Coreutils and OpenSSL. 5 | 6 | Only the [webpack example (3)](./3-react-with-webpack) has enabled HTTPS headers, so it can use web workers. 7 | The examples [1 (directly with http-server)](./1-directly-in-the-browser) and [2 (parcel)](./2-nodejs-with-parcel) have instructions on how you could enable the HTTP headers yourself. 8 | 9 | -------------------------------------------------------------------------------- /examples/binaries/base32.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/base64.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/basename.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/cat.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/coreutils.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptool-org/wasm-webterm/7e6e7c7a32222a3898ace803f46e68352896da30/examples/binaries/coreutils.wasm -------------------------------------------------------------------------------- /examples/binaries/dirname.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/echo.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/env.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/ls.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/mkdir.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/mv.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/openssl.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptool-org/wasm-webterm/7e6e7c7a32222a3898ace803f46e68352896da30/examples/binaries/openssl.wasm -------------------------------------------------------------------------------- /examples/binaries/printf.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/pwd.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/sum.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /examples/binaries/wc.lnk: -------------------------------------------------------------------------------- 1 | coreutils -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm-webterm", 3 | "version": "0.0.11", 4 | "description": "xterm.js addon to run webassembly binaries (emscripten + wasmer)", 5 | "files": [ 6 | "webterm.bundle.js", 7 | "webterm.bundle.js.map" 8 | ], 9 | "main": "webterm.bundle.js", 10 | "scripts": { 11 | "build": "webpack --mode production && rm -rf ./*.LICENSE.*", 12 | "dev": "webpack watch --mode development" 13 | }, 14 | "keywords": [ 15 | "xterm.js", 16 | "emscripten", 17 | "wasmer", 18 | "wasi", 19 | "wasm", 20 | "webassembly", 21 | "js", 22 | "javascript" 23 | ], 24 | "author": "cryptool.org", 25 | "license": "Apache-2.0", 26 | "dependencies": { 27 | "@wasmer/wasi": "^0.12.0", 28 | "@wasmer/wasm-transformer": "^0.12.0", 29 | "@wasmer/wasmfs": "^0.12.0", 30 | "comlink": "^4.4.2", 31 | "js-untar": "^2.0.0", 32 | "local-echo": "github:wavesoft/local-echo", 33 | "pako": "^2.1.0", 34 | "shell-quote": "^1.8.2", 35 | "xterm-addon-fit": "^0.5.0" 36 | }, 37 | "devDependencies": { 38 | "memfs": "^4.17.2", 39 | "prettier": "^3.5.3", 40 | "process": "^0.11.10", 41 | "webpack": "^5.99.9", 42 | "webpack-cli": "^6.0.1" 43 | }, 44 | "peerDependencies": { 45 | "xterm": ">4.9.0 <=4.19.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/History.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Shell history controller for `size` most-recent entries. 3 | */ 4 | export default class History { 5 | #size 6 | #entries = [] 7 | #cursor = 0 8 | 9 | constructor(size) { 10 | this.#size = size 11 | } 12 | 13 | get size() { 14 | return this.#size 15 | } 16 | 17 | // push an entry and maintain buffer size (drop oldest entry) 18 | push(entry) { 19 | // skip empty entries 20 | if (entry.trim() === "") return 21 | // skip duplicate entries 22 | const last = this.#entries[this.#entries.length - 1] 23 | if (entry === last) return 24 | 25 | this.#entries.push(entry) 26 | if (this.#entries.length > this.size) { 27 | this.#entries.shift() // drop oldest entry to keep size 28 | } 29 | 30 | this.#cursor = this.#entries.length 31 | } 32 | 33 | // rewind cursor to the latest entry 34 | rewind() { 35 | this.#cursor = this.#entries.length 36 | } 37 | 38 | // move cursor to the previous entry and return it 39 | getPrevious() { 40 | this.#cursor = Math.max(0, this.#cursor - 1) 41 | return this.#entries[this.#cursor] 42 | } 43 | 44 | // move cursor to the next entry and return it 45 | getNext() { 46 | this.#cursor = Math.min(this.#entries.length, this.#cursor + 1) 47 | return this.#entries[this.#cursor] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/LineBuffer.js: -------------------------------------------------------------------------------- 1 | export default class LineBuffer { 2 | #outputFn 3 | #buffer 4 | 5 | constructor(outputFn) { 6 | if (typeof outputFn !== "function") { 7 | throw new ValueError( 8 | `'outputFn' must be a function but is '${typeof outputFn}'!` 9 | ) 10 | } 11 | this.#outputFn = outputFn 12 | this.#buffer = "" 13 | } 14 | 15 | write(value) { 16 | // numbers are interpreted as char codes -> convert to string 17 | if (typeof value == "number") value = String.fromCharCode(value) 18 | 19 | this.#buffer += value 20 | 21 | let idx = this.#buffer.indexOf("\n") 22 | while (idx >= 0) { 23 | this.#outputFn(this.#buffer.slice(0, idx + 1)) 24 | this.#buffer = this.#buffer.slice(idx + 1) 25 | idx = this.#buffer.indexOf("\n") 26 | } 27 | } 28 | 29 | flush() { 30 | if (this.#buffer.length > 0) { 31 | this.#outputFn(this.#buffer) 32 | this.#buffer = "" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/WapmFetchUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Script to fetch wasmer binaries from wapm.io -- Code taken from: 3 | * https://github.com/wasmerio/wasmer-js/tree/0.x/packages/wasm-terminal 4 | */ 5 | 6 | // some imports need to be lowered 7 | // import { lowerI64Imports } from "@wasmer/wasm-transformer" 8 | 9 | // packages come as .tar.gz 10 | import pako from "pako" // gunzip 11 | import untar from "js-untar" // untar 12 | 13 | class WapmFetchUtil { 14 | static WAPM_GRAPHQL_QUERY = `query shellGetCommandQuery($command: String!) { 15 | command: getCommand(name: $command) { 16 | command 17 | module { 18 | name 19 | abi 20 | source 21 | } 22 | packageVersion { 23 | version 24 | package { 25 | name 26 | displayName 27 | } 28 | filesystem { 29 | wasm 30 | host 31 | } 32 | distribution { 33 | downloadUrl 34 | } 35 | modules { 36 | name 37 | publicUrl 38 | abi 39 | } 40 | commands { 41 | command 42 | module { 43 | name 44 | abi 45 | source 46 | } 47 | } 48 | } 49 | } 50 | }` 51 | 52 | static getCommandFromWAPM = async (commandName) => { 53 | const fetchResponse = await fetch("https://registry.wapm.io/graphql", { 54 | method: "POST", 55 | mode: "cors", 56 | headers: { 57 | Accept: "application/json", 58 | "Content-Type": "application/json", 59 | }, 60 | body: JSON.stringify({ 61 | operationName: "shellGetCommandQuery", 62 | query: WapmFetchUtil.WAPM_GRAPHQL_QUERY, 63 | variables: { 64 | command: commandName, 65 | }, 66 | }), 67 | }) 68 | 69 | const response = await fetchResponse.json() 70 | 71 | if (response && response.data && response.data.command) 72 | return response.data.command 73 | else throw new Error(`command not found ${commandName}`) 74 | } 75 | 76 | static fetchCommandFromWAPM = async ({ args, env }) => { 77 | const commandName = args[0] 78 | const command = await WapmFetchUtil.getCommandFromWAPM(commandName) 79 | if (command.module.abi !== "wasi") 80 | throw new Error( 81 | `Only WASI modules are supported. The "${commandName}" command is using the "${command.module.abi}" ABI.` 82 | ) 83 | return command 84 | } 85 | 86 | static WAPM_PACKAGE_QUERY = `query shellGetPackageQuery($name: String!, $version: String) { 87 | packageVersion: getPackageVersion(name: $name, version: $version) { 88 | version 89 | package { 90 | name 91 | displayName 92 | } 93 | filesystem { 94 | wasm 95 | host 96 | } 97 | distribution { 98 | downloadUrl 99 | } 100 | modules { 101 | name 102 | publicUrl 103 | abi 104 | } 105 | commands { 106 | command 107 | module { 108 | name 109 | abi 110 | source 111 | } 112 | } 113 | } 114 | }` 115 | 116 | static execWapmQuery = async (query, variables) => { 117 | const fetchResponse = await fetch("https://registry.wapm.io/graphql", { 118 | method: "POST", 119 | mode: "cors", 120 | headers: { 121 | Accept: "application/json", 122 | "Content-Type": "application/json", 123 | }, 124 | body: JSON.stringify({ 125 | query, 126 | variables, 127 | }), 128 | }) 129 | 130 | const response = await fetchResponse.json() 131 | if (response && response.data) return response.data 132 | } 133 | 134 | static getBinaryFromUrl = async (url) => { 135 | const fetched = await fetch(url) 136 | const buffer = await fetched.arrayBuffer() 137 | return new Uint8Array(buffer) 138 | } 139 | 140 | static getWAPMPackageFromPackageName = async (packageName) => { 141 | let version 142 | 143 | if (packageName.indexOf("@") > -1) { 144 | const splitted = packageName.split("@") 145 | packageName = splitted[0] 146 | version = splitted[1] 147 | } 148 | 149 | let data = await WapmFetchUtil.execWapmQuery( 150 | WapmFetchUtil.WAPM_PACKAGE_QUERY, 151 | { name: packageName, version: version } 152 | ) 153 | 154 | if (data && data.packageVersion) return data.packageVersion 155 | else throw new Error(`Package not found in the registry ${packageName}`) 156 | } 157 | 158 | static getWasmBinaryFromUrl = async (url) => { 159 | const fetched = await fetch(url) 160 | const buffer = await fetched.arrayBuffer() 161 | return new Uint8Array(buffer) 162 | } 163 | 164 | static getWasmBinaryFromCommand = async (programName) => { 165 | // fetch command from wapm.io (includes path to binary) 166 | const command = await WapmFetchUtil.fetchCommandFromWAPM({ 167 | args: [programName], 168 | }) 169 | 170 | // fetch binary from wapm and extract wasmer files from .tar.gz 171 | const binary = await WapmFetchUtil.getBinaryFromUrl( 172 | command.packageVersion.distribution.downloadUrl 173 | ) 174 | const inflatedBinary = pako.inflate(binary) 175 | const wapmFiles = await untar(inflatedBinary.buffer) 176 | const wasmerFiles = wapmFiles.filter((wapmFile) => 177 | wapmFile.name.split("/").pop().endsWith(".wasm") 178 | ) 179 | 180 | // console.log("wasmerFiles", wasmerFiles) 181 | 182 | // check if we got exactly one binary and then lower its imports 183 | if (wasmerFiles.length > 1) 184 | throw Error("more than 1 wasm file, don't know what to do :D") 185 | const wasmModule = wasmerFiles[0].buffer // await lowerI64Imports(wasmerFiles[0].buffer) 186 | 187 | // todo: there is a file "wapm.toml" that contains info about which command uses which module/binary 188 | 189 | return wasmModule 190 | } 191 | } 192 | 193 | export default WapmFetchUtil 194 | -------------------------------------------------------------------------------- /src/WasmWebTerm.js: -------------------------------------------------------------------------------- 1 | import { proxy, wrap } from "comlink" 2 | 3 | import { FitAddon } from "xterm-addon-fit" 4 | import XtermEchoAddon from "local-echo" 5 | import parse from "shell-quote/parse" 6 | import { inflate } from "pako" // fallback for DecompressionStream API (free as its used in WapmFetchUtil) 7 | 8 | import LineBuffer from "./LineBuffer" 9 | import History from "./History" 10 | import WasmWorkerRAW from "./runners/WasmWorker" // will be prebuilt using webpack 11 | import { 12 | default as PromptsFallback, 13 | MODULE_ID as WasmRunnerID, // get the id of this module in the final webpack bundle 14 | } from "./runners/WasmRunner" 15 | import WapmFetchUtil from "./WapmFetchUtil" 16 | 17 | class WasmWebTerm { 18 | isRunningCommand 19 | 20 | onActivated 21 | onDisposed 22 | 23 | onFileSystemUpdate 24 | onBeforeCommandRun 25 | onCommandRunFinish 26 | 27 | _xterm 28 | _xtermEcho 29 | _xtermPrompt 30 | 31 | _worker 32 | _wasmRunner // prompts fallback 33 | 34 | _jsCommands 35 | _wasmModules 36 | _wasmFsFiles 37 | 38 | _stdoutBuffer 39 | _stderrBuffer 40 | 41 | _outputBuffer 42 | _lastOutputTime 43 | 44 | constructor(wasmBinaryPath) { 45 | this.wasmBinaryPath = wasmBinaryPath 46 | 47 | this._jsCommands = new Map() // js commands and their callback functions 48 | this.isRunningCommand = false // allow running only 1 command in parallel 49 | 50 | this._worker = false // fallback (do not use worker until it is initialized) 51 | this._wasmModules = [] // [{ name: "abc", type: "emscripten|wasmer", module: WebAssembly.Module, [runtime: Blob] }] 52 | this._wasmFsFiles = [] // files created during wasm execution (will be written to wasm runtime's FS) 53 | 54 | this._outputBuffer = "" // buffers outputs to determine if it ended with line break 55 | this._lastOutputTime = 0 // can be used for guessing if output is complete on stdin calls 56 | 57 | this.onActivated = () => {} // can be overwritten to know when activation is complete 58 | this.onDisposed = () => {} // can be overwritten to know when disposition is complete 59 | 60 | this.onFileSystemUpdate = () => {} // can be overwritten to handle emscr file changes 61 | this.onBeforeCommandRun = () => {} // can be overwritten to show load animation (etc) 62 | this.onCommandRunFinish = () => {} // can be overwritten to hide load animation (etc) 63 | 64 | // check if browser support for web working is available 65 | if ( 66 | ![typeof Worker, typeof SharedArrayBuffer, typeof Atomics].includes( 67 | "undefined" 68 | ) 69 | ) 70 | // if yes, initialize worker 71 | this._initWorker() 72 | // if no support -> use prompts as fallback 73 | else this._wasmRunner = new PromptsFallback() 74 | 75 | this._suppressOutputs = false 76 | window.term = this // todo: debug 77 | } 78 | 79 | /* xterm.js addon life cycle */ 80 | 81 | async activate(xterm) { 82 | this._xterm = xterm 83 | 84 | // create xterm addon to fit size 85 | this._xtermFitAddon = new FitAddon() 86 | this._xtermFitAddon.activate(this._xterm) 87 | 88 | // fit xterm size to container 89 | setTimeout(() => this._xtermFitAddon.fit(), 1) 90 | 91 | // handle container resize 92 | window.addEventListener("resize", () => { 93 | this._xtermFitAddon.fit() 94 | }) 95 | 96 | // handle module drag and drop 97 | setTimeout(() => this._initWasmModuleDragAndDrop(), 1) 98 | 99 | // set xterm prompt 100 | this._xtermPrompt = async () => "$ " 101 | // async to be able to fetch sth here 102 | 103 | // create xterm local echo addon 104 | this._xtermEcho = new XtermEchoAddon(null, { historySize: 1000 }) 105 | this._xtermEcho.activate(this._xterm) 106 | 107 | // patch history controller 108 | this._xtermEcho.history = new History(this._xtermEcho.history.size || 10) 109 | 110 | // register available js commands 111 | this.registerJsCommand("help", async function* (argv) { 112 | yield "todo: show helping things\n" 113 | }) 114 | this.registerJsCommand("about", async (argv) => { 115 | return ( 116 | "Wasm-WebTerm version " + 117 | __VERSION__ + 118 | ".\nBackend: " + 119 | (this._worker ? "WebWorker" : "Prompts Fallback") + 120 | ".\n" 121 | ) 122 | }) 123 | this.registerJsCommand( 124 | "clear", 125 | async (argv) => await this.printWelcomeMessagePlusControlSequences() 126 | ) 127 | 128 | // if using webworker -> wait until initialized 129 | if (this._worker instanceof Promise) await this._worker 130 | 131 | // register xterm data handler for Ctrl+C 132 | this._xterm.onData((data) => this._onXtermData(data)) 133 | 134 | // notify that we're ready 135 | await this.onActivated() 136 | 137 | // write welcome message to terminal 138 | this._xterm.writeln( 139 | await this.printWelcomeMessagePlusControlSequences(), 140 | 141 | // callback for when welcome message was printed 142 | () => { 143 | // start REPL 144 | this.repl() 145 | 146 | // focus terminal cursor 147 | setTimeout(() => this._xterm.focus(), 1) 148 | } 149 | ) 150 | } 151 | 152 | async dispose() { 153 | await this._xtermEcho.dispose() 154 | await this._xtermFitAddon.dispose() 155 | if (this._worker) this._terminateWorker() 156 | await this.onDisposed() 157 | } 158 | 159 | /* js command handling */ 160 | 161 | registerJsCommand(name, callback, autocomplete) { 162 | this._jsCommands.set(name, { name, callback, autocomplete }) 163 | return this // to be able to stack these calls 164 | } 165 | 166 | unregisterJsCommand(name) { 167 | return this._jsCommands.delete(name) 168 | } 169 | 170 | get jsCommands() { 171 | return this._jsCommands 172 | } 173 | 174 | /* read eval print loop */ 175 | 176 | async repl() { 177 | try { 178 | // read 179 | const prompt = await this._xtermPrompt() 180 | const line = await this._xtermEcho.read(prompt) 181 | 182 | // empty input -> prompt again 183 | if (line.trim() == "") return this.repl() 184 | 185 | // give user possibility to exec sth before run 186 | await this.onBeforeCommandRun() 187 | 188 | // print newline before 189 | this._xterm.write("\r\n") 190 | 191 | // eval and print 192 | await this.runLine(line) 193 | 194 | // print extra newline if output does not end with one 195 | if (this._outputBuffer.slice(-1) != "\n") this._xterm.write("\u23CE\r\n") 196 | 197 | // print newline after 198 | this._xterm.write("\r\n") 199 | 200 | // give user possibility to run sth after exec 201 | await this.onCommandRunFinish() 202 | 203 | // loop 204 | this.repl() 205 | } catch (e) { 206 | /* console.error("Error during REPL:", e) */ 207 | } 208 | } 209 | 210 | /* parse line as commands and handle them */ 211 | _parseCommands(line) { 212 | let usesEnvironmentVars = false 213 | let usesBashFeatures = false 214 | 215 | // parse line into tokens (respect escaped spaces and quotation marks) 216 | const commandLine = parse(line, (_key) => { 217 | usesEnvironmentVars = true 218 | return undefined 219 | }) 220 | 221 | const commands = [] 222 | let cmd = [] 223 | 224 | splitter: { 225 | for (let idx = 0; idx < commandLine.length; ++idx) { 226 | const item = commandLine[idx] 227 | 228 | if (typeof item === "string") { 229 | if (cmd.length === 0 && item.match(/^\w+=.*$/)) { 230 | usesEnvironmentVars = true 231 | continue 232 | } else { 233 | cmd.push(item) 234 | } 235 | } else { 236 | switch (item.op) { 237 | case "|": 238 | commands.push(cmd) 239 | cmd = [] 240 | break 241 | default: 242 | usesBashFeatures = true 243 | console.error("Unsupported shell operator:", item.op) 244 | break splitter 245 | } 246 | } 247 | } 248 | } 249 | commands.push(cmd) 250 | 251 | if (usesEnvironmentVars) { 252 | this._stderr( 253 | "\x1b[1m[\x1b[33mWARN\x1b[39m]\x1b[0m Environment variables are not supported!\n" 254 | ) 255 | } 256 | if (usesBashFeatures) { 257 | this._stderr( 258 | "\x1b[1m[\x1b[33mWARN\x1b[39m]\x1b[0m Advanced bash features are not supported! Only the pipe '|' works for now.\n" 259 | ) 260 | } 261 | 262 | return commands 263 | } 264 | 265 | async runLine(line) { 266 | try { 267 | let stdinPreset = null 268 | this._suppressOutputs = false 269 | 270 | const commandsInLine = this._parseCommands(line) 271 | for (const [index, argv] of commandsInLine.entries()) { 272 | // split into command name and argv 273 | const commandName = argv.shift() 274 | const command = this._jsCommands.get(commandName) 275 | 276 | // try user registered js commands first 277 | if (typeof command?.callback == "function") { 278 | // todo: move this to a method like "runJsCommand"? 279 | 280 | // call registered user function 281 | const result = command.callback(argv, stdinPreset) 282 | let output // where user function outputs are stored 283 | 284 | /** 285 | * user functions are another word for custom js 286 | * commands and can pass outputs in various ways: 287 | * 288 | * 1) return value normally via "return" 289 | * 2) pass value through promise resolve() / async 290 | * 3) yield values via generator functions 291 | */ 292 | 293 | // await promises if any (2) 294 | if (result.then) output = ((await result) || "").toString() 295 | // await yielding generator functions (3) 296 | else if (result.next) 297 | for await (let data of result) 298 | output = output == null ? data : output + data 299 | // default: when functions return "normally" (1) 300 | else output = result.toString() 301 | 302 | // if is last command in pipe -> print output to xterm 303 | if (index == commandsInLine.length - 1) this._stdout(output) 304 | else stdinPreset = output || null // else -> use output as stdinPreset 305 | 306 | // todo: make it possible for user functions to use stdERR. 307 | // exceptions? they end function execution.. 308 | } 309 | 310 | // otherwise try wasm commands 311 | else if (command == undefined) { 312 | // if is not last command in pipe 313 | if (index < commandsInLine.length - 1) { 314 | const output = await this.runWasmCommandHeadless( 315 | commandName, 316 | argv, 317 | stdinPreset 318 | ) 319 | stdinPreset = output.stdout // apply last stdout to next stdin 320 | } 321 | 322 | // if is last command -> run normally and reset stdinPreset 323 | else { 324 | await this.runWasmCommand(commandName, argv, stdinPreset) 325 | stdinPreset = null 326 | } 327 | } 328 | 329 | // command is defined but has no function -> can not handle 330 | else 331 | console.error("command is defined but has no function:", commandName) 332 | } 333 | } catch (e) { 334 | // catch errors (print to terminal and developer console) 335 | if (this._outputBuffer.slice(-1) != "\n") this._stderr("\n") 336 | this._stderr(`\x1b[1m[\x1b[31mERROR\x1b[39m]\x1b[0m ${e.toString()}\n`) 337 | console.error("Error running line:", e) 338 | } 339 | } 340 | 341 | /* running single wasm commands */ 342 | 343 | runWasmCommand(programName, argv, stdinPreset, onFinishCallback) { 344 | console.log("called runWasmCommand:", programName, argv) 345 | 346 | if (this.isRunningCommand) throw "WasmWebTerm is already running a command" 347 | else this.isRunningCommand = true 348 | 349 | // enable outputs if they were suppressed 350 | this._suppressOutputs = false 351 | this._outputBuffer = "" 352 | 353 | // define callback for when command has finished 354 | const onFinish = proxy(async (files) => { 355 | console.log("command finished:", programName, argv) 356 | 357 | // enable commands to run again 358 | this.isRunningCommand = false 359 | 360 | // store created files 361 | this._wasmFsFiles = files 362 | await this.onFileSystemUpdate(this._wasmFsFiles) 363 | 364 | // wait until outputs are rendered 365 | await this._waitForOutputPause() 366 | 367 | // flush out any pending outputs 368 | this._stdoutBuffer.flush() 369 | this._stderrBuffer.flush() 370 | 371 | // wait until the rest is rendered 372 | this._waitForOutputPause().then(() => { 373 | // notify caller that command run is over 374 | if (typeof onFinishCallback == "function") onFinishCallback() 375 | 376 | // resolve await from shell 377 | this._runWasmCommandPromise?.resolve() 378 | }) 379 | }) 380 | 381 | // define callback for when errors occur 382 | const onError = proxy((value) => this._stderr(value + "\n")) 383 | 384 | // get or initialize wasm module 385 | this._stdout("loading web assembly ...") 386 | this._getOrFetchWasmModule(programName) 387 | .then((wasmModule) => { 388 | // clear last line 389 | this._xterm.write("\x1b[2K\r") 390 | 391 | // check if we can run on worker 392 | if (this._worker) 393 | // delegate command execution to worker thread 394 | this._worker.runCommand( 395 | programName, 396 | wasmModule.module, 397 | wasmModule.type, 398 | argv, 399 | this._stdinProxy, 400 | this._stdoutProxy, 401 | this._stderrProxy, 402 | this._wasmFsFiles, 403 | onFinish, 404 | onError, 405 | null, 406 | stdinPreset, 407 | wasmModule.runtime 408 | ) 409 | // if not -> fallback with prompts 410 | // start execution on the MAIN thread (freezes terminal) 411 | else 412 | this._wasmRunner.runCommand( 413 | programName, 414 | wasmModule.module, 415 | wasmModule.type, 416 | argv, 417 | null, 418 | this._stdoutProxy, 419 | this._stderrProxy, 420 | this._wasmFsFiles, 421 | onFinish, 422 | onError, 423 | null, 424 | stdinPreset, 425 | wasmModule.runtime 426 | ) 427 | }) 428 | 429 | // catch errors (command not running anymore + reject (returns to shell)) 430 | .catch((e) => { 431 | this.isRunningCommand = false 432 | this._runWasmCommandPromise?.reject(e) 433 | }) 434 | 435 | // return promise (makes shell await) 436 | return new Promise( 437 | (resolve, reject) => (this._runWasmCommandPromise = { resolve, reject }) 438 | ) 439 | } 440 | 441 | runWasmCommandHeadless(programName, argv, stdinPreset, onFinishCallback) { 442 | if (this.isRunningCommand) throw "WasmWebTerm is already running a command" 443 | else this.isRunningCommand = true 444 | 445 | // promise for resolving / rejecting command execution 446 | let runWasmCommandHeadlessPromise = { resolve: () => {}, reject: () => {} } 447 | 448 | // define callback for when command has finished 449 | const onFinish = proxy((outBuffers) => { 450 | // enable commands to run again 451 | this.isRunningCommand = false 452 | 453 | // flush outputs 454 | this._stdoutBuffer.flush() 455 | this._stderrBuffer.flush() 456 | 457 | // call on finish callback 458 | if (typeof onFinishCallback == "function") onFinishCallback(outBuffers) 459 | 460 | // resolve promise 461 | runWasmCommandHeadlessPromise.resolve(outBuffers) 462 | }) 463 | 464 | // define callback for when errors occur 465 | const onError = proxy((value) => this._stderr(value + "\n")) 466 | 467 | // define callback for onSuccess (contains files) 468 | const onSuccess = proxy(() => {}) // not used currently 469 | 470 | // get or initialize wasm module 471 | this._getOrFetchWasmModule(programName) 472 | .then((wasmModule) => { 473 | if (this._worker) 474 | // check if we can run on worker 475 | 476 | // delegate command execution to worker thread 477 | this._worker.runCommandHeadless( 478 | programName, 479 | wasmModule.module, 480 | wasmModule.type, 481 | argv, 482 | this._wasmFsFiles, 483 | onFinish, 484 | onError, 485 | onSuccess, 486 | stdinPreset, 487 | wasmModule.runtime 488 | ) 489 | // if not -> use fallback 490 | // start execution on the MAIN thread (freezes terminal) 491 | else 492 | this._wasmRunner.runCommandHeadless( 493 | programName, 494 | wasmModule.module, 495 | wasmModule.type, 496 | argv, 497 | this._wasmFsFiles, 498 | onFinish, 499 | onError, 500 | onSuccess, 501 | stdinPreset, 502 | wasmModule.runtime 503 | ) 504 | }) 505 | 506 | // catch errors (command not running anymore + reject promise) 507 | .catch((e) => { 508 | this.isRunningCommand = false 509 | runWasmCommandHeadlessPromise.reject(e) 510 | }) 511 | 512 | // return promise (makes shell await) 513 | return new Promise( 514 | (resolve, reject) => (runWasmCommandHeadlessPromise = { resolve, reject }) 515 | ) 516 | } 517 | 518 | /* wasm module handling */ 519 | 520 | _getOrFetchWasmModule(programName) { 521 | return new Promise(async (resolve, reject) => { 522 | let wasmModule, 523 | wasmBinary, 524 | localBinaryFound = false 525 | 526 | // check if there is an initialized module already 527 | this._wasmModules.forEach((moduleObj) => { 528 | if (moduleObj.name == programName) wasmModule = moduleObj 529 | }) 530 | 531 | // if a module was found -> resolve 532 | if (wasmModule?.module instanceof WebAssembly.Module) resolve(wasmModule) 533 | else 534 | try { 535 | // if none is found -> initialize a new one 536 | 537 | // create wasm module object (to resolve and to store) 538 | wasmModule = { 539 | name: programName, 540 | type: "emscripten", 541 | module: undefined, 542 | } 543 | 544 | // try to find local wasm binary 545 | // (only if wasmBinaryPath is provided, otherwise use wapm directly) 546 | if (this.wasmBinaryPath != undefined) { 547 | // try to fetch local wasm binaries first 548 | const localBinaryResponse = await fetch( 549 | this.wasmBinaryPath + "/" + programName + ".wasm" 550 | ) 551 | wasmBinary = await localBinaryResponse.arrayBuffer() 552 | 553 | // validate if localBinaryResponse contains a wasm binary 554 | if (localBinaryResponse?.ok && WebAssembly.validate(wasmBinary)) { 555 | // try to fetch emscripten js runtime 556 | const jsRuntimeResponse = await fetch( 557 | this.wasmBinaryPath + "/" + programName + ".js" 558 | ) 559 | if (jsRuntimeResponse?.ok) { 560 | // read js runtime from response 561 | const jsRuntimeCode = await jsRuntimeResponse.arrayBuffer() 562 | 563 | // check if the first char of the response is not "<" 564 | // (because dumb parcel does not return http errors but an html page) 565 | const firstChar = String.fromCharCode( 566 | new Uint8Array(jsRuntimeCode).subarray(0, 1).toString() 567 | ) 568 | if (firstChar != "<") 569 | // set this module's runtime 570 | wasmModule.runtime = jsRuntimeCode 571 | } 572 | 573 | // if no valid js runtime was found -> it's considered a wasmer binary 574 | if (!wasmModule.runtime) wasmModule.type = "wasmer" 575 | 576 | // local binary was found -> do not fetch wapm 577 | localBinaryFound = true 578 | } 579 | 580 | // if none was found or it was invalid -> try for a .lnk file 581 | else { 582 | // explanation: .lnk files can contain a different module/runtime name. 583 | // this enables `echo` and `ls` to both use `coreutils.wasm`, for example. 584 | 585 | // try to fetch .lnk file 586 | const linkResponse = await fetch( 587 | this.wasmBinaryPath + "/" + programName + ".lnk" 588 | ) 589 | if (linkResponse?.ok) { 590 | // read new program name from .lnk file 591 | const linkedProgramName = await linkResponse.text() 592 | const linkDestination = 593 | this.wasmBinaryPath + "/" + linkedProgramName + ".wasm" 594 | 595 | // try to fetch the new binary 596 | const linkedBinaryResponse = await fetch(linkDestination) 597 | if (linkedBinaryResponse?.ok) { 598 | // read binary from response 599 | wasmBinary = await linkedBinaryResponse.arrayBuffer() 600 | 601 | // validate if linkedBinaryResponse contains a wasm binary 602 | if (WebAssembly.validate(wasmBinary)) { 603 | // try to fetch emscripten js runtime 604 | const jsRuntimeResponse = await fetch( 605 | this.wasmBinaryPath + "/" + linkedProgramName + ".js" 606 | ) 607 | if (jsRuntimeResponse?.ok) { 608 | // todo: note that this code is redundant, maybe use a function? 609 | 610 | // read js runtime from response 611 | const jsRuntimeCode = 612 | await jsRuntimeResponse.arrayBuffer() 613 | 614 | // check if the first char of the response is not "<" 615 | // (because dumb parcel does not return http errors but an html page) 616 | const firstChar = String.fromCharCode( 617 | new Uint8Array(jsRuntimeCode).subarray(0, 1).toString() 618 | ) 619 | if (firstChar != "<") 620 | // set this module's runtime 621 | wasmModule.runtime = jsRuntimeCode 622 | } 623 | 624 | // if no valid js runtime was found -> it's considered a wasmer binary 625 | if (!wasmModule.runtime) wasmModule.type = "wasmer" 626 | 627 | // local binary was found -> do not fetch wapm 628 | localBinaryFound = true 629 | } 630 | } 631 | } 632 | } 633 | } 634 | 635 | // if no local binary was found -> fetch from wapm.io 636 | if (!localBinaryFound) { 637 | wasmBinary = 638 | await WapmFetchUtil.getWasmBinaryFromCommand(programName) 639 | wasmModule.type = "wasmer" 640 | } 641 | 642 | // compile fetched bytes into wasm module 643 | wasmModule.module = await WebAssembly.compile(wasmBinary) 644 | 645 | // store compiled module 646 | this._wasmModules.push(wasmModule) 647 | 648 | // continue execution 649 | resolve(wasmModule) 650 | } catch (e) { 651 | reject(e) 652 | } 653 | }) 654 | } 655 | 656 | _initWasmModuleDragAndDrop() { 657 | // event handler for when user starts to drag file 658 | this._xterm.element.addEventListener("dragenter", (e) => { 659 | this._xterm.element.style.opacity = "0.8" 660 | }) 661 | 662 | // needed for drop event to be fired on div 663 | this._xterm.element.addEventListener("dragover", (e) => { 664 | e.preventDefault() 665 | this._xterm.element.style.opacity = "0.8" 666 | }) 667 | 668 | // event handler for when user stops to drag file 669 | this._xterm.element.addEventListener("dragleave", (e) => { 670 | this._xterm.element.style.opacity = "" 671 | }) 672 | 673 | // event handler for when the user drops the file 674 | this._xterm.element.addEventListener( 675 | "drop", 676 | async (e) => { 677 | e.preventDefault() 678 | let files = [] 679 | 680 | if (e.dataTransfer.items) 681 | // read files from .items 682 | for (let i = 0; i < e.dataTransfer.items.length; i++) 683 | if (e.dataTransfer.items[i].kind == "file") 684 | files.push(e.dataTransfer.items[i].getAsFile()) 685 | // read files from .files (other browsers) 686 | else 687 | for (let i = 0; i < e.dataTransfer.files.length; i++) 688 | files.push(e.dataTransfer.files[i]) 689 | 690 | // parse dropped files into modules 691 | for (let i = 0; i < files.length; i++) { 692 | const file = files[i] 693 | if (file.name.endsWith(".wasm")) { 694 | // todo: also support dropping .lnk files? 695 | 696 | const programName = file.name.replace(/\.wasm$/, "") 697 | 698 | // remove existing modules with that name 699 | this._wasmModules = this._wasmModules.filter( 700 | (mod) => mod.name != programName 701 | ) 702 | 703 | // if has .js file -> it's an emscripten binary 704 | if (files.some((f) => f.name == programName + ".js")) { 705 | // load emscripten js runtime and compile emscripten wasm binary 706 | const emscrJsRuntime = files.find( 707 | (f) => f.name == programName + ".js" 708 | ) 709 | const emscrWasmModule = await WebAssembly.compile( 710 | await file.arrayBuffer() 711 | ) 712 | 713 | // add compiled emscripten module to this._wasmModules 714 | this._wasmModules.push({ 715 | name: programName, 716 | type: "emscripten", 717 | runtime: await emscrJsRuntime.arrayBuffer(), 718 | module: emscrWasmModule, 719 | }) 720 | 721 | alert("Emscripten Wasm Module added: " + programName) 722 | } else { 723 | // if not -> its considered a wasmer binary 724 | 725 | // compile wasmer module and store in this._wasmModules 726 | const wasmerModule = await WebAssembly.compile( 727 | await file.arrayBuffer() 728 | ) 729 | this._wasmModules.push({ 730 | name: programName, 731 | type: "wasmer", 732 | module: wasmerModule, 733 | }) 734 | 735 | alert("WASI Module added: " + programName) 736 | } 737 | } 738 | } 739 | 740 | this._xterm.element.style.opacity = "" 741 | }, 742 | false 743 | ) 744 | } 745 | 746 | /* worker execution flow */ 747 | 748 | _initWorker() { 749 | this._worker = new Promise(async (resolve) => { 750 | // init buffers for pausing worker and passing stdin values 751 | this._pauseBuffer = new Int32Array( 752 | new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 1) 753 | ) // 1 bit to shift 754 | this._stdinBuffer = new Int32Array( 755 | new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 1000) 756 | ) // 1000 chars buffer 757 | 758 | // create blob including webworker and its dependencies 759 | const workerSource = fetch(WasmWorkerRAW).then((response) => { 760 | if ("DecompressionStream" in self) { 761 | const stream = response.body.pipeThrough( 762 | new DecompressionStream("gzip") 763 | ) 764 | const decompressed = new Response(stream) 765 | return decompressed.text() 766 | } else { 767 | return response.bytes().then((data) => inflate(data)) 768 | } 769 | }) 770 | 771 | // HACK: Forward all modules for the `WasmRunner`(`PromptsFallback`) to the worker 772 | // instead of bundling them directly. This saves about 90 KiB by not serializing 773 | // these dependencies twice. This works fine, since we already use the identical 774 | // module already as a fallback. 775 | const extraModules = {} 776 | try { 777 | const webpackModuleIds = Object.keys(__webpack_modules__) 778 | let dependencies = [WasmRunnerID] // start with the `WasmRunner` module 779 | for ( 780 | let dep = dependencies.shift(); 781 | dep != undefined; 782 | dep = dependencies.shift() 783 | ) { 784 | // put the module into the list of modules to forward 785 | const mod = __webpack_modules__[dep] 786 | extraModules[dep] = mod 787 | 788 | // parse module for imports of other module dependencies (like `r(MODULE_ID)`) 789 | const matches = mod.toString().matchAll(/\br\((\d+)\)/g) 790 | for (const match of matches) { 791 | if (webpackModuleIds.includes(match[1])) dependencies.push(match[1]) 792 | } 793 | } 794 | } catch (err) { 795 | // Could not collect the `WasmRunner` module and its dependencies for forwarding 796 | console.error( 797 | `Cannot collect the \`WasmRunner\` module and its dependencies for use in the Worker: ${err}` 798 | ) 799 | console.error( 800 | "Falling back to excuting `WasmRunner` in the PromptsFallback" 801 | ) 802 | 803 | // -> use prompts as fallback 804 | this._wasmRunner = new PromptsFallback() 805 | this._worker = false 806 | resolve(this._worker) 807 | 808 | return 809 | } 810 | 811 | // prepend source of collected modules as well as the ID for the 812 | // `WasmRunner` module to the original worker source 813 | let prelude = "self._modules={" 814 | for (const [id, module] of Object.entries(extraModules)) { 815 | prelude += `${id}:${module.toString()},` 816 | } 817 | prelude += `};self.WasmRunnerID=${WasmRunnerID}\n` 818 | 819 | const blob = new Blob([prelude, await workerSource], { 820 | type: "application/javascript", 821 | }) 822 | 823 | // init webworker from blob (no separate file, no cross-origin problems) 824 | this._workerRAW = new Worker(URL.createObjectURL(blob)) 825 | const WasmWorker = wrap(this._workerRAW) 826 | this._worker = await new WasmWorker(this._pauseBuffer, this._stdinBuffer) 827 | 828 | resolve(this._worker) // webworker is now initialized 829 | }) 830 | } 831 | 832 | _resumeWorker() { 833 | console.log("resuming worker (request)") 834 | Atomics.store(this._pauseBuffer, 0, 0) // mem[0] = 0 (means do not hold) 835 | Atomics.notify(this._pauseBuffer, 0) // awake waiting 836 | } 837 | 838 | _terminateWorker() { 839 | console.log("called terminate worker") 840 | this._workerRAW?.terminate() 841 | } 842 | 843 | _waitForOutputPause(pauseDuration = 80, interval = 20) { 844 | // note: timeout because web worker outputs are not always rendered to 845 | // the term directly. therefore we wait until we guess it's all there. 846 | return new Promise((resolve) => { 847 | const timeout = () => { 848 | setTimeout(() => { 849 | // if there has been output in the last pauseDuration -> run again 850 | if (this._lastOutputTime > Date.now() - pauseDuration) timeout() 851 | // if not -> resolve 852 | else resolve() 853 | }, interval) 854 | } 855 | timeout() 856 | }) 857 | } 858 | 859 | /* input output handling -> web worker */ 860 | 861 | _setStdinBuffer(string) { 862 | for (let i = 0; i < this._stdinBuffer.length; i++) 863 | this._stdinBuffer[i] = string[i] ? string[i].charCodeAt(0) : 0 // 0 = null (empty bits = end of string) 864 | } 865 | 866 | _stdinProxy = proxy((message) => { 867 | this._waitForOutputPause().then(async () => { 868 | console.log("called _stdinProxy", message) 869 | 870 | // flush outputs (to show the prompt) 871 | this._stdoutBuffer.flush() 872 | this._stderrBuffer.flush() 873 | 874 | // read input until RETURN (LF), CTRL+D (EOF), or CTRL+C 875 | const input = await new Promise((resolve, reject) => { 876 | let buffer = "" 877 | const handler = this._xterm.onData((data) => { 878 | // CTRL + C -> return without data 879 | if (data == "\x03") { 880 | this._xterm.write("^C") 881 | handler.dispose() 882 | return resolve("") 883 | } 884 | // CTRL + D -> return input buffer 885 | else if (data == "\x04") { 886 | handler.dispose() 887 | return resolve(buffer) 888 | } 889 | 890 | // map return to '\n' 891 | if (data == "\r") data = "\n" 892 | // map backspace to CTRL+H 893 | else if (data == "\x7f") data = "\x08" 894 | 895 | // add character or delete last one 896 | if (data == "\x08") buffer = buffer.slice(0, -1) 897 | else buffer += data 898 | 899 | // line complete -> return the input buffer 900 | if (data == "\n") { 901 | // only echo the linebreak when there is no prompt (i.e. we assume multi-line input) 902 | if (!message) this._xterm.write("\r\n") 903 | 904 | handler.dispose() 905 | return resolve(buffer) 906 | } 907 | 908 | // echo input back (special handling for backspace and escape sequences) 909 | if (data == "\x08") this._xterm.write("^H") 910 | else if (data.charCodeAt(0) == 0x1b) 911 | this._xterm.write("^[" + data.slice(1)) 912 | else this._xterm.write(data) 913 | }) 914 | }) 915 | 916 | // pass value to webworker 917 | this._setStdinBuffer(input) 918 | this._resumeWorker() 919 | }) 920 | }) 921 | 922 | _stdoutProxy = proxy((value) => { 923 | this._lastOutputTime = Date.now() // keep track of time 924 | this._stdoutBuffer.write(value) 925 | }) 926 | _stderrProxy = proxy((value) => { 927 | this._lastOutputTime = Date.now() // keep track of time 928 | this._stderrBuffer.write(value) 929 | }) 930 | 931 | /* input output handling -> term */ 932 | 933 | _stdoutBuffer = new LineBuffer(this._stdout.bind(this)) 934 | _stderrBuffer = new LineBuffer(this._stderr.bind(this)) 935 | 936 | _stdout(value) { 937 | // string or char code 938 | 939 | if (this._suppressOutputs) return // used for Ctrl+C 940 | 941 | // numbers are interpreted as char codes -> convert to string 942 | if (typeof value == "number") value = String.fromCharCode(value) 943 | 944 | // avoid offsets with line breaks 945 | value = value.replace(/\n/g, "\r\n") 946 | 947 | // write to terminal 948 | this._outputBuffer += value 949 | this._xterm.write(value) 950 | } 951 | 952 | _stderr(value) { 953 | // check if it's a javascript error 954 | if (value instanceof Error) { 955 | // log error to the console 956 | console.error("stderr error:", value) 957 | 958 | // convert error object to string 959 | value = value.toString() + "\n" 960 | } 961 | 962 | // print to terminal 963 | this._stdout(value) 964 | } 965 | 966 | async printWelcomeMessage() { 967 | let message = `\x1b[1;32m 968 | _ _ _ _ _ _ _ _____ \r 969 | | | | |___ ___ _____ | | | |___| |_ |_ _|___ ___ _____ \r 970 | | | | | .'|_ -| | | | | | -_| . | | | | -_| _| |\r 971 | |_____|__,|___|_|_|_| |_____|___|___| |_| |___|_| |_|_|_|\r 972 | \x1b[37m\r\n` 973 | 974 | message += 975 | "Run WebAssembly binaries compiled with Emscripten or Wasmer.\r\n" 976 | message += 977 | "You can also define and run custom JavaScript functions.\r\n\r\n" 978 | 979 | message += "Version: " + __VERSION__ + ". " 980 | message += 981 | "Backend: " + (this._worker ? "WebWorker" : "Prompts Fallback") + ".\r\n" 982 | message += 983 | "Commands: " + 984 | [...this._jsCommands] 985 | .map((commandObj) => commandObj[0]) 986 | .sort() 987 | .join(", ") + 988 | ". " 989 | 990 | return message 991 | } 992 | 993 | // helper function to cleanly print the welcome message 994 | async printWelcomeMessagePlusControlSequences() { 995 | // clear terminal, reset color, print welcome message, reset color, add empty line 996 | return ( 997 | "\x1bc" + 998 | "\x1b[0;37m" + 999 | (await this.printWelcomeMessage()) + 1000 | "\x1b[0;37m\r\n" 1001 | ) 1002 | } 1003 | 1004 | _onXtermData(data) { 1005 | if (data == "\x03") { 1006 | // custom handler for Ctrl+C (webworker only) 1007 | if (this._worker) { 1008 | this._suppressOutputs = true 1009 | this._terminateWorker() 1010 | this._initWorker() // reinit 1011 | this._runWasmCommandPromise?.reject("Ctrl + C") 1012 | this.isRunningCommand = false 1013 | } 1014 | } 1015 | } 1016 | } 1017 | 1018 | export default WasmWebTerm 1019 | -------------------------------------------------------------------------------- /src/WasmWebTerm.md: -------------------------------------------------------------------------------- 1 | # [`WasmWebTerm.js`](./WasmWebTerm.js) Code API 2 | 3 | The main class `WasmWebTerm`, located in `WasmWebTerm.js`, has some attributes and methods that you can use or overwrite to adapt its behaviour to your needs. You can see an [example on how we used it for OpenSSL](https://github.com/cryptool-org/openssl-webterm). 4 | 5 | For example, you can interact with the files in the filesystem, or change the welcome message, or write custom command with JS functions. 6 | 7 | > Private attributes or methods are indicated by an underscore (`_`). For example: [`_jsCommands`](#_jscommands) would be private while [`jsCommands`](#jscommands) would be public. You can of course use these none the less. 8 | 9 | To begin with, initialize a new instance of `WasmWebTerm`. Then overwrite its methods (if you want) and attach it to the xterm.js `Terminal`. Then you can execute methods on it. 10 | 11 | Here's an example for a webterm with custom welcome message and custom prompt. It then executes `cowsay hi` ([cowsay binary](https://wapm.io/syrusakbary/cowsay)) 12 | 13 | ```js 14 | import { Terminal } from "xterm" 15 | import WasmWebTerm from "wasm-webterm" 16 | 17 | let term = new Terminal() 18 | let wasmterm = new WasmWebTerm() 19 | 20 | wasmterm.printWelcomeMessage = () => "Hello world shell \r\n" 21 | wasmterm._xtermPrompt = () => "custom> " 22 | 23 | term.loadAddon(wasmterm) 24 | term.open(document.getElementById("terminal")) 25 | 26 | wasmterm.runWasmCommand("cowsay", ["hi"]) 27 | ``` 28 | 29 | 30 | ### Public Attributes 31 | 32 | * #### `isRunningCommand` 33 | Boolean value if the addon is currently running a command. Both using the `Terminal` or executing headless. This is to make sure, only a single command runs in parallel. 34 | 35 | * #### `jsCommands` 36 | Getter for [`_jsCommands`](#_jscommands), a [`Map()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) containing JS commands that can be ran on the webterm. 37 | 38 | 39 | ### Public Methods 40 | 41 | * #### async `repl()` 42 | Starts a [Read Eval Print Loop](https://en.wikipedia.org/wiki/Read–eval–print_loop). It reads a line from the terminal, calls `onBeforeCommandRun()`, calls `runLine(line)` (which evaluates the line and runs the contained command), calls `onCommandRunFinish()`, and then recursively calls itself again (loop). 43 | 44 | * #### async `runLine(line)` 45 | Gets a string (line), splits it into single commands (separated by `|`), and iterates over them. It then checks, if there is a JS function defined in [`_jsCommands`](#_jscommands) with the given command name. If there is, it'll execute it. See [defining custom JS commands](#defining-custom-js-commands) for more details. Otherwise, it will interpret the command name as the name of a WebAssembly binary and delegate to `runWasmCommand(..)` and `runWasmCommandHeadless(..)`. 46 | 47 | * #### `runWasmCommand(programName, argv, stdinPreset, onFinishCallback)` 48 | The method for running single wasm commands on the Terminal. Only one command in parallel is allowed. It will call [`_getOrFetchWasmModule(name)`](#_getorfetchwasmmoduleprogramname) to fetch the according WebAssembly Module. It will then delegate the call to the Worker or the WasmRunner (which is the Prompts fallback). It also defines callback functions for when the wasm execution has finished or errored. It also passes (proxies to) `_stdout(val)`, `_stderr(val)`, and `_stdinProxy(msg)` to the WebAssembly binary. `stdinPreset` can be a string (when using pipes) or `null`. After the run (if successfull or on errors), `onFinishCallback()` will be called. This method can also be awaited instead of using the callback. 49 | 50 | * #### `runWasmCommandHeadless(programName, argv, stdinPreset, onFinishCallback)` 51 | Same as `runWasmCommand(..)` but without writing to the Terminal. It does not pass proxies for input/output but buffers outputs and returns them in the callback. Errors will be printed though. This method can also be awaited instead of using the callback. 52 | 53 | * #### `registerJsCommand(name, callback, autocomplete)` 54 | Registers a JS function in `callback` as to be called when the command with the name of `name` is entered into the Terminal. `autocomplete` does not work yet. These JS functions are also refered to as "user functions". 55 | 56 | They're callback functions will receive `argv` (array) and `stdinPreset` (string) as input. `argv` contains the command parameters and `stdinPreset` contains the output of a previous command when using pipes). 57 | 58 | They can pass outputs in 3 ways: 59 | 60 | * Normally return a string via `return` 61 | * Return a promise and use `resolve()` (async functions are fine too) 62 | * Using `yield` in generator functions 63 | 64 | See [defining custom JS commands](#defining-custom-js-commands) for examples. 65 | 66 | * #### `unregisterJsCommand(name)` 67 | Counter part to [`registerJsCommand(..)`](#registerjscommandname-callback-autocomplete). Removes the entry from the Map. 68 | 69 | * #### async `printWelcomeMessage()` 70 | Returns a string which is then printed to the Terminal on startup. This can be overwritten and is async so you could fetch something. 71 | 72 | 73 | ### Event methods 74 | 75 | The following methods are called on specific events. You can overwrite them to customly handle the events. 76 | 77 | * #### async `onActivated()` 78 | Is fired after the addon has been attached to the xterm.js Terminal instance. Usually triggered by including the Terminal into the DOM. It is called in the method `activate`, where the addon is being initialized. 79 | 80 | * #### async `onDisposed()` 81 | The counter part to `onActivated` is called when the addon has been detached from the Terminal. Usually triggered by closing the tab or something. 82 | 83 | * #### async `onFileSystemUpdate(_wasmFsFiles)` 84 | Is called every time the filesystem is being updated. This does not happen immediatly when a file is being written by the wasm binary, but when a command has been ran. Contains the value of `_wasmFsFiles`. 85 | 86 | * #### async `onBeforeCommandRun()` 87 | Is called before every line/command ran via the REPL. Gives the opportunity to show loading animations or something like that. 88 | 89 | * #### async `onCommandRunFinish()` 90 | Is called after a line/command has been ran via the REPL. Gives the opportunity to hide loading animations etc. 91 | 92 | 93 | ### Private Attributes 94 | 95 | * #### `_xterm` 96 | The local instance of xterm.js `Terminal` which the addon is attached to. 97 | 98 | * #### `_xtermEcho` 99 | The local instance of `local-echo`, which provides the possibility to read from the Terminal. 100 | > It also makes sense to look at the underlying [local-echo](https://github.com/wavesoft/local-echo). For example, its API offers the possibility to `.abortRead(reason)`, which exits the REPL. 101 | 102 | * #### `_xtermPrompt` 103 | An async function that returns what is shown as prompt in the Terminal. Default is `$`. 104 | 105 | * #### `_jsCommands` 106 | ES6 [`Map()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) containing JS commands in the form of `["command" => function(argv, stdin)]` (simplified). There is a getter called [`jsCommands`](#jscommands), so you don't need the underscore. This can be mutated by using [`registerJsCommand(..)`](#registerjscommandname-callback-autocomplete) and [`unregisterJsCommand(..)`]((#unregisterjscommand)). 107 | 108 | * #### `_worker` 109 | Instance of [Comlink](https://github.com/GoogleChromeLabs/comlink) worker if there is support for Workers. Or the boolean value `false` if there is not and the Prompts Fallback is being used. 110 | 111 | * #### `_pauseBuffer` 112 | [SharedArrayBuffer](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) (1 bit) for pausing the Worker by using [Atomics](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Atomics). If the value is set to `1`, the worker will hold on its next call of `pauseExecution()` inside of [`WasmWorker`](https://github.com/cryptool-org/wasm-webterm/blob/master/src/runners/WasmWorker.js). It can then be resumed by setting the value to `0` again. 113 | 114 | * #### `_stdinBuffer` 115 | [SharedArrayBuffer](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) (1000 bit) for passing strings to the Worker which are then used as Stdin values. Zeros (0) mark the end of the string. 116 | 117 | * #### `_wasmRunner` 118 | Instance of `WasmRunner` (see `WasmRunner.js`) that is being used as fallback, when Workers are not available. Also refered to as "Prompts Fallback", as it uses `window.prompt()` and runs on the main thread. 119 | 120 | * #### `_wasmModules` 121 | Array of objects containing WebAssembly modules. The object structure is as follows. The `runtime` will only be set if it's an Emscripten binary and contain the JS code then. 122 | ```json 123 | [{ name: "", type: "emscripten|wasmer", module: WebAssembly.Module, runtime: [optional] }] 124 | ``` 125 | 126 | * #### `_wasmFsFiles` 127 | Array of objects containing the files from the virtual memory filesystem used for the wasm binaries encoded as binary data. The format is as follows: 128 | ```json 129 | [{ name: "", timestamp: , bytes: Uint8Array }] 130 | ``` 131 | 132 | * #### `_outputBuffer` 133 | String that buffers both outputs (stdout and stderr). It is used to determine if a command's output has ended with a line break or not, thus one should be appended or not. 134 | 135 | * #### `_lastOutputTime` 136 | Unix timestamp updated on every output (stdout and stderr). Worker outputs are not always rendered to the Terminal directly. Therefore we wait for like 80ms before we ask for Stdin or return control to the REPL. 137 | 138 | 139 | ### Internal methods 140 | 141 | * #### async `activate(xterm)` 142 | This is an [xterm.js addon life cycle method](https://xtermjs.org/docs/guides/using-addons/#creating-an-addon) and it's being called when the addon is loaded into the xterm.js Terminal instance. It loads the [xterm.js FitAddon](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-fit) for dynamic Terminal resizing and the [`local-echo` addon](https://github.com/wavesoft/local-echo) for reading from the Terminal. It also initializes the drag&drop mechanism, registers default JS commands (`help` and `clear`), prints the welcome message, and starts the REPL. 143 | 144 | * #### async `dispose()` 145 | Counter part to `activate(xterm)`. Disposes the FitAddon and `local-echo` and terminates the Worker. 146 | 147 | * #### `_onXtermData(data)` 148 | Handler for data from the xterm.js Terminal. Whenever a user enters something, this method is called. It's currently only used for `Ctrl+C` but could be overwritten and extended. 149 | 150 | * #### `_getOrFetchWasmModule(programName)` 151 | Fetches WebAssembly binaries and compiles them into WebAssembly Modules. Returns Promise to be awaited or handled by using `.then(wasmModule)`. If there already is a compiled module stored in `_wasmModules`, it will be used and nothing will be fetched. If there is none yet, it will fetch `/.wasm` and validate if it's WebAssembly. If so, it will also try to fetch a JS runtime at `/.js`. If it is found, the wasm binary is determined to be an Emscripten binary and the JS runtime is stored. If none is found, the wasm binary is considered a WASI binary. If no `.wasm` binary is found, it will query [wapm.io](https://wapm.io) and try to fetch a WASI binary from there. 152 | 153 | * #### `_initWasmModuleDragAndDrop()` 154 | Registers event handlers for dragging and dropping WebAssembly binaries into the Terminal window. If binaries are dropped, they're compiled and added to `_wasmModules`. 155 | 156 | * #### `_initWorker()` 157 | Creates [SharedArrayBuffer](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer)s (`_pauseBuffer` and `_stdinBuffer`) and creates a Comlink instance of the prebuilt `WasmWorker` bundle, which is being initialized as a Worker thread from a Blob. This Blob initialization only works because all dependencies are bundles into `worker.bundle.js` by Webpack. 158 | 159 | * #### `_resumeWorker()` 160 | Sets the [SharedArrayBuffer](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) `_pauseBuffer` to `0` which resumes the Worker if its locked in `WasmWorker`'s `pauseExecution()`. 161 | 162 | * #### `_terminateWorker()` 163 | Immediately [terminates](https://developer.mozilla.org/docs/Web/API/Worker/terminate) the Worker thread. "This does not offer the worker an opportunity to finish its operations; it is stopped at once." 164 | 165 | * #### `_waitForOutputPause(pauseDuration = 80, interval = 20)` 166 | Worker outputs are not always rendered to the Terminal directly. Therefore we wait for like 80ms before we ask for Stdin or return control to the REPL. `interval` determines the time between each check. 167 | 168 | * #### `_setStdinBuffer(string)` 169 | Sets the value of `_stdinBuffer` to a given string, which can then be read from the Worker. 170 | 171 | * #### `_stdinProxy(message)` 172 | Comlink Proxy which will be passed to the Worker thread. It will be called when the wasm binary reads from `/dev/stdin` or `/dev/tty`. It then reads a line from the xterm.js Terminal by using `local-echo`, sets the `_stdinBuffer` accordingly, and resumes the Worker. 173 | 174 | * #### `_stdoutProxy(value)` and `_stderrProxy(value)` 175 | Comlink proxies that map to `_stdout(value)` and `_stderr(value)`. They're proxies so that we can pass them to the Worker. But they can also be called directly, so we can also pass them to the `WasmRunner` Prompts fallback. 176 | 177 | * #### `_stdout(value)` 178 | Prints the string `value` to the xterm.js Terminal, stores it in the `_outputBuffer`, and updates `_lastOutputTime`. If `value` is a number, it will be interpreted as an ASCI char code and converted into a string. 179 | 180 | * #### `_stderr(value)` 181 | Just maps to `_stdout(value)` but could be used to handle Stderr separatly. 182 | 183 | 184 | ----- 185 | -------------------------------------------------------------------------------- /src/runnables/EmscriptenRunnable.js: -------------------------------------------------------------------------------- 1 | class EmscrWasmRunnable { 2 | /* method wrapper class for emscr wasm modules. 3 | loads emscripten js runtime and can execute wasm. */ 4 | 5 | programName 6 | wasmModule 7 | 8 | #emscrJsRuntime 9 | 10 | constructor(programName, wasmModule, jsRuntime) { 11 | this.programName = programName 12 | this.wasmModule = wasmModule 13 | this._loadEmscrJsRuntime(jsRuntime) 14 | } 15 | 16 | /** 17 | * Executes given arguments (argv) on wasm module. 18 | * onFinish is called even if there are errors during execution. 19 | * 20 | * if stdinPreset (string) is set, it will be delivered instead of reading stdin 21 | * from terminal (this feature is used for piping and running headless commands) 22 | */ 23 | run( 24 | argv, 25 | stdin, 26 | stdout, 27 | stderr, 28 | files, 29 | onFinish, 30 | onError, 31 | onSuccess, 32 | stdinPreset 33 | ) { 34 | console.log("emscr runnable run:", this.programName, argv) 35 | 36 | // initialize default methods and values 37 | if (typeof stdin != "function") stdin = () => {} 38 | if (typeof stdout != "function") stdout = () => {} 39 | if (typeof stderr != "function") stderr = () => {} 40 | if (!(files instanceof Array)) files = [] 41 | 42 | // initialize default callbacks 43 | if (typeof onFinish != "function") onFinish = () => {} 44 | if (typeof onError != "function") 45 | onError = (e) => { 46 | console.error(e) 47 | } 48 | if (typeof onSuccess != "function") onSuccess = () => {} 49 | 50 | // define emscr module 51 | let emscrModule = { 52 | thisProgramm: this.programName, 53 | instantiateWasm: (imports, callback) => { 54 | WebAssembly.instantiate(this.wasmModule, imports).then((instance) => 55 | callback(instance, this.wasmModule) 56 | ) 57 | return {} 58 | }, 59 | preInit: [ 60 | () => { 61 | emscrModule.TTY.register(emscrModule.FS.makedev(5, 0), { 62 | get_char: (tty) => stdin(tty), 63 | put_char: (tty, val) => { 64 | tty.output.push(val) 65 | stdout(val) 66 | }, 67 | flush: (tty) => (tty.output = []), 68 | fsync: (tty) => 69 | console.log( 70 | "fsynced stdout (EmscriptenRunnable does nothing in this case)" 71 | ), 72 | }) 73 | emscrModule.TTY.register(emscrModule.FS.makedev(6, 0), { 74 | get_char: (tty) => stdin(tty), 75 | put_char: (tty, val) => { 76 | tty.output.push(val) 77 | stderr(val) 78 | }, 79 | flush: (tty) => (tty.output = []), 80 | fsync: (tty) => 81 | console.log( 82 | "fsynced stderr (EmscriptenRunnable does nothing in this case)" 83 | ), 84 | }) 85 | }, 86 | ], 87 | } 88 | 89 | if (stdinPreset) { 90 | // with stdinPreset you can preset a value for stdin 91 | 92 | if (typeof stdinPreset != "string") 93 | stdinPreset = (stdinPreset || "").toString() 94 | 95 | let stdinIndex = 0 96 | emscrModule.stdin = () => { 97 | if (stdinIndex < stdinPreset.length) 98 | return stdinPreset.charCodeAt(stdinIndex++) 99 | return null 100 | } 101 | } 102 | 103 | let filesPostRun // instantiate emscripten module and call main 104 | this.#emscrJsRuntime(emscrModule) 105 | .then((instance) => { 106 | // emscr module instance 107 | 108 | // write submitted files to wasm 109 | this._writeFilesToFS(instance, files) 110 | 111 | // execute command 112 | instance.callMain(argv) 113 | 114 | // read created files from wasm 115 | filesPostRun = this._readFilesFromFS(instance) 116 | 117 | // success callback 118 | onSuccess(filesPostRun) 119 | }) 120 | 121 | .catch((error) => onError(error)) 122 | .finally(() => onFinish(filesPostRun || files)) 123 | } 124 | 125 | /** 126 | * Executes a command without command line input/output. 127 | * It runs the command and returns all outputs in onFinish. 128 | * 129 | * --> only supports commands that do not ask for CLI input 130 | * --> stdin can be preset by passing string as stdinPreset 131 | */ 132 | runHeadless(argv, files, onFinish, onError, onSuccess, stdinPreset) { 133 | console.log("emscr runnable run headless:", this.programName, argv) 134 | 135 | // initialize default callback 136 | if (typeof onFinish != "function") onFinish = () => {} 137 | 138 | // stdin is not needed 139 | const stdin = () => { 140 | console.log("called runHeadless stdin") 141 | } 142 | 143 | // output is redirected into buffer 144 | let outputBuffer = "", 145 | stdoutBuffer = "", 146 | stderrBuffer = "" 147 | const stdout = (val) => { 148 | outputBuffer += String.fromCharCode(val) 149 | stdoutBuffer += String.fromCharCode(val) 150 | } 151 | const stderr = (val) => { 152 | outputBuffer += String.fromCharCode(val) 153 | stderrBuffer += String.fromCharCode(val) 154 | } 155 | 156 | // run command with custom input/output 157 | this.run( 158 | argv, 159 | stdin, 160 | stdout, 161 | stderr, 162 | files, 163 | () => 164 | onFinish({ 165 | output: outputBuffer, 166 | stdout: stdoutBuffer, 167 | stderr: stderrBuffer, 168 | }), 169 | onError, 170 | onSuccess, 171 | stdinPreset 172 | ) 173 | } 174 | 175 | /* file handling */ 176 | 177 | _readFilesFromFS(instance, directory = "/", includeBinary = true) { 178 | const path = instance.FS.lookupPath(directory) 179 | 180 | const getFilesFromNode = (parentNode) => { 181 | let files = [] 182 | Object.values(parentNode.contents).forEach((node) => { 183 | let nodePath = instance.FS.getPath(node) 184 | if (instance.FS.isFile(node.mode)) 185 | files.push({ 186 | name: nodePath, 187 | timestamp: node.timestamp, 188 | bytes: includeBinary 189 | ? instance.FS.readFile(nodePath) 190 | : new Uint8Array(), 191 | }) 192 | if (instance.FS.isDir(node.mode)) 193 | files = [...files, ...getFilesFromNode(node)] 194 | }) 195 | return files 196 | } 197 | 198 | let files = getFilesFromNode(path.node) 199 | return files 200 | } 201 | 202 | _writeFilesToFS(instance, files = []) { 203 | files.forEach((file) => { 204 | try { 205 | if (file.bytes instanceof Uint8Array) { 206 | instance.FS.writeFile(file.name, file.bytes) 207 | 208 | const timestamp = 209 | file.timestamp instanceof Date 210 | ? file.timestamp.getTime() 211 | : file.timestamp 212 | if (typeof timestamp === "number") 213 | instance.FS.utime(file.name, timestamp, timestamp) 214 | } 215 | } catch (e) { 216 | console.error(e.name + ": " + e.message) 217 | } 218 | }) 219 | } 220 | 221 | /* internal methods */ 222 | 223 | _loadEmscrJsRuntime(jsRuntime) { 224 | const emscrJsModuleName = "EmscrJSR_" + this.programName 225 | 226 | // try worker import 227 | if (this._isWorkerScope()) { 228 | // import js runtime 229 | let blob = new Blob([jsRuntime], { type: "application/javascript" }) 230 | importScripts(URL.createObjectURL(blob)) 231 | 232 | console.log(jsRuntime, blob) 233 | 234 | // read emscripten Module from js runtime 235 | this.#emscrJsRuntime = 236 | self[emscrJsModuleName] || self["_createPyodideModule"] 237 | // todo: find better solution for module names 238 | } 239 | 240 | // check if is in normal browser dom 241 | else if (typeof document != "undefined") { 242 | const jsRuntimeElemID = this.programName + "_emscrJSR" 243 | 244 | // inject js runtime if not done before 245 | if (!document.getElementById(jsRuntimeElemID)) { 246 | // create new script element for runtime 247 | let script = document.createElement("script") 248 | script.type = "text/javascript" 249 | script.id = jsRuntimeElemID 250 | 251 | // insert js runtime script into DOM 252 | script.innerHTML = new TextDecoder("utf-8").decode(jsRuntime) 253 | document.head.appendChild(script) 254 | } 255 | 256 | // read emscripten Module from js runtime 257 | this.#emscrJsRuntime = window[emscrJsModuleName] 258 | } else throw new Error("can not load emscr js runtime environment") 259 | } 260 | 261 | _isWorkerScope() { 262 | // checks if script is executed in worker or main thread 263 | return ( 264 | typeof WorkerGlobalScope != "undefined" && 265 | self instanceof WorkerGlobalScope 266 | ) 267 | } 268 | } 269 | 270 | export default EmscrWasmRunnable 271 | -------------------------------------------------------------------------------- /src/runnables/WasmerRunnable.js: -------------------------------------------------------------------------------- 1 | import { WASI } from "@wasmer/wasi" 2 | import browserBindings from "@wasmer/wasi/lib/bindings/browser" 3 | import { WasmFs } from "@wasmer/wasmfs" 4 | 5 | const S_IFCHR = 8192 // magic constant from memFS 6 | 7 | class WasmerRunnable { 8 | /* method wrapper class for wasmer wasm modules. 9 | initializes memory filesystem and can execute wasm. */ 10 | 11 | programName 12 | wasmModule 13 | 14 | constructor(programName, wasmModule) { 15 | this.programName = programName 16 | this.wasmModule = wasmModule 17 | } 18 | 19 | run( 20 | argv, 21 | stdin, 22 | stdout, 23 | stderr, 24 | files, 25 | onFinish, 26 | onError, 27 | onSuccess, 28 | stdinPreset 29 | ) { 30 | console.log("wasmer runnable run:", this.programName, argv) 31 | 32 | // initialize default methods and values 33 | if (typeof stdin != "function") stdin = () => {} 34 | if (typeof stdout != "function") stdout = () => {} 35 | if (typeof stderr != "function") stderr = () => {} 36 | if (!(files instanceof Array)) files = [] 37 | 38 | // initialize default callbacks 39 | if (typeof onFinish != "function") onFinish = () => {} 40 | if (typeof onError != "function") 41 | onError = (e) => { 42 | console.error(e) 43 | } 44 | if (typeof onSuccess != "function") onSuccess = () => {} 45 | 46 | // init new memory filesystem 47 | const wasmFs = new WasmFs() 48 | 49 | // write all files to wasmfs 50 | this._writeFilesToFS(wasmFs, files) 51 | 52 | // set /dev/stdin to stdin function 53 | wasmFs.volume.fds[0].node.read = stdin 54 | 55 | if (stdinPreset) { 56 | // with stdinPreset you can preset a value for stdin 57 | 58 | if (typeof stdinPreset != "string") 59 | stdinPreset = (stdinPreset || "").toString() 60 | 61 | let stdinCallCounter = 0 62 | wasmFs.volume.fds[0].node.read = (stdinBuffer) => { 63 | // second read means end of string 64 | if (stdinCallCounter % 2 !== 0) { 65 | stdinCallCounter++ 66 | return 0 67 | } 68 | 69 | // copy stdin preset to stdinBuffer 70 | for (let i = 0; i < stdinPreset.length; i++) 71 | stdinBuffer[i] = stdinPreset.charCodeAt(i) 72 | 73 | // indicate we've read once 74 | stdinCallCounter++ 75 | 76 | // return how much to read 77 | return stdinPreset.length 78 | } 79 | } 80 | 81 | // set /dev/stdout to stdout function 82 | wasmFs.volume.fds[1].node.write = ( 83 | stdoutBuffer, 84 | offset, 85 | length, 86 | position 87 | ) => { 88 | stdout(new TextDecoder("utf-8").decode(stdoutBuffer)) 89 | return stdoutBuffer.length 90 | } 91 | 92 | // set /dev/stderr to stderr function 93 | wasmFs.volume.fds[2].node.write = ( 94 | stderrBuffer, 95 | offset, 96 | length, 97 | position 98 | ) => { 99 | stderr(new TextDecoder("utf-8").decode(stderrBuffer)) 100 | return stderrBuffer.length 101 | } 102 | 103 | // map /dev/tty to /dev/stdin and /dev/stdout 104 | const ttyFd = wasmFs.volume.openSync("/dev/tty", "w+") 105 | wasmFs.volume.fds[ttyFd].node.read = wasmFs.volume.fds[0].node.read 106 | wasmFs.volume.fds[ttyFd].node.write = wasmFs.volume.fds[1].node.write 107 | 108 | // mark /dev/{stdin,stdout,stderr,tty} as character devices 109 | wasmFs.volume.fds[0].node.setModeProperty(S_IFCHR) 110 | wasmFs.volume.fds[1].node.setModeProperty(S_IFCHR) 111 | wasmFs.volume.fds[2].node.setModeProperty(S_IFCHR) 112 | wasmFs.volume.fds[ttyFd].node.setModeProperty(S_IFCHR) 113 | 114 | // create wasi runtime 115 | let wasi = new WASI({ 116 | args: [this.programName, ...argv], 117 | env: {}, // todo: maybe use environment variables? 118 | bindings: { 119 | ...browserBindings, 120 | fs: wasmFs.fs, 121 | }, 122 | preopens: { 123 | ".": ".", 124 | "/": "/", 125 | }, 126 | }) 127 | 128 | // instantiate wasm module 129 | const imports = wasi.getImports(this.wasmModule) // WebAssembly.Module.imports(this.wasmModule) 130 | WebAssembly.instantiate(this.wasmModule, { ...imports }).then( 131 | (instance) => { 132 | let filesPostRun 133 | try { 134 | // write submitted files to wasm 135 | this._writeFilesToFS(wasmFs, files) 136 | 137 | // execute command 138 | try { 139 | wasi.start(instance) 140 | } catch (e) { 141 | // make browserBindings not throw on code 0 (normal exit) 142 | if (e.code != 0) browserBindings.exit(e.code) 143 | } 144 | 145 | // read created files from wasm 146 | filesPostRun = this._readFilesFromFS(wasmFs) 147 | 148 | // success callback 149 | onSuccess(filesPostRun) 150 | } catch (e) { 151 | onError(e.message) 152 | } finally { 153 | onFinish(filesPostRun || files) 154 | } 155 | } 156 | ) 157 | } 158 | 159 | runHeadless(argv, files, onFinish, onError, onSuccess, stdinPreset) { 160 | console.log("wasmer runnable run headless:", this.programName, argv) 161 | 162 | // initialize default callback 163 | if (typeof onFinish != "function") onFinish = () => {} 164 | 165 | // stdin is not needed 166 | const stdin = () => { 167 | console.log("called runHeadless stdin") 168 | return 0 169 | } 170 | 171 | // output is redirected into buffer 172 | let outputBuffer = "", 173 | stdoutBuffer = "", 174 | stderrBuffer = "" 175 | const stdout = (stdoutVal) => { 176 | outputBuffer += stdoutVal 177 | stdoutBuffer += stdoutVal 178 | return stdoutVal.length 179 | } 180 | const stderr = (stderrVal) => { 181 | outputBuffer += stderrVal 182 | stderrBuffer += stderrVal 183 | return stderrVal.length 184 | } 185 | 186 | // run command with custom input/output 187 | this.run( 188 | argv, 189 | stdin, 190 | stdout, 191 | stderr, 192 | files, 193 | () => 194 | onFinish({ 195 | output: outputBuffer, 196 | stdout: stdoutBuffer, 197 | stderr: stderrBuffer, 198 | }), 199 | onError, 200 | onSuccess, 201 | stdinPreset 202 | ) 203 | } 204 | 205 | /* file handling */ 206 | 207 | _readFilesFromFS(wasmFs, directory = "/", includeBinary = true) { 208 | const rootLink = wasmFs.volume.getLinkAsDirOrThrow(directory) 209 | 210 | const getFilesFromLink = (parentLink) => { 211 | let files = [] 212 | Object.values(parentLink.children).forEach((link) => { 213 | let linkPath = link.getPath() 214 | let node = link.getNode() 215 | if (node.isFile()) 216 | files.push({ 217 | name: linkPath, 218 | timestamp: node.mtime.getTime(), 219 | bytes: includeBinary 220 | ? wasmFs.fs.readFileSync(linkPath) 221 | : new Uint8Array(), 222 | }) 223 | if (node.isDirectory()) files = [...files, ...getFilesFromLink(link)] 224 | }) 225 | return files 226 | } 227 | 228 | let files = getFilesFromLink(rootLink) 229 | return files 230 | } 231 | 232 | _writeFilesToFS(wasmFs, files = []) { 233 | files.forEach((file) => { 234 | try { 235 | if (file.bytes instanceof Uint8Array) { 236 | const path = file.name.split("/").slice(0, -1).join("/") 237 | wasmFs.fs.mkdirSync(path, { recursive: true }) 238 | wasmFs.fs.writeFileSync(file.name, file.bytes) 239 | 240 | if (file.timestamp instanceof Date) 241 | wasmFs.fs.utimesSync(file.name, file.timestamp, file.timestamp) 242 | else if (typeof file.timestamp === "number") 243 | wasmFs.fs.utimesSync( 244 | file.name, 245 | file.timestamp / 1000, 246 | file.timestamp / 1000 247 | ) 248 | } 249 | } catch (e) { 250 | console.error(e.name + ": " + e.message) 251 | } 252 | }) 253 | } 254 | } 255 | 256 | export default WasmerRunnable 257 | -------------------------------------------------------------------------------- /src/runners/ImportInjectedModules.js: -------------------------------------------------------------------------------- 1 | // HACK: Inject all the modules that have been forward from the parent thread into 2 | // our own bundle. This has to be done before importing any of these modules. 3 | const existingModules = Object.keys(__webpack_modules__) 4 | 5 | for (const [id, mod] of Object.entries(global._modules)) { 6 | if (!existingModules.includes(id)) __webpack_modules__[id] = mod 7 | } 8 | 9 | // HACK: Make the lexicographic first export of the `WasmRunner` module available 10 | // as the default export. Webpack does mangle/minify names longer than 2 characters. 11 | const WasmRunnerModule = __webpack_require__(global.WasmRunnerID) 12 | const defaultKey = Object.keys(WasmRunnerModule)[0] 13 | self.WasmRunner = WasmRunnerModule[defaultKey] 14 | -------------------------------------------------------------------------------- /src/runners/WasmRunner.js: -------------------------------------------------------------------------------- 1 | import EmscrWasmRunnable from "../runnables/EmscriptenRunnable" 2 | import WasmerRunnable from "../runnables/WasmerRunnable" 3 | 4 | /* executes wasm on the main thread. asks for stdin by using prompts. 5 | this class will be extented by WasmWorker to run on worker thread. */ 6 | 7 | class WasmRunner { 8 | outputBuffer 9 | 10 | constructor() { 11 | this.outputBuffer = "" 12 | } 13 | 14 | runCommand( 15 | programName, 16 | wasmModule, 17 | wasmModuleType, 18 | argv, 19 | stdinProxy, 20 | stdoutProxy, 21 | stderrProxy, 22 | files, 23 | onFinish, 24 | onError, 25 | onSuccess, 26 | stdinPreset, 27 | emscrJsRuntime 28 | ) { 29 | // initialize default callbacks 30 | if (typeof onFinish != "function") onFinish = () => {} 31 | if (typeof onError != "function") 32 | onError = (e) => { 33 | console.error(e) 34 | } 35 | if (typeof onSuccess != "function") onSuccess = () => {} 36 | 37 | // store outputs of stdout in buffer (for stdin prompts) 38 | const bufferOutputs = (value) => { 39 | this.outputBuffer += 40 | typeof value == "number" ? String.fromCharCode(value) : value 41 | } 42 | const stdoutHandler = (value) => { 43 | bufferOutputs(value) 44 | return stdoutProxy(value) 45 | } 46 | const stderrHandler = (value) => { 47 | bufferOutputs(value) 48 | return stderrProxy(value) 49 | } 50 | 51 | if (wasmModuleType == "emscripten") { 52 | // instantiate new emscr runnable 53 | console.log("wasm runner creates new emscr runnable") 54 | let emscrWasmExe = new EmscrWasmRunnable( 55 | programName, 56 | wasmModule, 57 | emscrJsRuntime 58 | ) 59 | 60 | // pipe stdin calls through stdin handler (which pauses thread) 61 | const stdinHandler = (tty) => 62 | this._onEmscrStdinCall(tty, stdinProxy, stdoutHandler, stderrHandler) 63 | 64 | // run command on it 65 | emscrWasmExe.run( 66 | argv, 67 | stdinHandler, 68 | stdoutHandler, 69 | stderrHandler, 70 | files, 71 | onFinish, 72 | onError, 73 | onSuccess, 74 | stdinPreset 75 | ) 76 | } else if (wasmModuleType == "wasmer") { 77 | // instantiate new wasmer runnable 78 | console.log("wasm runner creates new wasmer runnable") 79 | let wasmerExe = new WasmerRunnable(programName, wasmModule) 80 | 81 | // pipe stdin calls through stdin handler (which pauses thread) 82 | const stdinHandler = (stdinBuffer) => 83 | this._onWasmerStdinCall( 84 | stdinBuffer, 85 | stdinProxy, 86 | stdoutHandler, 87 | stderrHandler 88 | ) 89 | 90 | // run command on it 91 | wasmerExe.run( 92 | argv, 93 | stdinHandler, 94 | stdoutHandler, 95 | stderrHandler, 96 | files, 97 | onFinish, 98 | onError, 99 | onSuccess, 100 | stdinPreset 101 | ) 102 | } else 103 | throw new Error( 104 | "Unknown wasm module type (can only handle emscripten or wasmer)" 105 | ) 106 | } 107 | 108 | runCommandHeadless( 109 | programName, 110 | wasmModule, 111 | wasmModuleType, 112 | argv, 113 | files, 114 | onFinish, 115 | onError, 116 | onSuccess, 117 | stdinPreset, 118 | emscrJsRuntime 119 | ) { 120 | // initialize default callbacks 121 | if (typeof onFinish != "function") onFinish = () => {} 122 | if (typeof onError != "function") 123 | onError = (e) => { 124 | console.error(e) 125 | } 126 | if (typeof onSuccess != "function") onSuccess = () => {} 127 | 128 | if (wasmModuleType == "emscripten") { 129 | // instantiate new emscr runnable 130 | console.log("wasm runner creates new emscr runnable") 131 | let emscrWasmExe = new EmscrWasmRunnable( 132 | programName, 133 | wasmModule, 134 | emscrJsRuntime 135 | ) 136 | 137 | // run command on it 138 | emscrWasmExe.runHeadless( 139 | argv, 140 | files, 141 | onFinish, 142 | onError, 143 | onSuccess, 144 | stdinPreset 145 | ) 146 | } else if (wasmModuleType == "wasmer") { 147 | // instantiate new wasmer runnable 148 | let wasmerExe = new WasmerRunnable(programName, wasmModule) 149 | 150 | // run command on it 151 | wasmerExe.runHeadless( 152 | argv, 153 | files, 154 | onFinish, 155 | onError, 156 | onSuccess, 157 | stdinPreset 158 | ) 159 | } else 160 | throw new Error( 161 | "Unknown wasm module type (can only handle emscripten or wasmer)" 162 | ) 163 | } 164 | 165 | // handles stdin calls from emscr (tty is passed by emscr js runtime) 166 | _onEmscrStdinCall(tty, stdinProxy, stdoutProxy, stderrProxy) { 167 | if (tty.input.length == 0) { 168 | // use last line from output buffer as prompt caption 169 | const promptCaption = this.outputBuffer.split(/\r?\n/g).pop() 170 | 171 | // get input from user via prompt 172 | const input = window.prompt(promptCaption) 173 | 174 | // if aborted -> end 175 | if (input == null) return null 176 | 177 | // print input to terminal 178 | stdoutProxy(input) 179 | if (!promptCaption) stdoutProxy("\r\n") 180 | 181 | // copy input value to tty input 182 | tty.input = (input + "\n").split("").map((char) => char.charCodeAt(0)) 183 | tty.input.push(null) // marks end 184 | } 185 | 186 | // deliver input 187 | return tty.input.shift() 188 | } 189 | 190 | // handles stdin calls from wasmer 191 | _onWasmerStdinCall(stdinBuffer, stdinProxy, stdoutProxy, stderrProxy) { 192 | // use last line from output buffer as prompt caption 193 | const promptCaption = this.outputBuffer.split(/\r?\n/g).pop() 194 | 195 | // get input from user via prompt 196 | const input = window.prompt(promptCaption) 197 | 198 | // if aborted -> end 199 | if (input == null) return 0 200 | 201 | // print input to terminal 202 | stdoutProxy(input) 203 | if (!promptCaption) stdoutProxy("\r\n") 204 | 205 | // copy input value to stdinBuffer 206 | Array.from(input + "\n").forEach( 207 | (char, i) => (stdinBuffer[i] = char.charCodeAt(0)) 208 | ) 209 | 210 | // return how much to read 211 | return input.length + 1 212 | } 213 | } 214 | 215 | export default WasmRunner 216 | export const MODULE_ID = __webpack_module__.id 217 | -------------------------------------------------------------------------------- /src/runners/WasmWorker.js: -------------------------------------------------------------------------------- 1 | import { expose } from "comlink" 2 | // HACK: make `WasmRunner` and its dependecies available from the injected modules by the 3 | // main thread, instead of importing and bundling them directly. 4 | import "./ImportInjectedModules" 5 | import WasmRunner from "./WasmRunner" 6 | 7 | class WasmWorker extends WasmRunner { 8 | #pauseBuffer 9 | #stdinBuffer 10 | 11 | constructor(pauseBuffer, stdinBuffer) { 12 | super() 13 | // buffers can be accessed both from the main thread and the worker 14 | this.#pauseBuffer = pauseBuffer // used to pause/resume worker execution 15 | this.#stdinBuffer = stdinBuffer // used to pass user inputs to worker 16 | } 17 | 18 | // note: running commands is handled by parent class 19 | 20 | /* pausing and resuming */ 21 | 22 | pauseExecution() { 23 | console.log("pausing worker execution") 24 | Atomics.store(this.#pauseBuffer, 0, 1) // mem[0] = 1 (means hold) 25 | Atomics.wait(this.#pauseBuffer, 0, 1) // wait while value is 1 26 | console.log("resuming worker execution") 27 | } 28 | 29 | resumeExecution() { 30 | console.log("resuming worker execution") 31 | Atomics.store(this.#pauseBuffer, 0, 0) // mem[0] = 0 (means do not hold) 32 | // note: this method is just for completeness (the worker will be 33 | // resumed from outside by changing the pause buffer value) 34 | } 35 | 36 | /* input output handling */ 37 | 38 | #readStdinBuffer(index = null) { 39 | // null = read all 40 | if (index != null) return Atomics.load(this.#stdinBuffer, index) 41 | let result = [] 42 | for (let i = 0; i < this.#stdinBuffer.length; i++) { 43 | const value = Atomics.load(this.#stdinBuffer, i) 44 | if (value === 0) break // 0 marks end of input 45 | result.push(value) 46 | } 47 | return result 48 | } 49 | 50 | // handles stdin calls from emscr (pause -> call stdin proxy -> deliver) 51 | _onEmscrStdinCall(tty, stdinProxy, stdoutProxy, stderrProxy) { 52 | if (tty.input.length == 0) { 53 | // read input (will set stdin buffer) 54 | stdinProxy(this.outputBuffer.split(/\r?\n/g).pop()) 55 | this.pauseExecution() // resumes after input 56 | 57 | // copy stdin buffer to tty input 58 | tty.input = this.#readStdinBuffer() 59 | this.outputBuffer += tty.input.map((c) => String.fromCharCode(c)).join("") 60 | 61 | if (tty.input.length == 0) return null 62 | else tty.input.push(null) // marks end 63 | } 64 | 65 | // deliver input 66 | return tty.input.shift() 67 | } 68 | 69 | // handles stdin calls from wasmer 70 | _onWasmerStdinCall(stdinBuffer, stdinProxy, stdoutProxy, stderrProxy) { 71 | // read input (will set stdin buffer) 72 | stdinProxy(this.outputBuffer.split(/\r?\n/g).pop()) 73 | this.pauseExecution() // resumes after input 74 | 75 | // copy stdin buffer to stdinBuffer 76 | const _stdinBuffer = this.#readStdinBuffer() 77 | _stdinBuffer.forEach((char, i) => (stdinBuffer[i] = char)) 78 | this.outputBuffer += _stdinBuffer 79 | .map((c) => String.fromCharCode(c)) 80 | .join("") 81 | 82 | // return how much to read 83 | return _stdinBuffer.length 84 | } 85 | 86 | /* expose (sets up comlink communication) */ 87 | 88 | static expose() { 89 | expose(WasmWorker) 90 | } 91 | 92 | static isWorkerScope() { 93 | // checks if script is executed in worker or main thread 94 | return ( 95 | typeof WorkerGlobalScope != "undefined" && 96 | self instanceof WorkerGlobalScope 97 | ) 98 | } 99 | } 100 | 101 | // if this runs in worker scope -> expose 102 | if (WasmWorker.isWorkerScope()) WasmWorker.expose() 103 | 104 | export default WasmWorker 105 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config to bundle WasmWebTerm 3 | * Generates the files "webterm.bundle.js" and "webterm.bundle.js.map" 4 | */ 5 | 6 | const webpack = require("webpack") 7 | 8 | module.exports = { 9 | entry: { 10 | WasmWebTerm: "./src/WasmWebTerm.js", 11 | }, 12 | output: { 13 | path: __dirname, 14 | filename: "webterm.bundle.js", 15 | library: { type: "umd", name: "[name]" }, 16 | }, 17 | optimization: { 18 | moduleIds: "deterministic", // share deterministic ids with the worker bundle 19 | }, 20 | plugins: [ 21 | new webpack.ProvidePlugin({ 22 | Buffer: ["buffer", "Buffer"], 23 | process: "process/browser", 24 | }), 25 | new webpack.DefinePlugin({ 26 | __VERSION__: JSON.stringify(process.env.npm_package_version), 27 | }), 28 | ], 29 | module: { 30 | rules: [ 31 | { 32 | test: __dirname + "/src/runners/WasmWorker.js", 33 | use: [{ loader: "worker-loader" }], 34 | type: "asset/source", 35 | }, 36 | ], 37 | }, 38 | resolveLoader: { 39 | alias: { "worker-loader": __dirname + "/worker.loader.js" }, 40 | }, 41 | devtool: "source-map", 42 | } 43 | -------------------------------------------------------------------------------- /worker.loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack loader to prebundle WasmWorker. 3 | * This eliminates the need of a separate webworker js file. 4 | */ 5 | 6 | const path = require("path") 7 | const webpack = require("webpack") 8 | const memfs = require("memfs") 9 | 10 | function bytesToBase64DataUrl(bytes, type = "application/octet-stream") { 11 | const buffer = Buffer.from(bytes) 12 | const encoded = buffer.toString("base64") 13 | return `data:${type};base64,${encoded}` 14 | } 15 | 16 | async function compress(bytes, method = "gzip") { 17 | const blob = new Blob([bytes]) 18 | const stream = blob.stream().pipeThrough(new CompressionStream(method)) 19 | const response = new Response(stream, { 20 | headers: { "Content-Type": `application/${method}` }, 21 | }) 22 | return response.arrayBuffer() 23 | } 24 | 25 | module.exports = function (source) { 26 | const inputFilename = "./src/runners/WasmWorker.js" 27 | const outputFilename = "worker.compiled.js" 28 | 29 | // create webpack compiler 30 | let compiler = webpack({ 31 | entry: inputFilename, 32 | output: { 33 | path: "/", 34 | filename: outputFilename, 35 | library: { 36 | type: "umd", 37 | name: "[name]", 38 | }, 39 | }, 40 | optimization: { 41 | moduleIds: "deterministic", // share deterministic ids with the main bundle 42 | }, 43 | externals: { 44 | // HACK: Do not bundle the `WasmRunner` module and its dependencies again. 45 | // Instead let the main thread forward it from its bundle when 46 | // instantiating the worker. 47 | "./WasmRunner": "global WasmRunner", 48 | }, 49 | plugins: [ 50 | new webpack.ProvidePlugin({ 51 | Buffer: ["buffer", "Buffer"], 52 | process: "process/browser", 53 | }), 54 | ], 55 | }) 56 | 57 | // make compiler use memfs as *output* file system 58 | compiler.outputFileSystem = memfs.createFsFromVolume(new memfs.Volume()) 59 | compiler.outputFileSystem.join = path.join.bind(path) 60 | 61 | return new Promise((resolve, reject) => { 62 | // compile webworker 63 | compiler.run(async (error, stats) => { 64 | // exit on errors 65 | if (error != null) reject(error) 66 | if (stats?.hasErrors()) reject(stats.compilation.errors) 67 | 68 | // read compiled bundle from file system and resolve 69 | try { 70 | const compiled = compiler.outputFileSystem.readFileSync( 71 | "/" + outputFilename, 72 | "utf-8" 73 | ) 74 | const compressed = await compress(compiled, "gzip") 75 | const encoded = bytesToBase64DataUrl(compressed, "application/gzip") 76 | console.log( 77 | "Worker size:", 78 | Math.ceil((compiled.length / 1024) * 10) / 10, 79 | "KiB,", 80 | Math.ceil((encoded.length / 1024) * 10) / 10, 81 | "KiB compressed" 82 | ) 83 | resolve(encoded) 84 | } catch (e) { 85 | console.error("Errors while compiling with worker.loader.js:", e) 86 | } 87 | }) 88 | }) 89 | } 90 | --------------------------------------------------------------------------------