Compare commits
44 Commits
1.32.0-310
...
1.604-vsc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd7281fa0 | ||
|
|
e12fcd3a0d | ||
|
|
4af84fcaf6 | ||
|
|
c607015a26 | ||
|
|
217515344e | ||
|
|
dcf409aecb | ||
|
|
2683b7c734 | ||
|
|
3a672d725a | ||
|
|
f484781693 | ||
|
|
97f5b07003 | ||
|
|
7481395353 | ||
|
|
033ef151ca | ||
|
|
3fec7f432c | ||
|
|
4887078423 | ||
|
|
91deaece47 | ||
|
|
03ad2a17b2 | ||
|
|
a4cca6b759 | ||
|
|
6105bba0a4 | ||
|
|
259095eae2 | ||
|
|
38a0706b18 | ||
|
|
c7ae12c2ed | ||
|
|
3331f9b28d | ||
|
|
def4104c53 | ||
|
|
4eb5331ddc | ||
|
|
3bb5c0bbe5 | ||
|
|
83aa952de2 | ||
|
|
e0d33f2399 | ||
|
|
194cbca0f2 | ||
|
|
1697cc32a3 | ||
|
|
f058f90340 | ||
|
|
ca4b0346cb | ||
|
|
8d692ded4a | ||
|
|
dc2253e718 | ||
|
|
d16c6aeb30 | ||
|
|
cdc40d36ff | ||
|
|
80c19878e0 | ||
|
|
18f395b853 | ||
|
|
75435be949 | ||
|
|
ce73bc58e5 | ||
|
|
70219d1071 | ||
|
|
e9e0bf7d84 | ||
|
|
3da1dccf73 | ||
|
|
e02101c676 | ||
|
|
ffc47054dd |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1,2 +1,2 @@
|
||||
* @coderasher @kylecarbs
|
||||
* @code-asher @kylecarbs
|
||||
Dockerfile @nhooyr
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
lib
|
||||
/lib
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
.DS_Store
|
||||
release
|
||||
.cache
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
8.15.0
|
||||
11
.travis.yml
11
.travis.yml
@@ -2,15 +2,16 @@ language: node_js
|
||||
node_js:
|
||||
- 8.15.0
|
||||
env:
|
||||
- VERSION="1.32.0-$TRAVIS_BUILD_NUMBER"
|
||||
- VSCODE_VERSION="1.32.0" MAJOR_VERSION="1" VERSION="$MAJOR_VERSION.$TRAVIS_BUILD_NUMBER-vsc$VSCODE_VERSION"
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
dist: ubuntu
|
||||
dist: trusty
|
||||
- os: osx
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install libxkbfile-dev
|
||||
libsecret-1-dev; fi
|
||||
- npm install -g yarn@1.12.3
|
||||
script:
|
||||
- scripts/build.sh
|
||||
before_deploy:
|
||||
@@ -35,4 +36,8 @@ deploy:
|
||||
on:
|
||||
repo: codercom/code-server
|
||||
branch: master
|
||||
cache: yarn
|
||||
cache:
|
||||
yarn: true
|
||||
timeout: 1000
|
||||
directories:
|
||||
- .cache
|
||||
|
||||
@@ -30,4 +30,5 @@ RUN locale-gen en_US.UTF-8
|
||||
# We unfortunately cannot use update-locale because docker will not use the env variables
|
||||
# configured in /etc/default/locale so we need to set it manually.
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENTRYPOINT ["code-server"]
|
||||
|
||||
11
README.md
11
README.md
@@ -2,8 +2,8 @@
|
||||
|
||||
[](https://github.com/codercom/code-server/issues)
|
||||
[](https://github.com/codercom/code-server/releases/latest)
|
||||
[](#)
|
||||
[](https://discord.gg/zxSwN8Z)
|
||||
[](https://github.com/codercom/code-server/blob/master/LICENSE)
|
||||
[](https://discord.gg/zxSwN8Z)
|
||||
|
||||
`code-server` is [VS Code](https://github.com/Microsoft/vscode) running on a remote server, accessible through the browser.
|
||||
|
||||
@@ -57,11 +57,16 @@ How to [secure your setup](/doc/security/ssl.md).
|
||||
- Creating custom VS Code extensions and debugging them doesn't work.
|
||||
|
||||
### Future
|
||||
|
||||
- **Stay up to date!** Get notified about new releases of code-server.
|
||||

|
||||
- Windows support.
|
||||
- Electron and Chrome OS applications to bridge the gap between local<->remote.
|
||||
- Run VS Code unit tests against our builds to ensure features work as expected.
|
||||
|
||||
### Extensions
|
||||
|
||||
At the moment we can't use the official VSCode Marketplace. We've created a custom extension marketplace focused around open-sourced extensions. However, if you have access to the `.vsix` file, you can manually install the extension.
|
||||
|
||||
## Contributing
|
||||
|
||||
Development guides are coming soon.
|
||||
|
||||
158
build/tasks.ts
158
build/tasks.ts
@@ -4,21 +4,22 @@ import * as fse from "fs-extra";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as zlib from "zlib";
|
||||
import * as https from "https";
|
||||
import * as tar from "tar";
|
||||
|
||||
const isWin = os.platform() === "win32";
|
||||
const libPath = path.join(__dirname, "../lib");
|
||||
const vscodePath = path.join(libPath, "vscode");
|
||||
const defaultExtensionsPath = path.join(libPath, "extensions");
|
||||
const pkgsPath = path.join(__dirname, "../packages");
|
||||
const defaultExtensionsPath = path.join(libPath, "VSCode-linux-x64/resources/app/extensions");
|
||||
const vscodeVersion = "1.32.0";
|
||||
const vscodeVersion = process.env.VSCODE_VERSION || "1.32.0";
|
||||
const vsSourceUrl = `https://codesrv-ci.cdr.sh/vstar-${vscodeVersion}.tar.gz`;
|
||||
|
||||
const buildServerBinary = register("build:server:binary", async (runner) => {
|
||||
await ensureInstalled();
|
||||
await copyForDefaultExtensions();
|
||||
await Promise.all([
|
||||
buildBootstrapFork(),
|
||||
buildWeb(),
|
||||
buildDefaultExtensions(),
|
||||
buildServerBundle(),
|
||||
buildAppBrowser(),
|
||||
]);
|
||||
@@ -33,50 +34,12 @@ const buildServerBinaryPackage = register("build:server:binary:package", async (
|
||||
throw new Error("Cannot build binary without server bundle built");
|
||||
}
|
||||
await buildServerBinaryCopy();
|
||||
await dependencyNexeBinary();
|
||||
const resp = await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build:nexe"]);
|
||||
const resp = await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build:binary"]);
|
||||
if (resp.exitCode !== 0) {
|
||||
throw new Error(`Failed to package binary: ${resp.stderr}`);
|
||||
}
|
||||
});
|
||||
|
||||
const dependencyNexeBinary = register("dependency:nexe", async (runner) => {
|
||||
if (os.platform() === "linux" && process.env.COMPRESS === "true") {
|
||||
// Download the nexe binary so we can compress it before nexe runs. If we
|
||||
// don't want compression we don't need to do anything since nexe will take
|
||||
// care of getting the binary.
|
||||
const nexeDir = path.join(os.homedir(), ".nexe");
|
||||
const targetBinaryName = `${os.platform()}-${os.arch()}-${process.version.substr(1)}`;
|
||||
const targetBinaryPath = path.join(nexeDir, targetBinaryName);
|
||||
if (!fs.existsSync(targetBinaryPath)) {
|
||||
fse.mkdirpSync(nexeDir);
|
||||
runner.cwd = nexeDir;
|
||||
await runner.execute("wget", [`https://github.com/nexe/nexe/releases/download/v3.0.0-beta.15/${targetBinaryName}`]);
|
||||
await runner.execute("chmod", ["+x", targetBinaryPath]);
|
||||
}
|
||||
// Compress with upx if it doesn't already look compressed.
|
||||
if (fs.statSync(targetBinaryPath).size >= 20000000) {
|
||||
// It needs to be executable for upx to work, which it might not be if
|
||||
// nexe downloaded it.
|
||||
fs.chmodSync(targetBinaryPath, "755");
|
||||
const upxFolder = path.join(os.tmpdir(), "upx");
|
||||
const upxBinary = path.join(upxFolder, "upx");
|
||||
if (!fs.existsSync(upxBinary)) {
|
||||
fse.mkdirpSync(upxFolder);
|
||||
runner.cwd = upxFolder;
|
||||
const upxExtract = await runner.execute("bash", ["-c", "curl -L https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz | tar xJ --strip-components=1"]);
|
||||
if (upxExtract.exitCode !== 0) {
|
||||
throw new Error(`Failed to extract upx: ${upxExtract.stderr}`);
|
||||
}
|
||||
}
|
||||
if (!fs.existsSync(upxBinary)) {
|
||||
throw new Error("Not sure how, but the UPX binary does not exist");
|
||||
}
|
||||
await runner.execute(upxBinary, [targetBinaryPath]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const buildServerBinaryCopy = register("build:server:binary:copy", async (runner) => {
|
||||
const cliPath = path.join(pkgsPath, "server");
|
||||
const cliBuildPath = path.join(cliPath, "build");
|
||||
@@ -167,97 +130,50 @@ const buildWeb = register("build:web", async (runner) => {
|
||||
await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build"]);
|
||||
});
|
||||
|
||||
const extDirPath = path.join("lib", "vscode-default-extensions");
|
||||
const copyForDefaultExtensions = register("build:copy-vscode", async (runner) => {
|
||||
if (!fs.existsSync(defaultExtensionsPath)) {
|
||||
await ensureClean();
|
||||
await ensureInstalled();
|
||||
await new Promise((resolve, reject): void => {
|
||||
fse.remove(extDirPath, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
await new Promise((resolve, reject): void => {
|
||||
fse.copy(vscodePath, extDirPath, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const buildDefaultExtensions = register("build:default-extensions", async (runner) => {
|
||||
if (!fs.existsSync(defaultExtensionsPath)) {
|
||||
await copyForDefaultExtensions();
|
||||
runner.cwd = extDirPath;
|
||||
const resp = await runner.execute(isWin ? "npx.cmd" : "npx", [isWin ? "gulp.cmd" : "gulp", "vscode-linux-x64", "--max-old-space-size=32384"]);
|
||||
if (resp.exitCode !== 0) {
|
||||
throw new Error(`Failed to build default extensions: ${resp.stderr}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const ensureInstalled = register("vscode:install", async (runner) => {
|
||||
await ensureCloned();
|
||||
runner.cwd = libPath;
|
||||
|
||||
runner.cwd = vscodePath;
|
||||
const install = await runner.execute(isWin ? "yarn.cmd" : "yarn", []);
|
||||
if (install.exitCode !== 0) {
|
||||
throw new Error(`Failed to install vscode dependencies: ${install.stderr}`);
|
||||
}
|
||||
});
|
||||
if (fs.existsSync(vscodePath) && fs.existsSync(defaultExtensionsPath)) {
|
||||
const pkgVersion = JSON.parse(fs.readFileSync(path.join(vscodePath, "package.json")).toString("utf8")).version;
|
||||
if (pkgVersion === vscodeVersion) {
|
||||
runner.cwd = vscodePath;
|
||||
|
||||
const ensureCloned = register("vscode:clone", async (runner) => {
|
||||
if (fs.existsSync(vscodePath)) {
|
||||
await ensureClean();
|
||||
} else {
|
||||
fse.mkdirpSync(libPath);
|
||||
runner.cwd = libPath;
|
||||
const clone = await runner.execute("git", ["clone", "https://github.com/microsoft/vscode", "--branch", vscodeVersion, "--single-branch", "--depth=1"]);
|
||||
if (clone.exitCode !== 0) {
|
||||
throw new Error(`Failed to clone: ${clone.exitCode}`);
|
||||
const reset = await runner.execute("git", ["reset", "--hard"]);
|
||||
if (reset.exitCode !== 0) {
|
||||
throw new Error(`Failed to clean git repository: ${reset.stderr}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
runner.cwd = vscodePath;
|
||||
const checkout = await runner.execute("git", ["checkout", vscodeVersion]);
|
||||
if (checkout.exitCode !== 0) {
|
||||
throw new Error(`Failed to checkout: ${checkout.stderr}`);
|
||||
}
|
||||
});
|
||||
fse.removeSync(libPath);
|
||||
fse.mkdirpSync(libPath);
|
||||
|
||||
const ensureClean = register("vscode:clean", async (runner) => {
|
||||
runner.cwd = vscodePath;
|
||||
await new Promise<void>((resolve, reject): void => {
|
||||
https.get(vsSourceUrl, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
return reject(res.statusMessage);
|
||||
}
|
||||
|
||||
const status = await runner.execute("git", ["status", "--porcelain"]);
|
||||
if (status.stdout.trim() !== "") {
|
||||
const clean = await runner.execute("git", ["clean", "-f", "-d", "-X"]);
|
||||
if (clean.exitCode !== 0) {
|
||||
throw new Error(`Failed to clean git repository: ${clean.stderr}`);
|
||||
}
|
||||
const removeUnstaged = await runner.execute("git", ["checkout", "--", "."]);
|
||||
if (removeUnstaged.exitCode !== 0) {
|
||||
throw new Error(`Failed to remove unstaged files: ${removeUnstaged.stderr}`);
|
||||
}
|
||||
}
|
||||
const fetch = await runner.execute("git", ["fetch", "--prune"]);
|
||||
if (fetch.exitCode !== 0) {
|
||||
throw new Error(`Failed to fetch latest changes: ${fetch.stderr}`);
|
||||
}
|
||||
res.pipe(tar.x({
|
||||
C: libPath,
|
||||
}).on("finish", () => {
|
||||
resolve();
|
||||
}).on("error", (err: Error) => {
|
||||
reject(err);
|
||||
}));
|
||||
}).on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const ensurePatched = register("vscode:patch", async (runner) => {
|
||||
if (!fs.existsSync(vscodePath)) {
|
||||
throw new Error("vscode must be cloned to patch");
|
||||
}
|
||||
await ensureClean();
|
||||
await ensureInstalled();
|
||||
|
||||
runner.cwd = vscodePath;
|
||||
const patchPath = path.join(__dirname, "../scripts/vscode.patch");
|
||||
@@ -274,7 +190,7 @@ register("package", async (runner, releaseTag) => {
|
||||
|
||||
const releasePath = path.resolve(__dirname, "../release");
|
||||
|
||||
const archiveName = `code-server-${releaseTag}-${os.platform()}-${os.arch()}`;
|
||||
const archiveName = `code-server${releaseTag}-${os.platform()}-${os.arch()}`;
|
||||
const archiveDir = path.join(releasePath, archiveName);
|
||||
fse.removeSync(archiveDir);
|
||||
fse.mkdirpSync(archiveDir);
|
||||
|
||||
@@ -56,7 +56,7 @@ If you're just starting out, we recommend [installing code-server locally](../..
|
||||
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../../security/ssl.md)
|
||||
- Finally, run
|
||||
```
|
||||
sudo ./code-server-linux -p 80
|
||||
sudo ./code-server -p 80
|
||||
```
|
||||
- When you visit the public IP for your AWS instance, you will be greeted with this page. Code-server is using a self-signed SSL certificate for easy setup. To proceed to the IDE, click **"Advanced"**<img src ="../../assets/chrome_warning.png">
|
||||
- Then click **"proceed anyway"**<img src="../../assets/chrome_confirm.png">
|
||||
|
||||
@@ -54,7 +54,7 @@ chmod +x code-server
|
||||
|
||||
- Start the code-server
|
||||
```
|
||||
sudo ./code-server-linux -p 80
|
||||
sudo ./code-server -p 80
|
||||
```
|
||||
|
||||
> For instructions on how to keep the server running after you end your SSH session please checkout [how to use systemd](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) to start linux based services if they are killed
|
||||
|
||||
BIN
doc/assets/cros.png
Normal file
BIN
doc/assets/cros.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
BIN
doc/assets/release.gif
Normal file
BIN
doc/assets/release.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
53
doc/self-hosted/cros-install.md
Normal file
53
doc/self-hosted/cros-install.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Installng code-server in your ChromiumOS/ChromeOS/CloudReady machine
|
||||
|
||||
This guide will show you how to install code-server into your CrOS machine.
|
||||
|
||||
## Using Crostini
|
||||
|
||||
One of the easier ways to run code-server is via [Crostini](https://www.aboutchromebooks.com/tag/project-crostini/), the Linux apps support feature in CrOS. Make sure you have enough RAM, HDD space and your CPU has VT-x/ AMD-V support. If your chromebook has this, then you are qualified to use Crostini.
|
||||
|
||||
If you are running R69, you might want to enable this on [Chrome Flags](chrome://flags/#enable-experimental-crostini-ui). If you run R72, however, this is already enabled for you.
|
||||
|
||||
After checking your prerequisites, follow the steps in [the self-host install guide](index.md) on installing code-server. Once done, make sure code-server works by running it. After running it, simply go to `penguin.linux.test:8443` to access code-server. Now you should be greeted with this screen. If you did, congratulations, you have installed code-server in your Chromebook!
|
||||
|
||||

|
||||
|
||||
Alternatively, if you ran code-server in another container and you need the IP for that specific container, simply go to Termina's shell via `crosh` and type `vsh termina`.
|
||||
|
||||
```bash
|
||||
Loading extra module: /usr/share/crosh/dev.d/50-crosh.sh
|
||||
Welcome to crosh, the Chrome OS developer shell.
|
||||
|
||||
If you got here by mistake, don't panic! Just close this tab and carry on.
|
||||
|
||||
Type 'help' for a list of commands.
|
||||
|
||||
If you want to customize the look/behavior, you can use the options page.
|
||||
Load it by using the Ctrl+Shift+P keyboard shortcut.
|
||||
|
||||
crosh> vsh termina
|
||||
(termina) chronos@localhost ~ $
|
||||
```
|
||||
While in termina, run `lxc list`. It should output the list of running containers.
|
||||
|
||||
```bash
|
||||
(termina) chronos@localhost ~ $ lxc list
|
||||
+---------+---------+-----------------------+------+------------+-----------+
|
||||
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
|
||||
+---------+---------+-----------------------+------+------------+-----------+
|
||||
| penguin | RUNNING | 100.115.92.199 (eth0) | | PERSISTENT | 0 |
|
||||
+---------+---------+-----------------------+------+------------+-----------+
|
||||
(termina) chronos@localhost ~ $
|
||||
```
|
||||
|
||||
For this example, we show the default `penguin` container, which is exposed on `eth0` at 100.115.92.199. Simply enter the IP of the container where the code-server runs to Chrome.
|
||||
|
||||
## Using Crouton
|
||||
|
||||
[Crouton](https://github.com/dnschneid/crouton) is one of the old ways to get a running full Linux via `chroot` on a Chromebook. To use crouton, enable developer mode and go to `crosh`. This time, run `shell`, which should drop you to `bash`.
|
||||
|
||||
Make sure you downloaded `crouton`, if so, go ahead and run it under `~/Downloads`. After installing your chroot container via crouton, go ahead and enter `enter-chroot` to enter your container.
|
||||
|
||||
Follow the instructions set in [the self-host install guide](index.md) to install code-server. After that is done, run `code-server` and verify it works by going to `localhost:8443`.
|
||||
|
||||
> At this point in writing, `localhost` seems to work in this method. However, the author is not sure if it applies still to newer Chromebooks.
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[code-server](https://coder.com) is used by developers at Azure, Google, Reddit, and more to give them access to VS Code in the browser.
|
||||
|
||||
## Quickstart guide
|
||||
## Quickstart Guide
|
||||
|
||||
> NOTE: If you get stuck or need help, [file an issue](https://github.com/codercom/code-server/issues/new?&title=Improve+self-hosted+quickstart+guide), [tweet (@coderhq)](https://twitter.com/coderhq) or [email](mailto:support@coder.com?subject=Self-hosted%20quickstart%20guide).
|
||||
|
||||
This document pertains to Coder specific implementations of VS Code. For documentation on how to use VS Code itself, please refer to the official [documentation for VS Code](https://code.visualstudio.com/docs)
|
||||
This document pertains to Coder specific implementations of VS Code. For documentation on how to use VS Code itself, please refer to the official [documentation for VS Code](https://code.visualstudio.com/docs)
|
||||
|
||||
It takes just a few minutes to get your own self-hosted server running. If you've got a machine running macOS, Windows, or Linux, you're ready to start the binary which listens on port `8443` by default.
|
||||
|
||||
@@ -24,7 +24,7 @@ It takes just a few minutes to get your own self-hosted server running. If you'v
|
||||
5. Paste the password from the cli into the login window<img src="../assets/server-password-modal.png">
|
||||
> NOTE: Be careful with your password as sharing it will grant those users access to your server's file system
|
||||
|
||||
### Things to know
|
||||
### Things To Know
|
||||
- When you visit the IP for your code-server, you will be greeted with this page. Code-server is using a self-signed SSL certificate for easy setup. To proceed to the IDE, click **"Advanced"**<img src ="../assets/chrome_warning.png">
|
||||
- Then click **"proceed anyway"**<img src="../assets/chrome_confirm.png">
|
||||
|
||||
@@ -54,28 +54,29 @@ OPTIONS
|
||||
--password=password
|
||||
```
|
||||
|
||||
### Data directory
|
||||
### Data Directory
|
||||
Use `code-server -d (path/to/directory)` or `code-server --data-dir=(path/to/directory)`, excluding the parentheses to specify the root folder that VS Code will start in
|
||||
|
||||
### Host
|
||||
By default, code-server will use `0.0.0.0` as its address. This can be changed by using `code-server -h` or `code-server --host=` followed by the address you want to use.
|
||||
By default, code-server will use `0.0.0.0` as its address. This can be changed by using `code-server -h` or `code-server --host=` followed by the address you want to use.
|
||||
> Example: `code-server -h 127.0.0.1`
|
||||
|
||||
### Open
|
||||
You can have the server automatically open the VS Code in your browser on startup by using the `code server -o` or `code-server --open` flags
|
||||
|
||||
### Port
|
||||
By default, code-server will use `8443` as its port. This can be changed by using `code-server -p` or `code-server --port=` followed by the port you want to use.
|
||||
### Port
|
||||
By default, code-server will use `8443` as its port. This can be changed by using `code-server -p` or `code-server --port=` followed by the port you want to use.
|
||||
> Example: `code-server -p 9000`
|
||||
|
||||
### Cert and Cert Key
|
||||
To encrypt the traffic between the browser and server use `code-server --cert=` followed by the path to your `.cer` file. Additionally, you can use certificate keys with `code-server --cert-key` followed by the path to your `.key` file.
|
||||
> Example (certificate and key): `code-server --cert /etc/letsencrypt/live/example.com/fullchain.cer --cert-key /etc/letsencrypt/live/example.com/fullchain.key`
|
||||
> Example (if you are using Letsencrypt or similar): `code-server --cert /etc/letsencrypt/live/example.com/fullchain.pem --cert-key /etc/letsencrypt/live/example.com/privkey.key`
|
||||
|
||||
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md)
|
||||
|
||||
### Nginx Reverse Proxy
|
||||
Nginx is for reverse proxy. Here is a example virtual host that works with code-server. Please also pass --allow-http. You can also use certbot by EFF to get a ssl certificates for free.
|
||||
Nginx is for reverse proxy. Below is a virtual host example that works with code-server. Please also pass --allow-http. You can also use certbot by EFF to get a ssl certificates for free.
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
@@ -85,9 +86,34 @@ OPTIONS
|
||||
proxy_pass http://localhost:8443/;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection upgrade;
|
||||
proxy_set_header Accept-Encoding gzip;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Apache Reverse Proxy
|
||||
Example of https virtualhost configuration for Apache as a reverse proxy. Please also pass --allow-http on code-server startup to allow the proxy to connect.
|
||||
```
|
||||
<VirtualHost *:80>
|
||||
ServerName code.example.com
|
||||
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTP:Upgrade} =websocket [NC]
|
||||
RewriteRule /(.*) ws://localhost:8443/$1 [P,L]
|
||||
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
|
||||
RewriteRule /(.*) http://localhost:8443/$1 [P,L]
|
||||
|
||||
ProxyRequests off
|
||||
|
||||
RequestHeader set X-Forwarded-Proto https
|
||||
RequestHeader set X-Forwarded-Port 443
|
||||
|
||||
ProxyPass / http://localhost:8443/ nocanon
|
||||
ProxyPassReverse / http://localhost:8443/
|
||||
|
||||
</VirtualHost>
|
||||
```
|
||||
*Important:* For more details about Apache reverse proxy configuration checkout the [documentation](https://httpd.apache.org/docs/current/mod/mod_proxy.html) - especially the [Securing your Server](https://httpd.apache.org/docs/current/mod/mod_proxy.html#access) section
|
||||
|
||||
### Help
|
||||
Use `code-server -h` or `code-server --help` to view the usage for the cli. This is also shown at the beginning of this section.
|
||||
Use `code-server -h` or `code-server --help` to view the usage for the cli. This is also shown at the beginning of this section.
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^5.0.4",
|
||||
"@types/node": "^10.12.18",
|
||||
"@types/tar": "^4.0.0",
|
||||
"@types/trash": "^4.3.1",
|
||||
"cache-loader": "^2.0.1",
|
||||
"cross-env": "^5.2.0",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"css-loader": "^2.1.0",
|
||||
@@ -34,13 +36,18 @@
|
||||
"sass-loader": "^7.1.0",
|
||||
"string-replace-loader": "^2.1.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"tar": "^4.4.8",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"ts-loader": "^5.3.3",
|
||||
"ts-node": "^7.0.1",
|
||||
"tsconfig-paths": "^3.8.0",
|
||||
"tslib": "^1.9.3",
|
||||
"tslint": "^5.12.1",
|
||||
"typescript": "^3.2.2",
|
||||
"typescript-tslint-plugin": "^0.2.1",
|
||||
"uglifyjs-webpack-plugin": "^2.1.1",
|
||||
"url-loader": "^1.1.2",
|
||||
"util": "^0.11.1",
|
||||
"webpack": "^4.28.4",
|
||||
"webpack-bundle-analyzer": "^3.0.3",
|
||||
"webpack-cli": "^3.2.1",
|
||||
|
||||
@@ -1,28 +1,51 @@
|
||||
import { IDisposable } from "@coder/disposable";
|
||||
|
||||
export interface Event<T> {
|
||||
(listener: (e: T) => void): IDisposable;
|
||||
(listener: (value: T) => void): IDisposable;
|
||||
(id: number | string, listener: (value: T) => void): IDisposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitter typecasts for a single event type.
|
||||
* Emitter typecasts for a single event type. You can optionally use IDs, but
|
||||
* using undefined with IDs will not work. If you emit without an ID, *all*
|
||||
* listeners regardless of their ID (or lack thereof) will receive the event.
|
||||
* Similarly, if you listen without an ID you will get *all* events for any or
|
||||
* no ID.
|
||||
*/
|
||||
export class Emitter<T> {
|
||||
private listeners = <Array<(e: T) => void>>[];
|
||||
private listeners = <Array<(value: T) => void>>[];
|
||||
private readonly idListeners = new Map<number | string, Array<(value: T) => void>>();
|
||||
|
||||
public get event(): Event<T> {
|
||||
return (cb: (e: T) => void): IDisposable => {
|
||||
if (this.listeners) {
|
||||
this.listeners.push(cb);
|
||||
return (id: number | string | ((value: T) => void), cb?: (value: T) => void): IDisposable => {
|
||||
if (typeof id !== "function") {
|
||||
if (this.idListeners.has(id)) {
|
||||
this.idListeners.get(id)!.push(cb!);
|
||||
} else {
|
||||
this.idListeners.set(id, [cb!]);
|
||||
}
|
||||
|
||||
return {
|
||||
dispose: (): void => {
|
||||
if (this.idListeners.has(id)) {
|
||||
const cbs = this.idListeners.get(id)!;
|
||||
const i = cbs.indexOf(cb!);
|
||||
if (i !== -1) {
|
||||
cbs.splice(i, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
cb = id;
|
||||
this.listeners.push(cb);
|
||||
|
||||
return {
|
||||
dispose: (): void => {
|
||||
if (this.listeners) {
|
||||
const i = this.listeners.indexOf(cb);
|
||||
if (i !== -1) {
|
||||
this.listeners.splice(i, 1);
|
||||
}
|
||||
const i = this.listeners.indexOf(cb!);
|
||||
if (i !== -1) {
|
||||
this.listeners.splice(i, 1);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -32,16 +55,45 @@ export class Emitter<T> {
|
||||
/**
|
||||
* Emit an event with a value.
|
||||
*/
|
||||
public emit(value: T): void {
|
||||
if (this.listeners) {
|
||||
this.listeners.forEach((t) => t(value));
|
||||
public emit(value: T): void;
|
||||
public emit(id: number | string, value: T): void;
|
||||
public emit(id: number | string | T, value?: T): void {
|
||||
if ((typeof id === "number" || typeof id === "string") && typeof value !== "undefined") {
|
||||
if (this.idListeners.has(id)) {
|
||||
this.idListeners.get(id)!.forEach((cb) => cb(value!));
|
||||
}
|
||||
this.listeners.forEach((cb) => cb(value!));
|
||||
} else {
|
||||
this.idListeners.forEach((cbs) => cbs.forEach((cb) => cb((id as T)!)));
|
||||
this.listeners.forEach((cb) => cb((id as T)!));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the current events.
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.listeners = [];
|
||||
public dispose(): void;
|
||||
public dispose(id: number | string): void;
|
||||
public dispose(id?: number | string): void {
|
||||
if (typeof id !== "undefined") {
|
||||
this.idListeners.delete(id);
|
||||
} else {
|
||||
this.listeners = [];
|
||||
this.idListeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public get counts(): { [key: string]: number } {
|
||||
const counts = <{ [key: string]: number }>{};
|
||||
if (this.listeners.length > 0) {
|
||||
counts["n/a"] = this.listeners.length;
|
||||
}
|
||||
this.idListeners.forEach((cbs, id) => {
|
||||
if (cbs.length > 0) {
|
||||
counts[`${id}`] = cbs.length;
|
||||
}
|
||||
});
|
||||
|
||||
return counts;
|
||||
}
|
||||
}
|
||||
|
||||
122
packages/events/test/events.test.ts
Normal file
122
packages/events/test/events.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { Emitter } from "../src/events";
|
||||
|
||||
describe("Event", () => {
|
||||
const emitter = new Emitter<number>();
|
||||
|
||||
it("should listen to global event", () => {
|
||||
const fn = jest.fn();
|
||||
const d = emitter.event(fn);
|
||||
emitter.emit(10);
|
||||
expect(fn).toHaveBeenCalledWith(10);
|
||||
d.dispose();
|
||||
});
|
||||
|
||||
it("should listen to id event", () => {
|
||||
const fn = jest.fn();
|
||||
const d = emitter.event(0, fn);
|
||||
emitter.emit(0, 5);
|
||||
expect(fn).toHaveBeenCalledWith(5);
|
||||
d.dispose();
|
||||
});
|
||||
|
||||
it("should listen to string id event", () => {
|
||||
const fn = jest.fn();
|
||||
const d = emitter.event("string", fn);
|
||||
emitter.emit("string", 55);
|
||||
expect(fn).toHaveBeenCalledWith(55);
|
||||
d.dispose();
|
||||
});
|
||||
|
||||
it("should not listen wrong id event", () => {
|
||||
const fn = jest.fn();
|
||||
const d = emitter.event(1, fn);
|
||||
emitter.emit(0, 5);
|
||||
emitter.emit(1, 6);
|
||||
expect(fn).toHaveBeenCalledWith(6);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
d.dispose();
|
||||
});
|
||||
|
||||
it("should listen to id event globally", () => {
|
||||
const fn = jest.fn();
|
||||
const d = emitter.event(fn);
|
||||
emitter.emit(1, 11);
|
||||
expect(fn).toHaveBeenCalledWith(11);
|
||||
d.dispose();
|
||||
});
|
||||
|
||||
it("should listen to global event", () => {
|
||||
const fn = jest.fn();
|
||||
const d = emitter.event(3, fn);
|
||||
emitter.emit(14);
|
||||
expect(fn).toHaveBeenCalledWith(14);
|
||||
d.dispose();
|
||||
});
|
||||
|
||||
it("should listen to id event multiple times", () => {
|
||||
const fn = jest.fn();
|
||||
const disposers = [
|
||||
emitter.event(934, fn),
|
||||
emitter.event(934, fn),
|
||||
emitter.event(934, fn),
|
||||
emitter.event(934, fn),
|
||||
];
|
||||
emitter.emit(934, 324);
|
||||
expect(fn).toHaveBeenCalledTimes(4);
|
||||
expect(fn).toHaveBeenCalledWith(324);
|
||||
disposers.forEach((d) => d.dispose());
|
||||
});
|
||||
|
||||
it("should dispose individually", () => {
|
||||
const fn = jest.fn();
|
||||
const d = emitter.event(fn);
|
||||
|
||||
const fn2 = jest.fn();
|
||||
const d2 = emitter.event(1, fn2);
|
||||
|
||||
d.dispose();
|
||||
|
||||
emitter.emit(12);
|
||||
emitter.emit(1, 12);
|
||||
|
||||
expect(fn).not.toBeCalled();
|
||||
expect(fn2).toBeCalledTimes(2);
|
||||
|
||||
d2.dispose();
|
||||
|
||||
emitter.emit(12);
|
||||
emitter.emit(1, 12);
|
||||
|
||||
expect(fn).not.toBeCalled();
|
||||
expect(fn2).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should dispose by id", () => {
|
||||
const fn = jest.fn();
|
||||
emitter.event(fn);
|
||||
|
||||
const fn2 = jest.fn();
|
||||
emitter.event(1, fn2);
|
||||
|
||||
emitter.dispose(1);
|
||||
|
||||
emitter.emit(12);
|
||||
emitter.emit(1, 12);
|
||||
|
||||
expect(fn).toBeCalledTimes(2);
|
||||
expect(fn2).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should dispose all", () => {
|
||||
const fn = jest.fn();
|
||||
emitter.event(fn);
|
||||
emitter.event(1, fn);
|
||||
|
||||
emitter.dispose();
|
||||
|
||||
emitter.emit(12);
|
||||
emitter.emit(1, 12);
|
||||
|
||||
expect(fn).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
28
packages/ide-api/api.d.ts
vendored
28
packages/ide-api/api.d.ts
vendored
@@ -1,3 +1,5 @@
|
||||
// tslint:disable no-any
|
||||
|
||||
export interface EvalHelper { }
|
||||
interface ActiveEvalEmitter {
|
||||
removeAllListeners(event?: string): void;
|
||||
@@ -106,7 +108,7 @@ interface IMenuItem {
|
||||
command: ICommandAction;
|
||||
alt?: ICommandAction;
|
||||
// when?: ContextKeyExpr;
|
||||
group?: 'navigation' | string;
|
||||
group?: "navigation" | string;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
@@ -135,23 +137,7 @@ interface ICommandRegistry {
|
||||
}
|
||||
|
||||
declare namespace ide {
|
||||
export const client: {
|
||||
run(func: (helper: ActiveEvalEmitter) => Disposer): ActiveEvalEmitter;
|
||||
run<T1>(func: (helper: ActiveEvalEmitter, a1: T1) => Disposer, a1: T1): ActiveEvalEmitter;
|
||||
run<T1, T2>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2) => Disposer, a1: T1, a2: T2): ActiveEvalEmitter;
|
||||
run<T1, T2, T3>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3) => Disposer, a1: T1, a2: T2, a3: T3): ActiveEvalEmitter;
|
||||
run<T1, T2, T3, T4>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3, a4: T4) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4): ActiveEvalEmitter;
|
||||
run<T1, T2, T3, T4, T5>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): ActiveEvalEmitter;
|
||||
run<T1, T2, T3, T4, T5, T6>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): ActiveEvalEmitter;
|
||||
|
||||
evaluate<R>(func: (helper: EvalHelper) => R | Promise<R>): Promise<R>;
|
||||
evaluate<R, T1>(func: (helper: EvalHelper, a1: T1) => R | Promise<R>, a1: T1): Promise<R>;
|
||||
evaluate<R, T1, T2>(func: (helper: EvalHelper, a1: T1, a2: T2) => R | Promise<R>, a1: T1, a2: T2): Promise<R>;
|
||||
evaluate<R, T1, T2, T3>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3) => R | Promise<R>, a1: T1, a2: T2, a3: T3): Promise<R>;
|
||||
evaluate<R, T1, T2, T3, T4>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4): Promise<R>;
|
||||
evaluate<R, T1, T2, T3, T4, T5>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): Promise<R>;
|
||||
evaluate<R, T1, T2, T3, T4, T5, T6>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): Promise<R>;
|
||||
};
|
||||
export const client: {};
|
||||
|
||||
export const workbench: {
|
||||
readonly statusbarService: IStatusbarService;
|
||||
@@ -177,8 +163,8 @@ declare namespace ide {
|
||||
Ignore = 0,
|
||||
Info = 1,
|
||||
Warning = 2,
|
||||
Error = 3
|
||||
}
|
||||
Error = 3,
|
||||
}
|
||||
|
||||
export enum StatusbarAlignment {
|
||||
LEFT = 0,
|
||||
@@ -229,7 +215,7 @@ declare namespace ide {
|
||||
declare global {
|
||||
interface Window {
|
||||
ide?: typeof ide;
|
||||
|
||||
|
||||
addEventListener(event: "ide-ready", callback: (ide: CustomEvent & { readonly ide: typeof ide }) => void): void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"name": "@coder/ide",
|
||||
"description": "Browser-based IDE client abstraction.",
|
||||
"main": "src/index.ts",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/rimraf": "^2.0.2",
|
||||
"rimraf": "^2.6.3"
|
||||
}
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
|
||||
@@ -1,195 +1,4 @@
|
||||
import * as cp from "child_process";
|
||||
import * as net from "net";
|
||||
import * as stream from "stream";
|
||||
import { CallbackEmitter, ActiveEvalReadable, ActiveEvalWritable } from "@coder/protocol";
|
||||
import { Module } from "@coder/protocol";
|
||||
import { client } from "./client";
|
||||
import { promisify } from "util";
|
||||
|
||||
declare var __non_webpack_require__: typeof require;
|
||||
|
||||
class ChildProcess extends CallbackEmitter implements cp.ChildProcess {
|
||||
private _connected: boolean = false;
|
||||
private _killed: boolean = false;
|
||||
private _pid = -1;
|
||||
public readonly stdin: stream.Writable;
|
||||
public readonly stdout: stream.Readable;
|
||||
public readonly stderr: stream.Readable;
|
||||
// We need the explicit type otherwise TypeScript thinks it is (Writable | Readable)[].
|
||||
public readonly stdio: [stream.Writable, stream.Readable, stream.Readable] = [this.stdin, this.stdout, this.stderr];
|
||||
|
||||
// tslint:disable no-any
|
||||
public constructor(method: "exec", command: string, options?: { encoding?: string | null } & cp.ExecOptions | null, callback?: (...args: any[]) => void);
|
||||
public constructor(method: "fork", modulePath: string, options?: cp.ForkOptions, args?: string[]);
|
||||
public constructor(method: "spawn", command: string, options?: cp.SpawnOptions, args?: string[]);
|
||||
public constructor(method: "exec" | "spawn" | "fork", command: string, options: object = {}, callback?: string[] | ((...args: any[]) => void)) {
|
||||
// tslint:enable no-any
|
||||
super();
|
||||
|
||||
let args: string[] = [];
|
||||
if (Array.isArray(callback)) {
|
||||
args = callback;
|
||||
callback = undefined;
|
||||
}
|
||||
|
||||
this.ae = client.run((ae, command, method, args, options, callbackId) => {
|
||||
const cp = __non_webpack_require__("child_process") as typeof import("child_process");
|
||||
|
||||
ae.preserveEnv(options);
|
||||
|
||||
let childProcess: cp.ChildProcess;
|
||||
switch (method) {
|
||||
case "exec":
|
||||
childProcess = cp.exec(command, options, ae.maybeCallback(callbackId));
|
||||
break;
|
||||
case "spawn":
|
||||
childProcess = cp.spawn(command, args, options);
|
||||
break;
|
||||
case "fork":
|
||||
childProcess = ae.fork(command, args, options);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`invalid method ${method}`);
|
||||
}
|
||||
|
||||
ae.on("disconnect", () => childProcess.disconnect());
|
||||
ae.on("kill", (signal: string) => childProcess.kill(signal));
|
||||
ae.on("ref", () => childProcess.ref());
|
||||
ae.on("send", (message: string, callbackId: number) => childProcess.send(message, ae.maybeCallback(callbackId)));
|
||||
ae.on("unref", () => childProcess.unref());
|
||||
|
||||
ae.emit("pid", childProcess.pid);
|
||||
childProcess.on("close", (code, signal) => ae.emit("close", code, signal));
|
||||
childProcess.on("disconnect", () => ae.emit("disconnect"));
|
||||
childProcess.on("error", (error) => ae.emit("error", error));
|
||||
childProcess.on("exit", (code, signal) => ae.emit("exit", code, signal));
|
||||
childProcess.on("message", (message) => ae.emit("message", message));
|
||||
|
||||
if (childProcess.stdin) {
|
||||
const stdinAe = ae.createUnique("stdin");
|
||||
stdinAe.bindWritable(childProcess.stdin);
|
||||
}
|
||||
if (childProcess.stdout) {
|
||||
const stdoutAe = ae.createUnique("stdout");
|
||||
stdoutAe.bindReadable(childProcess.stdout);
|
||||
}
|
||||
if (childProcess.stderr) {
|
||||
const stderrAe = ae.createUnique("stderr");
|
||||
stderrAe.bindReadable(childProcess.stderr);
|
||||
}
|
||||
|
||||
return {
|
||||
onDidDispose: (cb): cp.ChildProcess => childProcess.on("close", cb),
|
||||
dispose: (): void => {
|
||||
childProcess.kill();
|
||||
setTimeout(() => childProcess.kill("SIGKILL"), 5000); // Double tap.
|
||||
},
|
||||
};
|
||||
}, command, method, args, options, this.storeCallback(callback));
|
||||
|
||||
this.ae.on("pid", (pid) => {
|
||||
this._pid = pid;
|
||||
this._connected = true;
|
||||
});
|
||||
|
||||
this.stdin = new ActiveEvalWritable(this.ae.createUnique("stdin"));
|
||||
this.stdout = new ActiveEvalReadable(this.ae.createUnique("stdout"));
|
||||
this.stderr = new ActiveEvalReadable(this.ae.createUnique("stderr"));
|
||||
|
||||
this.ae.on("close", (code, signal) => this.emit("close", code, signal));
|
||||
this.ae.on("disconnect", () => this.emit("disconnect"));
|
||||
this.ae.on("error", (error) => this.emit("error", error));
|
||||
this.ae.on("exit", (code, signal) => {
|
||||
this._connected = false;
|
||||
this._killed = true;
|
||||
this.emit("exit", code, signal);
|
||||
});
|
||||
this.ae.on("message", (message) => this.emit("message", message));
|
||||
}
|
||||
|
||||
public get pid(): number { return this._pid; }
|
||||
public get connected(): boolean { return this._connected; }
|
||||
public get killed(): boolean { return this._killed; }
|
||||
|
||||
public kill(): void { this.ae.emit("kill"); }
|
||||
public disconnect(): void { this.ae.emit("disconnect"); }
|
||||
public ref(): void { this.ae.emit("ref"); }
|
||||
public unref(): void { this.ae.emit("unref"); }
|
||||
|
||||
public send(
|
||||
message: any, // tslint:disable-line no-any to match spec
|
||||
sendHandle?: net.Socket | net.Server | ((error: Error) => void),
|
||||
options?: cp.MessageOptions | ((error: Error) => void),
|
||||
callback?: (error: Error) => void): boolean {
|
||||
if (typeof sendHandle === "function") {
|
||||
callback = sendHandle;
|
||||
sendHandle = undefined;
|
||||
} else if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
if (sendHandle || options) {
|
||||
throw new Error("sendHandle and options are not supported");
|
||||
}
|
||||
this.ae.emit("send", message, this.storeCallback(callback));
|
||||
|
||||
// Unfortunately this will always have to be true since we can't retrieve
|
||||
// the actual response synchronously.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class CP {
|
||||
public readonly ChildProcess = ChildProcess;
|
||||
|
||||
public exec = (
|
||||
command: string,
|
||||
options?: { encoding?: string | null } & cp.ExecOptions | null | ((error: cp.ExecException | null, stdout: string, stderr: string) => void) | ((error: cp.ExecException | null, stdout: Buffer, stderr: Buffer) => void),
|
||||
callback?: ((error: cp.ExecException | null, stdout: string, stderr: string) => void) | ((error: cp.ExecException | null, stdout: Buffer, stderr: Buffer) => void),
|
||||
): cp.ChildProcess => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
|
||||
return new ChildProcess("exec", command, options, callback);
|
||||
}
|
||||
|
||||
public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
|
||||
if (args && !Array.isArray(args)) {
|
||||
options = args;
|
||||
args = undefined;
|
||||
}
|
||||
|
||||
return new ChildProcess("fork", modulePath, options, args);
|
||||
}
|
||||
|
||||
public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => {
|
||||
if (args && !Array.isArray(args)) {
|
||||
options = args;
|
||||
args = undefined;
|
||||
}
|
||||
|
||||
return new ChildProcess("spawn", command, options, args);
|
||||
}
|
||||
}
|
||||
|
||||
const fillCp = new CP();
|
||||
// Methods that don't follow the standard callback pattern (an error followed
|
||||
// by a single result) need to provide a custom promisify function.
|
||||
Object.defineProperty(fillCp.exec, promisify.custom, {
|
||||
value: (
|
||||
command: string,
|
||||
options?: { encoding?: string | null } & cp.ExecOptions | null,
|
||||
): Promise<{ stdout: string | Buffer, stderr: string | Buffer }> => {
|
||||
return new Promise((resolve, reject): void => {
|
||||
fillCp.exec(command, options, (error: cp.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
export = fillCp;
|
||||
export = client.modules[Module.ChildProcess];
|
||||
|
||||
@@ -11,7 +11,7 @@ class WebsocketConnection implements ReadWriteConnection {
|
||||
private activeSocket: WebSocket | undefined;
|
||||
private readonly messageBuffer = <Uint8Array[]>[];
|
||||
private readonly socketTimeoutDelay = 60 * 1000;
|
||||
private readonly retryName = "Socket";
|
||||
private readonly retry = retry.register("Socket", () => this.connect());
|
||||
private isUp: boolean = false;
|
||||
private closed: boolean = false;
|
||||
|
||||
@@ -26,11 +26,14 @@ class WebsocketConnection implements ReadWriteConnection {
|
||||
public readonly onMessage = this.messageEmitter.event;
|
||||
|
||||
public constructor() {
|
||||
retry.register(this.retryName, () => this.connect());
|
||||
retry.block(this.retryName);
|
||||
retry.run(this.retryName);
|
||||
this.retry.block();
|
||||
this.retry.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data across the socket. If closed, will error. If connecting, will
|
||||
* queue.
|
||||
*/
|
||||
public send(data: Buffer | Uint8Array): void {
|
||||
if (this.closed) {
|
||||
throw new Error("web socket is closed");
|
||||
@@ -42,6 +45,9 @@ class WebsocketConnection implements ReadWriteConnection {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close socket connection.
|
||||
*/
|
||||
public close(): void {
|
||||
this.closed = true;
|
||||
this.dispose();
|
||||
@@ -61,7 +67,12 @@ class WebsocketConnection implements ReadWriteConnection {
|
||||
socket.addEventListener("close", (event) => {
|
||||
if (this.isUp) {
|
||||
this.isUp = false;
|
||||
this.downEmitter.emit(undefined);
|
||||
try {
|
||||
this.downEmitter.emit(undefined);
|
||||
} catch (error) {
|
||||
// Don't let errors here prevent restarting.
|
||||
logger.error(error.message);
|
||||
}
|
||||
}
|
||||
logger.warn(
|
||||
"Web socket closed",
|
||||
@@ -70,8 +81,8 @@ class WebsocketConnection implements ReadWriteConnection {
|
||||
field("wasClean", event.wasClean),
|
||||
);
|
||||
if (!this.closed) {
|
||||
retry.block(this.retryName);
|
||||
retry.run(this.retryName);
|
||||
this.retry.block();
|
||||
this.retry.run();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -103,15 +114,19 @@ class WebsocketConnection implements ReadWriteConnection {
|
||||
}, this.socketTimeoutDelay);
|
||||
|
||||
await new Promise((resolve, reject): void => {
|
||||
const onClose = (): void => {
|
||||
const doReject = (): void => {
|
||||
clearTimeout(socketWaitTimeout);
|
||||
socket.removeEventListener("close", onClose);
|
||||
socket.removeEventListener("error", doReject);
|
||||
socket.removeEventListener("close", doReject);
|
||||
reject();
|
||||
};
|
||||
socket.addEventListener("close", onClose);
|
||||
socket.addEventListener("error", doReject);
|
||||
socket.addEventListener("close", doReject);
|
||||
|
||||
socket.addEventListener("open", async () => {
|
||||
socket.addEventListener("open", () => {
|
||||
clearTimeout(socketWaitTimeout);
|
||||
socket.removeEventListener("error", doReject);
|
||||
socket.removeEventListener("close", doReject);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
/// <reference path="../../../../lib/vscode/src/typings/electron.d.ts" />
|
||||
import { EventEmitter } from "events";
|
||||
import * as fs from "fs";
|
||||
import * as trash from "trash";
|
||||
import { logger, field } from "@coder/logger";
|
||||
import { IKey, Dialog as DialogBox } from "./dialog";
|
||||
import { clipboard } from "./clipboard";
|
||||
import { client } from "./client";
|
||||
|
||||
declare var __non_webpack_require__: typeof require;
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
(global as any).getOpenUrls = (): string[] => {
|
||||
@@ -184,9 +182,7 @@ class Clipboard {
|
||||
|
||||
class Shell {
|
||||
public async moveItemToTrash(path: string): Promise<void> {
|
||||
await client.evaluate((helper, path) => {
|
||||
return helper.modules.trash(path);
|
||||
}, path);
|
||||
await trash(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,763 +1,4 @@
|
||||
import { EventEmitter } from "events";
|
||||
import * as fs from "fs";
|
||||
import * as stream from "stream";
|
||||
import { Client, IEncodingOptions, IEncodingOptionsCallback } from "@coder/protocol";
|
||||
import { Module } from "@coder/protocol";
|
||||
import { client } from "./client";
|
||||
import { promisify } from "util";
|
||||
|
||||
declare var __non_webpack_require__: typeof require;
|
||||
declare var _Buffer: typeof Buffer;
|
||||
|
||||
/**
|
||||
* Implements the native fs module
|
||||
* Doesn't use `implements typeof import("fs")` to remove need for __promisify__ impls
|
||||
*
|
||||
* TODO: For now we can't use async in the evaluate calls because they get
|
||||
* transpiled to TypeScript's helpers. tslib is included but we also need to set
|
||||
* _this somehow which the __awaiter helper uses.
|
||||
*/
|
||||
class FS {
|
||||
public constructor(
|
||||
private readonly client: Client,
|
||||
) { }
|
||||
|
||||
public access = (path: fs.PathLike, mode: number | undefined | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof mode === "function") {
|
||||
callback = mode;
|
||||
mode = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, path, mode) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.access)(path, mode);
|
||||
}, path, mode).then(() => {
|
||||
callback!(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback!(ex);
|
||||
});
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public appendFile = (file: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, path, data, options) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.appendFile)(path, data, options);
|
||||
}, file, data, options).then(() => {
|
||||
callback!(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback!(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public chmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, path, mode) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.chmod)(path, mode);
|
||||
}, path, mode).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public chown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, path, uid, gid) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.chown)(path, uid, gid);
|
||||
}, path, uid, gid).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public close = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, fd) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.close)(fd);
|
||||
}, fd).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public copyFile = (src: fs.PathLike, dest: fs.PathLike, flags: number | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof flags === "function") {
|
||||
callback = flags;
|
||||
}
|
||||
this.client.evaluate((_helper, src, dest, flags) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.copyFile)(src, dest, flags);
|
||||
}, src, dest, typeof flags !== "function" ? flags : undefined).then(() => {
|
||||
callback!(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback!(ex);
|
||||
});
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public createWriteStream = (path: fs.PathLike, options?: any): fs.WriteStream => {
|
||||
const ae = this.client.run((ae, path, options) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const str = fs.createWriteStream(path, options);
|
||||
ae.on("write", (d: string) => str.write(_Buffer.from(d, "utf8")));
|
||||
ae.on("close", () => str.close());
|
||||
ae.on("destroy", () => str.destroy());
|
||||
str.on("close", () => ae.emit("close"));
|
||||
str.on("open", (fd) => ae.emit("open", fd));
|
||||
str.on("error", (err) => ae.emit(err));
|
||||
|
||||
return {
|
||||
onDidDispose: (cb): fs.WriteStream => str.on("close", cb),
|
||||
dispose: (): void => str.close(),
|
||||
};
|
||||
}, path, options);
|
||||
|
||||
return new (class WriteStream extends stream.Writable implements fs.WriteStream {
|
||||
|
||||
private _bytesWritten: number = 0;
|
||||
|
||||
public constructor() {
|
||||
super({
|
||||
write: (data, encoding, cb): void => {
|
||||
this._bytesWritten += data.length;
|
||||
ae.emit("write", Buffer.from(data, encoding), encoding);
|
||||
cb();
|
||||
},
|
||||
});
|
||||
|
||||
ae.on("open", (fd: number) => this.emit("open", fd));
|
||||
ae.on("close", () => this.emit("close"));
|
||||
}
|
||||
|
||||
public get bytesWritten(): number {
|
||||
return this._bytesWritten;
|
||||
}
|
||||
|
||||
public get path(): string | Buffer {
|
||||
return "";
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
ae.emit("close");
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
ae.emit("destroy");
|
||||
}
|
||||
|
||||
}) as fs.WriteStream;
|
||||
}
|
||||
|
||||
public exists = (path: fs.PathLike, callback: (exists: boolean) => void): void => {
|
||||
this.client.evaluate((_helper, path) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.exists)(path);
|
||||
}, path).then((r) => {
|
||||
callback(r);
|
||||
}).catch(() => {
|
||||
callback(false);
|
||||
});
|
||||
}
|
||||
|
||||
public fchmod = (fd: number, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, fd, mode) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.fchmod)(fd, mode);
|
||||
}, fd, mode).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public fchown = (fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, fd, uid, gid) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.fchown)(fd, uid, gid);
|
||||
}, fd, uid, gid).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public fdatasync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, fd) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.fdatasync)(fd);
|
||||
}, fd).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public fstat = (fd: number, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
|
||||
this.client.evaluate((_helper, fd) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
const tslib = __non_webpack_require__("tslib") as typeof import("tslib");
|
||||
|
||||
return util.promisify(fs.fstat)(fd).then((stats) => {
|
||||
return tslib.__assign(stats, {
|
||||
_isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false,
|
||||
_isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false,
|
||||
_isDirectory: stats.isDirectory(),
|
||||
_isFIFO: stats.isFIFO ? stats.isFIFO() : false,
|
||||
_isFile: stats.isFile(),
|
||||
_isSocket: stats.isSocket ? stats.isSocket() : false,
|
||||
_isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false,
|
||||
});
|
||||
});
|
||||
}, fd).then((stats) => {
|
||||
callback(undefined!, new Stats(stats));
|
||||
}).catch((ex) => {
|
||||
callback(ex, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
public fsync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, fd) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.fsync)(fd);
|
||||
}, fd).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public ftruncate = (fd: number, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof len === "function") {
|
||||
callback = len;
|
||||
len = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, fd, len) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.ftruncate)(fd, len);
|
||||
}, fd, len).then(() => {
|
||||
callback!(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback!(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public futimes = (fd: number, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, fd, atime, mtime) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.futimes)(fd, atime, mtime);
|
||||
}, fd, atime, mtime).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public lchmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, path, mode) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.lchmod)(path, mode);
|
||||
}, path, mode).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public lchown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, path, uid, gid) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.lchown)(path, uid, gid);
|
||||
}, path, uid, gid).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public link = (existingPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, existingPath, newPath) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.link)(existingPath, newPath);
|
||||
}, existingPath, newPath).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public lstat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
|
||||
this.client.evaluate((_helper, path) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
const tslib = __non_webpack_require__("tslib") as typeof import("tslib");
|
||||
|
||||
return util.promisify(fs.lstat)(path).then((stats) => {
|
||||
return tslib.__assign(stats, {
|
||||
_isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false,
|
||||
_isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false,
|
||||
_isDirectory: stats.isDirectory(),
|
||||
_isFIFO: stats.isFIFO ? stats.isFIFO() : false,
|
||||
_isFile: stats.isFile(),
|
||||
_isSocket: stats.isSocket ? stats.isSocket() : false,
|
||||
_isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false,
|
||||
});
|
||||
});
|
||||
}, path).then((stats) => {
|
||||
callback(undefined!, new Stats(stats));
|
||||
}).catch((ex) => {
|
||||
callback(ex, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
public mkdir = (path: fs.PathLike, mode: number | string | fs.MakeDirectoryOptions | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof mode === "function") {
|
||||
callback = mode;
|
||||
mode = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, path, mode) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.mkdir)(path, mode);
|
||||
}, path, mode).then(() => {
|
||||
callback!(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback!(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public mkdtemp = (prefix: string, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, folder: string | Buffer) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, prefix, options) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.mkdtemp)(prefix, options);
|
||||
}, prefix, options).then((folder) => {
|
||||
callback!(undefined!, folder);
|
||||
}).catch((ex) => {
|
||||
callback!(ex, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
public open = (path: fs.PathLike, flags: string | number, mode: string | number | undefined | null | ((err: NodeJS.ErrnoException, fd: number) => void), callback?: (err: NodeJS.ErrnoException, fd: number) => void): void => {
|
||||
if (typeof mode === "function") {
|
||||
callback = mode;
|
||||
mode = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, path, flags, mode) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.open)(path, flags, mode);
|
||||
}, path, flags, mode).then((fd) => {
|
||||
callback!(undefined!, fd);
|
||||
}).catch((ex) => {
|
||||
callback!(ex, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
public read = <TBuffer extends Buffer | Uint8Array>(fd: number, buffer: TBuffer, offset: number, length: number, position: number | null, callback: (err: NodeJS.ErrnoException, bytesRead: number, buffer: TBuffer) => void): void => {
|
||||
this.client.evaluate((_helper, fd, length, position) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
const buffer = new _Buffer(length);
|
||||
|
||||
return util.promisify(fs.read)(fd, buffer, 0, length, position).then((resp) => {
|
||||
return {
|
||||
bytesRead: resp.bytesRead,
|
||||
content: resp.bytesRead < buffer.length ? buffer.slice(0, resp.bytesRead) : buffer,
|
||||
};
|
||||
});
|
||||
}, fd, length, position).then((resp) => {
|
||||
buffer.set(resp.content, offset);
|
||||
callback(undefined!, resp.bytesRead, resp.content as TBuffer);
|
||||
}).catch((ex) => {
|
||||
callback(ex, undefined!, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
public readFile = (path: fs.PathLike | number, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, data: string | Buffer) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, path, options) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.readFile)(path, options).then((value) => value.toString());
|
||||
}, path, options).then((buffer) => {
|
||||
callback!(undefined!, buffer);
|
||||
}).catch((ex) => {
|
||||
callback!(ex, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
public readdir = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, files: Buffer[] | fs.Dirent[] | string[]) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
// TODO: options can also take `withFileTypes` but the types aren't working.
|
||||
this.client.evaluate((_helper, path, options) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.readdir)(path, options);
|
||||
}, path, options).then((files) => {
|
||||
callback!(undefined!, files);
|
||||
}).catch((ex) => {
|
||||
callback!(ex, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
public readlink = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, linkString: string | Buffer) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, path, options) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.readlink)(path, options);
|
||||
}, path, options).then((linkString) => {
|
||||
callback!(undefined!, linkString);
|
||||
}).catch((ex) => {
|
||||
callback!(ex, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
public realpath = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, resolvedPath: string | Buffer) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, path, options) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.realpath)(path, options);
|
||||
}, path, options).then((resolvedPath) => {
|
||||
callback!(undefined!, resolvedPath);
|
||||
}).catch((ex) => {
|
||||
callback!(ex, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
public rename = (oldPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, oldPath, newPath) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.rename)(oldPath, newPath);
|
||||
}, oldPath, newPath).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public rmdir = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, path) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.rmdir)(path);
|
||||
}, path).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public stat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
|
||||
this.client.evaluate((_helper, path) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
const tslib = __non_webpack_require__("tslib") as typeof import("tslib");
|
||||
|
||||
return util.promisify(fs.stat)(path).then((stats) => {
|
||||
return tslib.__assign(stats, {
|
||||
/**
|
||||
* We need to check if functions exist because nexe's implemented FS
|
||||
* lib doesnt implement fs.stats properly
|
||||
*/
|
||||
_isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false,
|
||||
_isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false,
|
||||
_isDirectory: stats.isDirectory(),
|
||||
_isFIFO: stats.isFIFO ? stats.isFIFO() : false,
|
||||
_isFile: stats.isFile(),
|
||||
_isSocket: stats.isSocket ? stats.isSocket() : false,
|
||||
_isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false,
|
||||
});
|
||||
});
|
||||
}, path).then((stats) => {
|
||||
callback(undefined!, new Stats(stats));
|
||||
}).catch((ex) => {
|
||||
callback(ex, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
public symlink = (target: fs.PathLike, path: fs.PathLike, type: fs.symlink.Type | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof type === "function") {
|
||||
callback = type;
|
||||
type = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, target, path, type) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.symlink)(target, path, type);
|
||||
}, target, path, type).then(() => {
|
||||
callback!(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback!(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public truncate = (path: fs.PathLike, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof len === "function") {
|
||||
callback = len;
|
||||
len = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, path, len) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.truncate)(path, len);
|
||||
}, path, len).then(() => {
|
||||
callback!(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback!(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public unlink = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, path) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.unlink)(path);
|
||||
}, path).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public utimes = (path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
this.client.evaluate((_helper, path, atime, mtime) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.utimes)(path, atime, mtime);
|
||||
}, path, atime, mtime).then(() => {
|
||||
callback(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public write = <TBuffer extends Buffer | Uint8Array>(fd: number, buffer: TBuffer, offset: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), length: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), position: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), callback?: (err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void): void => {
|
||||
if (typeof offset === "function") {
|
||||
callback = offset;
|
||||
offset = undefined;
|
||||
}
|
||||
if (typeof length === "function") {
|
||||
callback = length;
|
||||
length = undefined;
|
||||
}
|
||||
if (typeof position === "function") {
|
||||
callback = position;
|
||||
position = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, fd, buffer, offset, length, position) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.write)(fd, _Buffer.from(buffer, "utf8"), offset, length, position).then((resp) => {
|
||||
return {
|
||||
bytesWritten: resp.bytesWritten,
|
||||
content: resp.buffer.toString("utf8"),
|
||||
};
|
||||
});
|
||||
}, fd, buffer.toString(), offset, length, position).then((r) => {
|
||||
callback!(undefined!, r.bytesWritten, Buffer.from(r.content, "utf8") as TBuffer);
|
||||
}).catch((ex) => {
|
||||
callback!(ex, undefined!, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public writeFile = (path: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
this.client.evaluate((_helper, path, data, options) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
|
||||
return util.promisify(fs.writeFile)(path, data, options);
|
||||
}, path, data, options).then(() => {
|
||||
callback!(undefined!);
|
||||
}).catch((ex) => {
|
||||
callback!(ex);
|
||||
});
|
||||
}
|
||||
|
||||
public watch = (filename: fs.PathLike, options?: IEncodingOptions | ((event: string, filename: string | Buffer) => void), listener?: ((event: string, filename: string | Buffer) => void)): fs.FSWatcher => {
|
||||
if (typeof options === "function") {
|
||||
listener = options;
|
||||
options = undefined;
|
||||
}
|
||||
|
||||
const ae = this.client.run((ae, filename, hasListener, options) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import ("fs");
|
||||
// tslint:disable-next-line no-any
|
||||
const watcher = fs.watch(filename, options as any, hasListener ? (event, filename): void => {
|
||||
ae.emit("listener", event, filename);
|
||||
} : undefined);
|
||||
watcher.on("change", (event, filename) => ae.emit("change", event, filename));
|
||||
watcher.on("error", (error) => ae.emit("error", error));
|
||||
ae.on("close", () => watcher.close());
|
||||
|
||||
return {
|
||||
onDidDispose: (cb): void => ae.on("close", cb),
|
||||
dispose: (): void => watcher.close(),
|
||||
};
|
||||
}, filename.toString(), !!listener, options);
|
||||
|
||||
return new class Watcher extends EventEmitter implements fs.FSWatcher {
|
||||
public constructor() {
|
||||
super();
|
||||
ae.on("change", (event: string, filename: string) => this.emit("change", event, filename));
|
||||
ae.on("error", (error: Error) => this.emit("error", error));
|
||||
ae.on("listener", (event: string, filename: string) => listener && listener(event, filename));
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
ae.emit("close");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface IStats {
|
||||
dev: number;
|
||||
ino: number;
|
||||
mode: number;
|
||||
nlink: number;
|
||||
uid: number;
|
||||
gid: number;
|
||||
rdev: number;
|
||||
size: number;
|
||||
blksize: number;
|
||||
blocks: number;
|
||||
atimeMs: number;
|
||||
mtimeMs: number;
|
||||
ctimeMs: number;
|
||||
birthtimeMs: number;
|
||||
atime: Date | string;
|
||||
mtime: Date | string;
|
||||
ctime: Date | string;
|
||||
birthtime: Date | string;
|
||||
_isFile: boolean;
|
||||
_isDirectory: boolean;
|
||||
_isBlockDevice: boolean;
|
||||
_isCharacterDevice: boolean;
|
||||
_isSymbolicLink: boolean;
|
||||
_isFIFO: boolean;
|
||||
_isSocket: boolean;
|
||||
}
|
||||
|
||||
class Stats implements fs.Stats {
|
||||
public readonly atime: Date;
|
||||
public readonly mtime: Date;
|
||||
public readonly ctime: Date;
|
||||
public readonly birthtime: Date;
|
||||
|
||||
public constructor(private readonly stats: IStats) {
|
||||
this.atime = new Date(stats.atime);
|
||||
this.mtime = new Date(stats.mtime);
|
||||
this.ctime = new Date(stats.ctime);
|
||||
this.birthtime = new Date(stats.birthtime);
|
||||
}
|
||||
|
||||
public get dev(): number { return this.stats.dev; }
|
||||
public get ino(): number { return this.stats.ino; }
|
||||
public get mode(): number { return this.stats.mode; }
|
||||
public get nlink(): number { return this.stats.nlink; }
|
||||
public get uid(): number { return this.stats.uid; }
|
||||
public get gid(): number { return this.stats.gid; }
|
||||
public get rdev(): number { return this.stats.rdev; }
|
||||
public get size(): number { return this.stats.size; }
|
||||
public get blksize(): number { return this.stats.blksize; }
|
||||
public get blocks(): number { return this.stats.blocks; }
|
||||
public get atimeMs(): number { return this.stats.atimeMs; }
|
||||
public get mtimeMs(): number { return this.stats.mtimeMs; }
|
||||
public get ctimeMs(): number { return this.stats.ctimeMs; }
|
||||
public get birthtimeMs(): number { return this.stats.birthtimeMs; }
|
||||
public isFile(): boolean { return this.stats._isFile; }
|
||||
public isDirectory(): boolean { return this.stats._isDirectory; }
|
||||
public isBlockDevice(): boolean { return this.stats._isBlockDevice; }
|
||||
public isCharacterDevice(): boolean { return this.stats._isCharacterDevice; }
|
||||
public isSymbolicLink(): boolean { return this.stats._isSymbolicLink; }
|
||||
public isFIFO(): boolean { return this.stats._isFIFO; }
|
||||
public isSocket(): boolean { return this.stats._isSocket; }
|
||||
|
||||
public toObject(): object {
|
||||
return JSON.parse(JSON.stringify(this));
|
||||
}
|
||||
}
|
||||
|
||||
const fillFs = new FS(client);
|
||||
// Methods that don't follow the standard callback pattern (an error followed
|
||||
// by a single result) need to provide a custom promisify function.
|
||||
Object.defineProperty(fillFs.exists, promisify.custom, {
|
||||
value: (path: fs.PathLike): Promise<boolean> => new Promise((resolve): void => fillFs.exists(path, resolve)),
|
||||
});
|
||||
export = fillFs;
|
||||
export = client.modules[Module.Fs];
|
||||
|
||||
@@ -1,258 +1,4 @@
|
||||
import * as net from "net";
|
||||
import { CallbackEmitter, ActiveEvalDuplex, ActiveEvalHelper } from "@coder/protocol";
|
||||
import { Module } from "@coder/protocol";
|
||||
import { client } from "./client";
|
||||
|
||||
declare var __non_webpack_require__: typeof require;
|
||||
|
||||
class Socket extends ActiveEvalDuplex implements net.Socket {
|
||||
private _connecting: boolean = false;
|
||||
private _destroyed: boolean = false;
|
||||
|
||||
public constructor(options?: net.SocketConstructorOpts, ae?: ActiveEvalHelper) {
|
||||
super(ae || client.run((ae, options) => {
|
||||
const net = __non_webpack_require__("net") as typeof import("net");
|
||||
|
||||
return ae.bindSocket(new net.Socket(options));
|
||||
}, options));
|
||||
|
||||
this.ae.on("connect", () => {
|
||||
this._connecting = false;
|
||||
this.emit("connect");
|
||||
});
|
||||
this.ae.on("error", () => {
|
||||
this._connecting = false;
|
||||
this._destroyed = true;
|
||||
});
|
||||
this.ae.on("lookup", (error, address, family, host) => this.emit("lookup", error, address, family, host));
|
||||
this.ae.on("timeout", () => this.emit("timeout"));
|
||||
}
|
||||
|
||||
public connect(options: net.SocketConnectOpts | number | string, host?: string | Function, connectionListener?: Function): this {
|
||||
// This is to get around type issues with socket.connect as well as extract
|
||||
// the function wherever it might be.
|
||||
switch (typeof options) {
|
||||
case "string": options = { path: options }; break;
|
||||
case "number": options = { port: options }; break;
|
||||
}
|
||||
switch (typeof host) {
|
||||
case "function": connectionListener = host; break;
|
||||
case "string": (options as net.TcpSocketConnectOpts).host = host; break;
|
||||
}
|
||||
|
||||
this._connecting = true;
|
||||
this.ae.emit("connect", options, this.storeCallback(connectionListener));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public write(data: any, encoding?: string | Function, fd?: string | Function): boolean {
|
||||
let callback: Function | undefined;
|
||||
if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
if (typeof fd === "function") {
|
||||
callback = fd;
|
||||
fd = undefined;
|
||||
}
|
||||
this.ae.emit("write", data, encoding, fd, this.storeCallback(callback));
|
||||
|
||||
return true; // Always true since we can't get this synchronously.
|
||||
}
|
||||
|
||||
public get connecting(): boolean { return this._connecting; }
|
||||
public get destroyed(): boolean { return this._destroyed; }
|
||||
|
||||
public get bufferSize(): number { throw new Error("not implemented"); }
|
||||
public get bytesRead(): number { throw new Error("not implemented"); }
|
||||
public get bytesWritten(): number { throw new Error("not implemented"); }
|
||||
public get localAddress(): string { throw new Error("not implemented"); }
|
||||
public get localPort(): number { throw new Error("not implemented"); }
|
||||
public address(): net.AddressInfo | string { throw new Error("not implemented"); }
|
||||
|
||||
public setTimeout(timeout: number, callback?: Function): this { return this.emitReturnThis("setTimeout", timeout, this.storeCallback(callback)); }
|
||||
public setNoDelay(noDelay?: boolean): this { return this.emitReturnThis("setNoDelay", noDelay); }
|
||||
public setKeepAlive(enable?: boolean, initialDelay?: number): this { return this.emitReturnThis("setKeepAlive", enable, initialDelay); }
|
||||
public unref(): void { this.ae.emit("unref"); }
|
||||
public ref(): void { this.ae.emit("ref"); }
|
||||
}
|
||||
|
||||
class Server extends CallbackEmitter implements net.Server {
|
||||
private readonly sockets = new Map<number, Socket>();
|
||||
private _listening: boolean = false;
|
||||
|
||||
public constructor(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: Socket) => void), connectionListener?: (socket: Socket) => void) {
|
||||
super();
|
||||
|
||||
if (typeof options === "function") {
|
||||
connectionListener = options;
|
||||
options = undefined;
|
||||
}
|
||||
|
||||
this.ae = client.run((ae, options, callbackId) => {
|
||||
const net = __non_webpack_require__("net") as typeof import("net");
|
||||
|
||||
let connectionId = 0;
|
||||
const sockets = new Map<number, net.Socket>();
|
||||
const storeSocket = (socket: net.Socket): number => {
|
||||
const socketId = connectionId++;
|
||||
sockets.set(socketId, socket);
|
||||
const socketAe = ae.createUnique(socketId);
|
||||
const disposer = socketAe.bindSocket(socket);
|
||||
socket.on("close", () => {
|
||||
disposer.dispose();
|
||||
sockets.delete(socketId);
|
||||
});
|
||||
|
||||
return socketId;
|
||||
};
|
||||
|
||||
const callback = ae.maybeCallback(callbackId);
|
||||
let server = new net.Server(options, typeof callback !== "undefined" ? (socket): void => {
|
||||
callback(storeSocket(socket));
|
||||
} : undefined);
|
||||
|
||||
server.on("close", () => ae.emit("close"));
|
||||
server.on("connection", (socket) => ae.emit("connection", storeSocket(socket)));
|
||||
server.on("error", (error) => ae.emit("error", error));
|
||||
server.on("listening", () => ae.emit("listening"));
|
||||
|
||||
ae.on("close", (callbackId: number) => server.close(ae.maybeCallback(callbackId)));
|
||||
ae.on("listen", (handle?: net.ListenOptions | number | string) => server.listen(handle));
|
||||
ae.on("ref", () => server.ref());
|
||||
ae.on("unref", () => server.unref());
|
||||
|
||||
return {
|
||||
onDidDispose: (cb): net.Server => server.on("close", cb),
|
||||
dispose: (): void => {
|
||||
server.removeAllListeners();
|
||||
server.close();
|
||||
sockets.forEach((socket) => {
|
||||
socket.removeAllListeners();
|
||||
socket.end();
|
||||
socket.destroy();
|
||||
socket.unref();
|
||||
});
|
||||
sockets.clear();
|
||||
},
|
||||
};
|
||||
}, options || {}, this.storeCallback(connectionListener));
|
||||
|
||||
this.ae.on("close", () => {
|
||||
this._listening = false;
|
||||
this.emit("close");
|
||||
});
|
||||
|
||||
this.ae.on("connection", (socketId) => {
|
||||
const socketAe = this.ae.createUnique(socketId);
|
||||
const socket = new Socket(undefined, socketAe);
|
||||
this.sockets.set(socketId, socket);
|
||||
socket.on("close", () => this.sockets.delete(socketId));
|
||||
if (connectionListener) {
|
||||
connectionListener(socket);
|
||||
}
|
||||
this.emit("connection", socket);
|
||||
});
|
||||
|
||||
this.ae.on("error", (error) => {
|
||||
this._listening = false;
|
||||
this.emit("error", error);
|
||||
});
|
||||
|
||||
this.ae.on("listening", () => {
|
||||
this._listening = true;
|
||||
this.emit("listening");
|
||||
});
|
||||
}
|
||||
|
||||
public listen(handle?: net.ListenOptions | number | string, hostname?: string | number | Function, backlog?: number | Function, listeningListener?: Function): this {
|
||||
if (typeof handle === "undefined") {
|
||||
throw new Error("no handle");
|
||||
}
|
||||
|
||||
switch (typeof handle) {
|
||||
case "number": handle = { port: handle }; break;
|
||||
case "string": handle = { path: handle }; break;
|
||||
}
|
||||
switch (typeof hostname) {
|
||||
case "function": listeningListener = hostname; break;
|
||||
case "string": handle.host = hostname; break;
|
||||
case "number": handle.backlog = hostname; break;
|
||||
}
|
||||
switch (typeof backlog) {
|
||||
case "function": listeningListener = backlog; break;
|
||||
case "number": handle.backlog = backlog; break;
|
||||
}
|
||||
|
||||
if (listeningListener) {
|
||||
this.ae.on("listening", () => {
|
||||
listeningListener!();
|
||||
});
|
||||
}
|
||||
|
||||
this.ae.emit("listen", handle);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public close(callback?: Function): this {
|
||||
// close() doesn't fire the close event until all connections are also
|
||||
// closed, but it does prevent new connections.
|
||||
this._listening = false;
|
||||
this.ae.emit("close", this.storeCallback(callback));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public get connections(): number { return this.sockets.size; }
|
||||
public get listening(): boolean { return this._listening; }
|
||||
|
||||
public get maxConnections(): number { throw new Error("not implemented"); }
|
||||
public address(): net.AddressInfo | string { throw new Error("not implemented"); }
|
||||
|
||||
public ref(): this { return this.emitReturnThis("ref"); }
|
||||
public unref(): this { return this.emitReturnThis("unref"); }
|
||||
public getConnections(cb: (error: Error | null, count: number) => void): void { cb(null, this.sockets.size); }
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
private emitReturnThis(event: string, ...args: any[]): this {
|
||||
this.ae.emit(event, ...args);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
type NodeNet = typeof net;
|
||||
|
||||
/**
|
||||
* Implementation of net for the browser.
|
||||
*/
|
||||
class Net implements NodeNet {
|
||||
// @ts-ignore this is because Socket is missing things from the Stream
|
||||
// namespace but I'm unsure how best to provide them (finished,
|
||||
// finished.__promisify__, pipeline, and some others) or if it even matters.
|
||||
public readonly Socket = Socket;
|
||||
public readonly Server = Server;
|
||||
|
||||
public createConnection(target: string | number | net.NetConnectOpts, host?: string | Function, callback?: Function): net.Socket {
|
||||
const socket = new Socket();
|
||||
socket.connect(target, host, callback);
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
public createServer(
|
||||
options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: net.Socket) => void),
|
||||
connectionListener?: (socket: net.Socket) => void,
|
||||
): net.Server {
|
||||
return new Server(options, connectionListener);
|
||||
}
|
||||
|
||||
public connect(): net.Socket { throw new Error("not implemented"); }
|
||||
public isIP(_input: string): number { throw new Error("not implemented"); }
|
||||
public isIPv4(_input: string): boolean { throw new Error("not implemented"); }
|
||||
public isIPv6(_input: string): boolean { throw new Error("not implemented"); }
|
||||
}
|
||||
|
||||
export = new Net();
|
||||
export = client.modules[Module.Net];
|
||||
|
||||
4
packages/ide/src/fill/trash.ts
Normal file
4
packages/ide/src/fill/trash.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Module } from "@coder/protocol";
|
||||
import { client } from "./client";
|
||||
|
||||
export = client.modules[Module.Trash].trash;
|
||||
@@ -1,14 +1,64 @@
|
||||
import { logger, field } from "@coder/logger";
|
||||
import { NotificationService, INotificationHandle, INotificationService, Severity } from "./fill/notification";
|
||||
|
||||
// tslint:disable no-any can have different return values
|
||||
|
||||
interface IRetryItem {
|
||||
/**
|
||||
* How many times this item has been retried.
|
||||
*/
|
||||
count?: number;
|
||||
delay?: number; // In seconds.
|
||||
end?: number; // In ms.
|
||||
fn(): any | Promise<any>; // tslint:disable-line no-any can have different return values
|
||||
|
||||
/**
|
||||
* In seconds.
|
||||
*/
|
||||
delay?: number;
|
||||
|
||||
/**
|
||||
* In milliseconds.
|
||||
*/
|
||||
end?: number;
|
||||
|
||||
/**
|
||||
* Function to run when retrying.
|
||||
*/
|
||||
fn(): any;
|
||||
|
||||
/**
|
||||
* Timer for running this item.
|
||||
*/
|
||||
timeout?: number | NodeJS.Timer;
|
||||
|
||||
/**
|
||||
* Whether the item is retrying or waiting to retry.
|
||||
*/
|
||||
running?: boolean;
|
||||
showInNotification: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An retry-able instance.
|
||||
*/
|
||||
export interface RetryInstance {
|
||||
/**
|
||||
* Run this retry.
|
||||
*/
|
||||
run(error?: Error): void;
|
||||
|
||||
/**
|
||||
* Block on this instance.
|
||||
*/
|
||||
block(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A retry-able instance that doesn't use a promise so it must be manually
|
||||
* ran again on failure and recovered on success.
|
||||
*/
|
||||
export interface ManualRetryInstance extends RetryInstance {
|
||||
/**
|
||||
* Mark this item as recovered.
|
||||
*/
|
||||
recover(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,7 +71,7 @@ interface IRetryItem {
|
||||
* to the user explaining what is happening with an option to immediately retry.
|
||||
*/
|
||||
export class Retry {
|
||||
private items = new Map<string, IRetryItem>();
|
||||
private readonly items = new Map<string, IRetryItem>();
|
||||
|
||||
// Times are in seconds.
|
||||
private readonly retryMinDelay = 1;
|
||||
@@ -50,13 +100,54 @@ export class Retry {
|
||||
}
|
||||
|
||||
/**
|
||||
* Block retries when we know they will fail (for example when starting Wush
|
||||
* back up). If a name is passed, that service will still be allowed to retry
|
||||
* Register a function to retry that starts/connects to a service.
|
||||
*
|
||||
* The service is automatically retried or recovered when the promise resolves
|
||||
* or rejects. If the service dies after starting, it must be manually
|
||||
* retried.
|
||||
*/
|
||||
public register(name: string, fn: () => Promise<any>): RetryInstance;
|
||||
/**
|
||||
* Register a function to retry that starts/connects to a service.
|
||||
*
|
||||
* Must manually retry if it fails to start again or dies after restarting and
|
||||
* manually recover if it succeeds in starting again.
|
||||
*/
|
||||
public register(name: string, fn: () => any): ManualRetryInstance;
|
||||
/**
|
||||
* Register a function to retry that starts/connects to a service.
|
||||
*/
|
||||
public register(name: string, fn: () => any): RetryInstance | ManualRetryInstance {
|
||||
if (this.items.has(name)) {
|
||||
throw new Error(`"${name}" is already registered`);
|
||||
}
|
||||
this.items.set(name, { fn });
|
||||
|
||||
return {
|
||||
block: (): void => this.block(name),
|
||||
run: (error?: Error): void => this.run(name, error),
|
||||
recover: (): void => this.recover(name),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-register a function to retry.
|
||||
*/
|
||||
public unregister(name: string): void {
|
||||
if (!this.items.has(name)) {
|
||||
throw new Error(`"${name}" is not registered`);
|
||||
}
|
||||
this.items.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Block retries when we know they will fail (for example when the socket is
|
||||
* down ). If a name is passed, that service will still be allowed to retry
|
||||
* (unless we have already blocked).
|
||||
*
|
||||
* Blocking without a name will override a block with a name.
|
||||
*/
|
||||
public block(name?: string): void {
|
||||
private block(name?: string): void {
|
||||
if (!this.blocked || !name) {
|
||||
this.blocked = name || true;
|
||||
this.items.forEach((item) => {
|
||||
@@ -68,7 +159,7 @@ export class Retry {
|
||||
/**
|
||||
* Unblock retries and run any that are pending.
|
||||
*/
|
||||
public unblock(): void {
|
||||
private unblock(): void {
|
||||
this.blocked = false;
|
||||
this.items.forEach((item, name) => {
|
||||
if (item.running) {
|
||||
@@ -77,35 +168,10 @@ export class Retry {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a function to retry that starts/connects to a service.
|
||||
*
|
||||
* If the function returns a promise, it will automatically be retried,
|
||||
* recover, & unblock after calling `run` once (otherwise they need to be
|
||||
* called manually).
|
||||
*/
|
||||
// tslint:disable-next-line no-any can have different return values
|
||||
public register(name: string, fn: () => any | Promise<any>, showInNotification: boolean = true): void {
|
||||
if (this.items.has(name)) {
|
||||
throw new Error(`"${name}" is already registered`);
|
||||
}
|
||||
this.items.set(name, { fn, showInNotification });
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a function to retry.
|
||||
*/
|
||||
public unregister(name: string): void {
|
||||
if (!this.items.has(name)) {
|
||||
throw new Error(`"${name}" is not registered`);
|
||||
}
|
||||
this.items.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry a service.
|
||||
*/
|
||||
public run(name: string, error?: Error): void {
|
||||
private run(name: string, error?: Error): void {
|
||||
if (!this.items.has(name)) {
|
||||
throw new Error(`"${name}" is not registered`);
|
||||
}
|
||||
@@ -149,7 +215,7 @@ export class Retry {
|
||||
/**
|
||||
* Reset a service after a successfully recovering.
|
||||
*/
|
||||
public recover(name: string): void {
|
||||
private recover(name: string): void {
|
||||
if (!this.items.has(name)) {
|
||||
throw new Error(`"${name}" is not registered`);
|
||||
}
|
||||
@@ -191,9 +257,9 @@ export class Retry {
|
||||
if (this.blocked === name) {
|
||||
this.unblock();
|
||||
}
|
||||
}).catch(() => {
|
||||
}).catch((error) => {
|
||||
endItem();
|
||||
this.run(name);
|
||||
this.run(name, error);
|
||||
});
|
||||
} else {
|
||||
endItem();
|
||||
@@ -214,8 +280,7 @@ export class Retry {
|
||||
|
||||
const now = Date.now();
|
||||
const items = Array.from(this.items.entries()).filter(([_, item]) => {
|
||||
return item.showInNotification
|
||||
&& typeof item.end !== "undefined"
|
||||
return typeof item.end !== "undefined"
|
||||
&& item.end > now
|
||||
&& item.delay && item.delay >= this.notificationThreshold;
|
||||
}).sort((a, b) => {
|
||||
|
||||
@@ -2,113 +2,3 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/events@*":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
||||
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
|
||||
|
||||
"@types/glob@*":
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
|
||||
integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
|
||||
dependencies:
|
||||
"@types/events" "*"
|
||||
"@types/minimatch" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
|
||||
|
||||
"@types/node@*":
|
||||
version "11.9.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.4.tgz#ceb0048a546db453f6248f2d1d95e937a6f00a14"
|
||||
integrity sha512-Zl8dGvAcEmadgs1tmSPcvwzO1YRsz38bVJQvH1RvRqSR9/5n61Q1ktcDL0ht3FXWR+ZpVmXVwN1LuH4Ax23NsA==
|
||||
|
||||
"@types/rimraf@^2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e"
|
||||
integrity sha512-Hm/bnWq0TCy7jmjeN5bKYij9vw5GrDFWME4IuxV08278NtU/VdGbzsBohcCUJ7+QMqmUq5hpRKB39HeQWJjztQ==
|
||||
dependencies:
|
||||
"@types/glob" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||
|
||||
glob@^7.1.3:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
|
||||
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
|
||||
|
||||
rimraf@^2.6.3:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
||||
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
"xmlhttprequest": "1.8.0"
|
||||
},
|
||||
"jest": {
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"diagnostics": false
|
||||
}
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
@@ -26,7 +31,10 @@
|
||||
"@coder/ide/src/fill/evaluation": "<rootDir>/ide/src/fill/evaluation",
|
||||
"@coder/ide/src/fill/client": "<rootDir>/ide/src/fill/client",
|
||||
"@coder/(.*)/test": "<rootDir>/$1/test",
|
||||
"@coder/(.*)": "<rootDir>/$1/src"
|
||||
"@coder/(.*)": "<rootDir>/$1/src",
|
||||
"vs/(.*)": "<rootDir>/../lib/vscode/src/vs/$1",
|
||||
"vszip": "<rootDir>/../lib/vscode/src/vs/base/node/zip.ts",
|
||||
"^node-pty": "node-pty-prebuilt"
|
||||
},
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
"node-pty-prebuilt": "^0.7.6",
|
||||
"spdlog": "^0.7.2",
|
||||
"trash": "^4.3.0",
|
||||
"tslib": "^1.9.3",
|
||||
"ws": "^6.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google-protobuf": "^3.2.7",
|
||||
"@types/rimraf": "^2.0.2",
|
||||
"@types/text-encoding": "^0.0.35",
|
||||
"rimraf": "^2.6.3",
|
||||
"text-encoding": "^0.7.0",
|
||||
"ts-protoc-gen": "^0.8.0"
|
||||
}
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
import { EventEmitter } from "events";
|
||||
import { PathLike } from "fs";
|
||||
import { ExecException, ExecOptions } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { Emitter } from "@coder/events";
|
||||
import { logger, field } from "@coder/logger";
|
||||
import { Ping, NewEvalMessage, ServerMessage, EvalDoneMessage, EvalFailedMessage, ClientMessage, WorkingInitMessage, EvalEventMessage } from "../proto";
|
||||
import { ReadWriteConnection, InitData, OperatingSystem, SharedProcessData } from "../common/connection";
|
||||
import { ActiveEvalHelper, EvalHelper, Disposer, ServerActiveEvalHelper } from "../common/helpers";
|
||||
import { stringify, parse } from "../common/util";
|
||||
import { ReadWriteConnection, InitData, SharedProcessData } from "../common/connection";
|
||||
import { Module, ServerProxy } from "../common/proxy";
|
||||
import { argumentToProto, protoToArgument, moduleToProto, protoToModule, protoToOperatingSystem } from "../common/util";
|
||||
import { Argument, Ping, ServerMessage, ClientMessage, Method, Event, Callback } from "../proto";
|
||||
import { FsModule, ChildProcessModule, NetModule, NodePtyModule, SpdlogModule, TrashModule } from "./modules";
|
||||
|
||||
// tslint:disable no-any
|
||||
|
||||
interface ProxyData {
|
||||
promise: Promise<void>;
|
||||
instance: any;
|
||||
callbacks: Map<number, (...args: any[]) => void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client accepts an arbitrary connection intended to communicate with the Server.
|
||||
* Client accepts a connection to communicate with the server.
|
||||
*/
|
||||
export class Client {
|
||||
private evalId = 0;
|
||||
private readonly evalDoneEmitter = new Emitter<EvalDoneMessage>();
|
||||
private readonly evalFailedEmitter = new Emitter<EvalFailedMessage>();
|
||||
private readonly evalEventEmitter = new Emitter<EvalEventMessage>();
|
||||
private messageId = 0;
|
||||
private callbackId = 0;
|
||||
private readonly proxies = new Map<number | Module, ProxyData>();
|
||||
private readonly successEmitter = new Emitter<Method.Success>();
|
||||
private readonly failEmitter = new Emitter<Method.Fail>();
|
||||
private readonly eventEmitter = new Emitter<{ event: string; args: any[]; }>();
|
||||
|
||||
private _initData: InitData | undefined;
|
||||
private readonly initDataEmitter = new Emitter<InitData>();
|
||||
@@ -22,37 +35,123 @@ export class Client {
|
||||
private readonly sharedProcessActiveEmitter = new Emitter<SharedProcessData>();
|
||||
public readonly onSharedProcessActive = this.sharedProcessActiveEmitter.event;
|
||||
|
||||
private disconnected: boolean = false;
|
||||
|
||||
// The socket timeout is 60s, so we need to send a ping periodically to
|
||||
// prevent it from closing.
|
||||
private pingTimeout: NodeJS.Timer | number | undefined;
|
||||
private readonly pingTimeoutDelay = 30000;
|
||||
|
||||
private readonly responseTimeout = 10000;
|
||||
|
||||
public readonly modules: {
|
||||
[Module.ChildProcess]: ChildProcessModule,
|
||||
[Module.Fs]: FsModule,
|
||||
[Module.Net]: NetModule,
|
||||
[Module.NodePty]: NodePtyModule,
|
||||
[Module.Spdlog]: SpdlogModule,
|
||||
[Module.Trash]: TrashModule,
|
||||
};
|
||||
|
||||
/**
|
||||
* @param connection Established connection to the server
|
||||
*/
|
||||
public constructor(
|
||||
private readonly connection: ReadWriteConnection,
|
||||
) {
|
||||
connection.onMessage((data) => {
|
||||
public constructor(private readonly connection: ReadWriteConnection) {
|
||||
connection.onMessage(async (data) => {
|
||||
let message: ServerMessage | undefined;
|
||||
try {
|
||||
message = ServerMessage.deserializeBinary(data);
|
||||
this.handleMessage(message);
|
||||
await this.handleMessage(message);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Failed to handle server message",
|
||||
field("id", message && message.hasEvalEvent() ? message.getEvalEvent()!.getId() : undefined),
|
||||
field("id", message && this.getMessageId(message)),
|
||||
field("length", data.byteLength),
|
||||
field("error", error.message),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
connection.onClose(() => {
|
||||
clearTimeout(this.pingTimeout as any); // tslint:disable-line no-any
|
||||
this.pingTimeout = undefined;
|
||||
this.createProxy(Module.ChildProcess);
|
||||
this.createProxy(Module.Fs);
|
||||
this.createProxy(Module.Net);
|
||||
this.createProxy(Module.NodePty);
|
||||
this.createProxy(Module.Spdlog);
|
||||
this.createProxy(Module.Trash);
|
||||
|
||||
this.modules = {
|
||||
[Module.ChildProcess]: new ChildProcessModule(this.getProxy(Module.ChildProcess).instance),
|
||||
[Module.Fs]: new FsModule(this.getProxy(Module.Fs).instance),
|
||||
[Module.Net]: new NetModule(this.getProxy(Module.Net).instance),
|
||||
[Module.NodePty]: new NodePtyModule(this.getProxy(Module.NodePty).instance),
|
||||
[Module.Spdlog]: new SpdlogModule(this.getProxy(Module.Spdlog).instance),
|
||||
[Module.Trash]: new TrashModule(this.getProxy(Module.Trash).instance),
|
||||
};
|
||||
|
||||
// Methods that don't follow the standard callback pattern (an error
|
||||
// followed by a single result) need to provide a custom promisify function.
|
||||
Object.defineProperty(this.modules[Module.Fs].exists, promisify.custom, {
|
||||
value: (path: PathLike): Promise<boolean> => {
|
||||
return new Promise((resolve): void => this.modules[Module.Fs].exists(path, resolve));
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(this.modules[Module.ChildProcess].exec, promisify.custom, {
|
||||
value: (
|
||||
command: string,
|
||||
options?: { encoding?: string | null } & ExecOptions | null,
|
||||
): Promise<{ stdout: string | Buffer, stderr: string | Buffer }> => {
|
||||
return new Promise((resolve, reject): void => {
|
||||
this.modules[Module.ChildProcess].exec(command, options, (error: ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* If the connection is interrupted, the calls will neither succeed nor fail
|
||||
* nor exit so we need to send a failure on all of them as well as trigger
|
||||
* events so things like child processes can clean up and possibly restart.
|
||||
*/
|
||||
const handleDisconnect = (): void => {
|
||||
this.disconnected = true;
|
||||
logger.trace(() => [
|
||||
"disconnected from server",
|
||||
field("proxies", this.proxies.size),
|
||||
field("callbacks", Array.from(this.proxies.values()).reduce((count, p) => count + p.callbacks.size, 0)),
|
||||
field("success listeners", this.successEmitter.counts),
|
||||
field("fail listeners", this.failEmitter.counts),
|
||||
field("event listeners", this.eventEmitter.counts),
|
||||
]);
|
||||
|
||||
const message = new Method.Fail();
|
||||
const error = new Error("disconnected");
|
||||
message.setResponse(argumentToProto(error));
|
||||
this.failEmitter.emit(message);
|
||||
|
||||
this.eventEmitter.emit({ event: "disconnected", args: [error] });
|
||||
this.eventEmitter.emit({ event: "done", args: [] });
|
||||
};
|
||||
|
||||
connection.onDown(() => handleDisconnect());
|
||||
connection.onClose(() => {
|
||||
clearTimeout(this.pingTimeout as any);
|
||||
this.pingTimeout = undefined;
|
||||
handleDisconnect();
|
||||
this.proxies.clear();
|
||||
this.successEmitter.dispose();
|
||||
this.failEmitter.dispose();
|
||||
this.eventEmitter.dispose();
|
||||
this.initDataEmitter.dispose();
|
||||
this.sharedProcessActiveEmitter.dispose();
|
||||
});
|
||||
connection.onUp(() => this.disconnected = false);
|
||||
|
||||
this.initDataPromise = new Promise((resolve): void => {
|
||||
this.initDataEmitter.event(resolve);
|
||||
});
|
||||
@@ -60,6 +159,9 @@ export class Client {
|
||||
this.startPinging();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the connection.
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.connection.close();
|
||||
}
|
||||
@@ -68,173 +170,217 @@ export class Client {
|
||||
return this.initDataPromise;
|
||||
}
|
||||
|
||||
public run(func: (helper: ServerActiveEvalHelper) => Disposer): ActiveEvalHelper;
|
||||
public run<T1>(func: (helper: ServerActiveEvalHelper, a1: T1) => Disposer, a1: T1): ActiveEvalHelper;
|
||||
public run<T1, T2>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2) => Disposer, a1: T1, a2: T2): ActiveEvalHelper;
|
||||
public run<T1, T2, T3>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3) => Disposer, a1: T1, a2: T2, a3: T3): ActiveEvalHelper;
|
||||
public run<T1, T2, T3, T4>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3, a4: T4) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4): ActiveEvalHelper;
|
||||
public run<T1, T2, T3, T4, T5>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): ActiveEvalHelper;
|
||||
public run<T1, T2, T3, T4, T5, T6>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): ActiveEvalHelper;
|
||||
/**
|
||||
* Run a function on the server and provide an event emitter which allows
|
||||
* listening and emitting to the emitter provided to that function. The
|
||||
* function should return a disposer for cleaning up when the client
|
||||
* disconnects and for notifying when disposal has happened outside manual
|
||||
* activation.
|
||||
* Make a remote call for a proxy's method using proto.
|
||||
*/
|
||||
public run<T1, T2, T3, T4, T5, T6>(func: (helper: ServerActiveEvalHelper, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6) => Disposer, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6): ActiveEvalHelper {
|
||||
const doEval = this.doEvaluate(func, a1, a2, a3, a4, a5, a6, true);
|
||||
|
||||
// This takes server events and emits them to the client's emitter.
|
||||
const eventEmitter = new EventEmitter();
|
||||
const d1 = this.evalEventEmitter.event((msg) => {
|
||||
if (msg.getId() === doEval.id) {
|
||||
eventEmitter.emit(msg.getEvent(), ...msg.getArgsList().map(parse));
|
||||
private remoteCall(proxyId: number | Module, method: string, args: any[]): Promise<any> {
|
||||
if (this.disconnected && typeof proxyId === "number") {
|
||||
// Can assume killing or closing works because a disconnected proxy
|
||||
// is disposed on the server's side.
|
||||
switch (method) {
|
||||
case "close":
|
||||
case "kill":
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
doEval.completed.then(() => {
|
||||
d1.dispose();
|
||||
}).catch((ex) => {
|
||||
d1.dispose();
|
||||
// This error event is only received by the client.
|
||||
eventEmitter.emit("error", ex);
|
||||
});
|
||||
return Promise.reject(
|
||||
new Error(`Unable to call "${method}" on proxy ${proxyId}: disconnected`),
|
||||
);
|
||||
}
|
||||
|
||||
return new ActiveEvalHelper({
|
||||
// This takes client events and emits them to the server's emitter and
|
||||
// listens to events received from the server (via the event hook above).
|
||||
// tslint:disable no-any
|
||||
on: (event: string, cb: (...args: any[]) => void): EventEmitter => eventEmitter.on(event, cb),
|
||||
emit: (event: string, ...args: any[]): void => {
|
||||
const eventsMsg = new EvalEventMessage();
|
||||
eventsMsg.setId(doEval.id);
|
||||
eventsMsg.setEvent(event);
|
||||
eventsMsg.setArgsList(args.map((a) => stringify(a)));
|
||||
const clientMsg = new ClientMessage();
|
||||
clientMsg.setEvalEvent(eventsMsg);
|
||||
this.connection.send(clientMsg.serializeBinary());
|
||||
},
|
||||
removeAllListeners: (event: string): EventEmitter => eventEmitter.removeAllListeners(event),
|
||||
// tslint:enable no-any
|
||||
});
|
||||
}
|
||||
const message = new Method();
|
||||
const id = this.messageId++;
|
||||
let proxyMessage: Method.Named | Method.Numbered;
|
||||
if (typeof proxyId === "string") {
|
||||
proxyMessage = new Method.Named();
|
||||
proxyMessage.setModule(moduleToProto(proxyId));
|
||||
message.setNamedProxy(proxyMessage);
|
||||
} else {
|
||||
proxyMessage = new Method.Numbered();
|
||||
proxyMessage.setProxyId(proxyId);
|
||||
message.setNumberedProxy(proxyMessage);
|
||||
}
|
||||
proxyMessage.setId(id);
|
||||
proxyMessage.setMethod(method);
|
||||
|
||||
public evaluate<R>(func: (helper: EvalHelper) => R | Promise<R>): Promise<R>;
|
||||
public evaluate<R, T1>(func: (helper: EvalHelper, a1: T1) => R | Promise<R>, a1: T1): Promise<R>;
|
||||
public evaluate<R, T1, T2>(func: (helper: EvalHelper, a1: T1, a2: T2) => R | Promise<R>, a1: T1, a2: T2): Promise<R>;
|
||||
public evaluate<R, T1, T2, T3>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3) => R | Promise<R>, a1: T1, a2: T2, a3: T3): Promise<R>;
|
||||
public evaluate<R, T1, T2, T3, T4>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4): Promise<R>;
|
||||
public evaluate<R, T1, T2, T3, T4, T5>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): Promise<R>;
|
||||
public evaluate<R, T1, T2, T3, T4, T5, T6>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): Promise<R>;
|
||||
/**
|
||||
* Evaluates a function on the server.
|
||||
* To pass variables, ensure they are serializable and passed through the included function.
|
||||
* @example
|
||||
* const returned = await this.client.evaluate((helper, value) => {
|
||||
* return value;
|
||||
* }, "hi");
|
||||
* console.log(returned);
|
||||
* // output: "hi"
|
||||
* @param func Function to evaluate
|
||||
* @returns Promise rejected or resolved from the evaluated function
|
||||
*/
|
||||
public evaluate<R, T1, T2, T3, T4, T5, T6>(func: (helper: EvalHelper, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6) => R | Promise<R>, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6): Promise<R> {
|
||||
return this.doEvaluate(func, a1, a2, a3, a4, a5, a6, false).completed;
|
||||
}
|
||||
const storeCallback = (cb: (...args: any[]) => void): number => {
|
||||
const callbackId = this.callbackId++;
|
||||
logger.trace(() => [
|
||||
"storing callback",
|
||||
field("proxyId", proxyId),
|
||||
field("callbackId", callbackId),
|
||||
]);
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
private doEvaluate<R, T1, T2, T3, T4, T5, T6>(func: (...args: any[]) => void | Promise<void> | R | Promise<R>, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6, active: boolean = false): {
|
||||
readonly completed: Promise<R>;
|
||||
readonly id: number;
|
||||
} {
|
||||
const newEval = new NewEvalMessage();
|
||||
const id = this.evalId++;
|
||||
newEval.setId(id);
|
||||
newEval.setActive(active);
|
||||
newEval.setArgsList([a1, a2, a3, a4, a5, a6].map((a) => stringify(a)));
|
||||
newEval.setFunction(func.toString());
|
||||
this.getProxy(proxyId).callbacks.set(callbackId, cb);
|
||||
|
||||
const clientMsg = new ClientMessage();
|
||||
clientMsg.setNewEval(newEval);
|
||||
this.connection.send(clientMsg.serializeBinary());
|
||||
return callbackId;
|
||||
};
|
||||
|
||||
const completed = new Promise<R>((resolve, reject): void => {
|
||||
logger.trace(() => [
|
||||
"sending",
|
||||
field("id", id),
|
||||
field("proxyId", proxyId),
|
||||
field("method", method),
|
||||
]);
|
||||
|
||||
proxyMessage.setArgsList(args.map((a) => argumentToProto(a, storeCallback)));
|
||||
|
||||
const clientMessage = new ClientMessage();
|
||||
clientMessage.setMethod(message);
|
||||
this.connection.send(clientMessage.serializeBinary());
|
||||
|
||||
// The server will send back a fail or success message when the method
|
||||
// has completed, so we listen for that based on the message's unique ID.
|
||||
const promise = new Promise((resolve, reject): void => {
|
||||
const dispose = (): void => {
|
||||
d1.dispose();
|
||||
d2.dispose();
|
||||
clearTimeout(timeout as any);
|
||||
};
|
||||
|
||||
const d1 = this.evalDoneEmitter.event((doneMsg) => {
|
||||
if (doneMsg.getId() === id) {
|
||||
dispose();
|
||||
resolve(parse(doneMsg.getResponse()));
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
dispose();
|
||||
reject(new Error("timed out"));
|
||||
}, this.responseTimeout);
|
||||
|
||||
const d1 = this.successEmitter.event(id, (message) => {
|
||||
dispose();
|
||||
resolve(this.protoToArgument(message.getResponse(), promise));
|
||||
});
|
||||
|
||||
const d2 = this.evalFailedEmitter.event((failedMsg) => {
|
||||
if (failedMsg.getId() === id) {
|
||||
dispose();
|
||||
reject(parse(failedMsg.getResponse()));
|
||||
}
|
||||
const d2 = this.failEmitter.event(id, (message) => {
|
||||
dispose();
|
||||
reject(protoToArgument(message.getResponse()));
|
||||
});
|
||||
});
|
||||
|
||||
return { completed, id };
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a message from the server. All incoming server messages should be
|
||||
* routed through here.
|
||||
* Handle all messages from the server.
|
||||
*/
|
||||
private handleMessage(message: ServerMessage): void {
|
||||
if (message.hasInit()) {
|
||||
const init = message.getInit()!;
|
||||
let opSys: OperatingSystem;
|
||||
switch (init.getOperatingSystem()) {
|
||||
case WorkingInitMessage.OperatingSystem.WINDOWS:
|
||||
opSys = OperatingSystem.Windows;
|
||||
break;
|
||||
case WorkingInitMessage.OperatingSystem.LINUX:
|
||||
opSys = OperatingSystem.Linux;
|
||||
break;
|
||||
case WorkingInitMessage.OperatingSystem.MAC:
|
||||
opSys = OperatingSystem.Mac;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unsupported operating system ${init.getOperatingSystem()}`);
|
||||
}
|
||||
this._initData = {
|
||||
dataDirectory: init.getDataDirectory(),
|
||||
homeDirectory: init.getHomeDirectory(),
|
||||
tmpDirectory: init.getTmpDirectory(),
|
||||
workingDirectory: init.getWorkingDirectory(),
|
||||
os: opSys,
|
||||
shell: init.getShell(),
|
||||
builtInExtensionsDirectory: init.getBuiltinExtensionsDir(),
|
||||
};
|
||||
this.initDataEmitter.emit(this._initData);
|
||||
} else if (message.hasEvalDone()) {
|
||||
this.evalDoneEmitter.emit(message.getEvalDone()!);
|
||||
} else if (message.hasEvalFailed()) {
|
||||
this.evalFailedEmitter.emit(message.getEvalFailed()!);
|
||||
} else if (message.hasEvalEvent()) {
|
||||
this.evalEventEmitter.emit(message.getEvalEvent()!);
|
||||
} else if (message.hasSharedProcessActive()) {
|
||||
const sharedProcessActiveMessage = message.getSharedProcessActive()!;
|
||||
this.sharedProcessActiveEmitter.emit({
|
||||
socketPath: sharedProcessActiveMessage.getSocketPath(),
|
||||
logPath: sharedProcessActiveMessage.getLogPath(),
|
||||
});
|
||||
} else if (message.hasPong()) {
|
||||
// Nothing to do since we run the pings on a timer, in case either message
|
||||
// is dropped which would break the ping cycle.
|
||||
} else {
|
||||
throw new Error("unknown message type");
|
||||
private async handleMessage(message: ServerMessage): Promise<void> {
|
||||
switch (message.getMsgCase()) {
|
||||
case ServerMessage.MsgCase.INIT:
|
||||
const init = message.getInit()!;
|
||||
this._initData = {
|
||||
dataDirectory: init.getDataDirectory(),
|
||||
homeDirectory: init.getHomeDirectory(),
|
||||
tmpDirectory: init.getTmpDirectory(),
|
||||
workingDirectory: init.getWorkingDirectory(),
|
||||
os: protoToOperatingSystem(init.getOperatingSystem()),
|
||||
shell: init.getShell(),
|
||||
extensionsDirectory: init.getExtensionsDirectory(),
|
||||
builtInExtensionsDirectory: init.getBuiltinExtensionsDir(),
|
||||
};
|
||||
this.initDataEmitter.emit(this._initData);
|
||||
break;
|
||||
case ServerMessage.MsgCase.SUCCESS:
|
||||
this.emitSuccess(message.getSuccess()!);
|
||||
break;
|
||||
case ServerMessage.MsgCase.FAIL:
|
||||
this.emitFail(message.getFail()!);
|
||||
break;
|
||||
case ServerMessage.MsgCase.EVENT:
|
||||
await this.emitEvent(message.getEvent()!);
|
||||
break;
|
||||
case ServerMessage.MsgCase.CALLBACK:
|
||||
await this.runCallback(message.getCallback()!);
|
||||
break;
|
||||
case ServerMessage.MsgCase.SHARED_PROCESS_ACTIVE:
|
||||
const sharedProcessActiveMessage = message.getSharedProcessActive()!;
|
||||
this.sharedProcessActiveEmitter.emit({
|
||||
socketPath: sharedProcessActiveMessage.getSocketPath(),
|
||||
logPath: sharedProcessActiveMessage.getLogPath(),
|
||||
});
|
||||
break;
|
||||
case ServerMessage.MsgCase.PONG:
|
||||
// Nothing to do since pings are on a timer rather than waiting for the
|
||||
// next pong in case a message from either the client or server is dropped
|
||||
// which would break the ping cycle.
|
||||
break;
|
||||
default:
|
||||
throw new Error("unknown message type");
|
||||
}
|
||||
}
|
||||
|
||||
private startPinging = (): void => {
|
||||
/**
|
||||
* Convert message to a success event.
|
||||
*/
|
||||
private emitSuccess(message: Method.Success): void {
|
||||
logger.trace(() => [
|
||||
"received resolve",
|
||||
field("id", message.getId()),
|
||||
]);
|
||||
|
||||
this.successEmitter.emit(message.getId(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert message to a fail event.
|
||||
*/
|
||||
private emitFail(message: Method.Fail): void {
|
||||
logger.trace(() => [
|
||||
"received reject",
|
||||
field("id", message.getId()),
|
||||
]);
|
||||
|
||||
this.failEmitter.emit(message.getId(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event received from the server. We could send requests for "on" to
|
||||
* the server and serialize functions using IDs, but doing it that way makes
|
||||
* it possible to miss events depending on whether the server receives the
|
||||
* request before it emits. Instead, emit all events from the server so all
|
||||
* events are always caught on the client.
|
||||
*/
|
||||
private async emitEvent(message: Event): Promise<void> {
|
||||
const eventMessage = message.getNamedEvent()! || message.getNumberedEvent()!;
|
||||
const proxyId = message.getNamedEvent()
|
||||
? protoToModule(message.getNamedEvent()!.getModule())
|
||||
: message.getNumberedEvent()!.getProxyId();
|
||||
const event = eventMessage.getEvent();
|
||||
await this.ensureResolved(proxyId);
|
||||
logger.trace(() => [
|
||||
"received event",
|
||||
field("proxyId", proxyId),
|
||||
field("event", event),
|
||||
]);
|
||||
|
||||
const args = eventMessage.getArgsList().map((a) => this.protoToArgument(a));
|
||||
this.eventEmitter.emit(proxyId, { event, args });
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback as requested by the server. Since we don't know when
|
||||
* callbacks get garbage collected we dispose them only when the proxy
|
||||
* disposes. That means they should only be used if they run for the lifetime
|
||||
* of the proxy (like child_process.exec), otherwise we'll leak. They should
|
||||
* also only be used when passed together with the method. If they are sent
|
||||
* afterward, they may never be called due to timing issues.
|
||||
*/
|
||||
private async runCallback(message: Callback): Promise<void> {
|
||||
const callbackMessage = message.getNamedCallback()! || message.getNumberedCallback()!;
|
||||
const proxyId = message.getNamedCallback()
|
||||
? protoToModule(message.getNamedCallback()!.getModule())
|
||||
: message.getNumberedCallback()!.getProxyId();
|
||||
const callbackId = callbackMessage.getCallbackId();
|
||||
await this.ensureResolved(proxyId);
|
||||
logger.trace(() => [
|
||||
"running callback",
|
||||
field("proxyId", proxyId),
|
||||
field("callbackId", callbackId),
|
||||
]);
|
||||
const args = callbackMessage.getArgsList().map((a) => this.protoToArgument(a));
|
||||
this.getProxy(proxyId).callbacks.get(callbackId)!(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the ping loop. Does nothing if already pinging.
|
||||
*/
|
||||
private readonly startPinging = (): void => {
|
||||
if (typeof this.pingTimeout !== "undefined") {
|
||||
return;
|
||||
}
|
||||
@@ -250,4 +396,142 @@ export class Client {
|
||||
|
||||
schedulePing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the message's ID if it has one or a string identifier. For logging
|
||||
* errors with an ID to make the error more useful.
|
||||
*/
|
||||
private getMessageId(message: ServerMessage): number | string | undefined {
|
||||
if (message.hasInit()) {
|
||||
return "init";
|
||||
} else if (message.hasSuccess()) {
|
||||
return message.getSuccess()!.getId();
|
||||
} else if (message.hasFail()) {
|
||||
return message.getFail()!.getId();
|
||||
} else if (message.hasEvent()) {
|
||||
const eventMessage = message.getEvent()!.getNamedEvent()!
|
||||
|| message.getEvent()!.getNumberedEvent()!;
|
||||
|
||||
return `event: ${eventMessage.getEvent()}`;
|
||||
} else if (message.hasCallback()) {
|
||||
const callbackMessage = message.getCallback()!.getNamedCallback()!
|
||||
|| message.getCallback()!.getNumberedCallback()!;
|
||||
|
||||
return `callback: ${callbackMessage.getCallbackId()}`;
|
||||
} else if (message.hasSharedProcessActive()) {
|
||||
return "shared";
|
||||
} else if (message.hasPong()) {
|
||||
return "pong";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a proxy that makes remote calls.
|
||||
*/
|
||||
private createProxy<T>(proxyId: number | Module, promise: Promise<any> = Promise.resolve()): T {
|
||||
logger.trace(() => [
|
||||
"creating proxy",
|
||||
field("proxyId", proxyId),
|
||||
]);
|
||||
|
||||
const instance = new Proxy({
|
||||
proxyId,
|
||||
onDone: (cb: (...args: any[]) => void): void => {
|
||||
this.eventEmitter.event(proxyId, (event) => {
|
||||
if (event.event === "done") {
|
||||
cb(...event.args);
|
||||
}
|
||||
});
|
||||
},
|
||||
onEvent: (cb: (event: string, ...args: any[]) => void): void => {
|
||||
this.eventEmitter.event(proxyId, (event) => {
|
||||
cb(event.event, ...event.args);
|
||||
});
|
||||
},
|
||||
}, {
|
||||
get: (target: any, name: string): any => {
|
||||
// When resolving a promise with a proxy, it will check for "then".
|
||||
if (name === "then") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof target[name] === "undefined") {
|
||||
target[name] = (...args: any[]): Promise<any> | ServerProxy => {
|
||||
return this.remoteCall(proxyId, name, args);
|
||||
};
|
||||
}
|
||||
|
||||
return target[name];
|
||||
},
|
||||
});
|
||||
|
||||
this.proxies.set(proxyId, {
|
||||
promise,
|
||||
instance,
|
||||
callbacks: new Map(),
|
||||
});
|
||||
|
||||
instance.onDone(() => {
|
||||
const log = (): void => {
|
||||
logger.trace(() => [
|
||||
typeof proxyId === "number" ? "disposed proxy" : "disposed proxy callbacks",
|
||||
field("proxyId", proxyId),
|
||||
field("disconnected", this.disconnected),
|
||||
field("callbacks", Array.from(this.proxies.values()).reduce((count, proxy) => count + proxy.callbacks.size, 0)),
|
||||
field("success listeners", this.successEmitter.counts),
|
||||
field("fail listeners", this.failEmitter.counts),
|
||||
field("event listeners", this.eventEmitter.counts),
|
||||
]);
|
||||
};
|
||||
|
||||
// Uniquely identified items (top-level module proxies) can continue to
|
||||
// be used so we don't need to delete them.
|
||||
if (typeof proxyId === "number") {
|
||||
const dispose = (): void => {
|
||||
this.proxies.delete(proxyId);
|
||||
this.eventEmitter.dispose(proxyId);
|
||||
log();
|
||||
};
|
||||
if (!this.disconnected) {
|
||||
instance.dispose().then(dispose).catch(dispose);
|
||||
} else {
|
||||
dispose();
|
||||
}
|
||||
} else {
|
||||
// The callbacks will still be unusable though.
|
||||
this.getProxy(proxyId).callbacks.clear();
|
||||
log();
|
||||
}
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* We aren't guaranteed the promise will call all the `then` callbacks
|
||||
* synchronously once it resolves, so the event message can come in and fire
|
||||
* before a caller has been able to attach an event. Waiting for the promise
|
||||
* ensures it runs after everything else.
|
||||
*/
|
||||
private async ensureResolved(proxyId: number | Module): Promise<void> {
|
||||
await this.getProxy(proxyId).promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as protoToArgument except provides createProxy.
|
||||
*/
|
||||
private protoToArgument(value?: Argument, promise?: Promise<any>): any {
|
||||
return protoToArgument(value, undefined, (id) => this.createProxy(id, promise));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a proxy. Error if it doesn't exist.
|
||||
*/
|
||||
private getProxy(proxyId: number | Module): ProxyData {
|
||||
if (!this.proxies.has(proxyId)) {
|
||||
throw new Error(`proxy ${proxyId} disposed too early`);
|
||||
}
|
||||
|
||||
return this.proxies.get(proxyId)!;
|
||||
}
|
||||
}
|
||||
|
||||
136
packages/protocol/src/browser/modules/child_process.ts
Normal file
136
packages/protocol/src/browser/modules/child_process.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as cp from "child_process";
|
||||
import * as net from "net";
|
||||
import * as stream from "stream";
|
||||
import { callbackify } from "util";
|
||||
import { ClientProxy } from "../../common/proxy";
|
||||
import { ChildProcessModuleProxy, ChildProcessProxy, ChildProcessProxies } from "../../node/modules/child_process";
|
||||
import { Readable, Writable } from "./stream";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
export class ChildProcess extends ClientProxy<ChildProcessProxy> implements cp.ChildProcess {
|
||||
public readonly stdin: stream.Writable;
|
||||
public readonly stdout: stream.Readable;
|
||||
public readonly stderr: stream.Readable;
|
||||
public readonly stdio: [stream.Writable, stream.Readable, stream.Readable];
|
||||
|
||||
private _connected: boolean = false;
|
||||
private _killed: boolean = false;
|
||||
private _pid = -1;
|
||||
|
||||
public constructor(proxyPromises: Promise<ChildProcessProxies>) {
|
||||
super(proxyPromises.then((p) => p.childProcess));
|
||||
this.stdin = new Writable(proxyPromises.then((p) => p.stdin!));
|
||||
this.stdout = new Readable(proxyPromises.then((p) => p.stdout!));
|
||||
this.stderr = new Readable(proxyPromises.then((p) => p.stderr!));
|
||||
this.stdio = [this.stdin, this.stdout, this.stderr];
|
||||
|
||||
this.catch(this.proxy.getPid().then((pid) => {
|
||||
this._pid = pid;
|
||||
this._connected = true;
|
||||
}));
|
||||
this.on("disconnect", () => this._connected = false);
|
||||
this.on("exit", () => {
|
||||
this._connected = false;
|
||||
this._killed = true;
|
||||
});
|
||||
}
|
||||
|
||||
public get pid(): number {
|
||||
return this._pid;
|
||||
}
|
||||
|
||||
public get connected(): boolean {
|
||||
return this._connected;
|
||||
}
|
||||
|
||||
public get killed(): boolean {
|
||||
return this._killed;
|
||||
}
|
||||
|
||||
public kill(): void {
|
||||
this._killed = true;
|
||||
this.catch(this.proxy.kill());
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
this.catch(this.proxy.disconnect());
|
||||
}
|
||||
|
||||
public ref(): void {
|
||||
this.catch(this.proxy.ref());
|
||||
}
|
||||
|
||||
public unref(): void {
|
||||
this.catch(this.proxy.unref());
|
||||
}
|
||||
|
||||
public send(
|
||||
message: any, // tslint:disable-line no-any
|
||||
sendHandle?: net.Socket | net.Server | ((error: Error) => void),
|
||||
options?: cp.MessageOptions | ((error: Error) => void),
|
||||
callback?: (error: Error) => void): boolean {
|
||||
if (typeof sendHandle === "function") {
|
||||
callback = sendHandle;
|
||||
sendHandle = undefined;
|
||||
} else if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
if (sendHandle || options) {
|
||||
throw new Error("sendHandle and options are not supported");
|
||||
}
|
||||
|
||||
callbackify(this.proxy.send)(message, (error) => {
|
||||
if (callback) {
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
|
||||
return true; // Always true since we can't get this synchronously.
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit and close the process when disconnected.
|
||||
*/
|
||||
protected handleDisconnect(): void {
|
||||
this.emit("exit", 1);
|
||||
this.emit("close");
|
||||
}
|
||||
}
|
||||
|
||||
export class ChildProcessModule {
|
||||
public constructor(private readonly proxy: ChildProcessModuleProxy) {}
|
||||
|
||||
public exec = (
|
||||
command: string,
|
||||
options?: { encoding?: string | null } & cp.ExecOptions | null
|
||||
| ((error: cp.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => void),
|
||||
callback?: ((error: cp.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => void),
|
||||
): cp.ChildProcess => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
|
||||
return new ChildProcess(this.proxy.exec(command, options, callback));
|
||||
}
|
||||
|
||||
public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
|
||||
if (!Array.isArray(args)) {
|
||||
options = args;
|
||||
args = undefined;
|
||||
}
|
||||
|
||||
return new ChildProcess(this.proxy.fork(modulePath, args, options));
|
||||
}
|
||||
|
||||
public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => {
|
||||
if (!Array.isArray(args)) {
|
||||
options = args;
|
||||
args = undefined;
|
||||
}
|
||||
|
||||
return new ChildProcess(this.proxy.spawn(command, args, options));
|
||||
}
|
||||
}
|
||||
357
packages/protocol/src/browser/modules/fs.ts
Normal file
357
packages/protocol/src/browser/modules/fs.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
import * as fs from "fs";
|
||||
import { callbackify } from "util";
|
||||
import { ClientProxy, Batch } from "../../common/proxy";
|
||||
import { IEncodingOptions, IEncodingOptionsCallback } from "../../common/util";
|
||||
import { FsModuleProxy, Stats as IStats, WatcherProxy, WriteStreamProxy } from "../../node/modules/fs";
|
||||
import { Writable } from "./stream";
|
||||
|
||||
// tslint:disable no-any
|
||||
// tslint:disable completed-docs
|
||||
|
||||
class StatBatch extends Batch<IStats, { path: fs.PathLike }> {
|
||||
public constructor(private readonly proxy: FsModuleProxy) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected remoteCall(batch: { path: fs.PathLike }[]): Promise<(IStats | Error)[]> {
|
||||
return this.proxy.statBatch(batch);
|
||||
}
|
||||
}
|
||||
|
||||
class LstatBatch extends Batch<IStats, { path: fs.PathLike }> {
|
||||
public constructor(private readonly proxy: FsModuleProxy) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected remoteCall(batch: { path: fs.PathLike }[]): Promise<(IStats | Error)[]> {
|
||||
return this.proxy.lstatBatch(batch);
|
||||
}
|
||||
}
|
||||
|
||||
class ReaddirBatch extends Batch<Buffer[] | fs.Dirent[] | string[], { path: fs.PathLike, options: IEncodingOptions }> {
|
||||
public constructor(private readonly proxy: FsModuleProxy) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected remoteCall(queue: { path: fs.PathLike, options: IEncodingOptions }[]): Promise<(Buffer[] | fs.Dirent[] | string[] | Error)[]> {
|
||||
return this.proxy.readdirBatch(queue);
|
||||
}
|
||||
}
|
||||
|
||||
class Watcher extends ClientProxy<WatcherProxy> implements fs.FSWatcher {
|
||||
public close(): void {
|
||||
this.catch(this.proxy.close());
|
||||
}
|
||||
|
||||
protected handleDisconnect(): void {
|
||||
this.emit("close");
|
||||
}
|
||||
}
|
||||
|
||||
class WriteStream extends Writable<WriteStreamProxy> implements fs.WriteStream {
|
||||
public get bytesWritten(): number {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public get path(): string | Buffer {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.catch(this.proxy.close());
|
||||
}
|
||||
}
|
||||
|
||||
export class FsModule {
|
||||
private readonly statBatch: StatBatch;
|
||||
private readonly lstatBatch: LstatBatch;
|
||||
private readonly readdirBatch: ReaddirBatch;
|
||||
|
||||
public constructor(private readonly proxy: FsModuleProxy) {
|
||||
this.statBatch = new StatBatch(this.proxy);
|
||||
this.lstatBatch = new LstatBatch(this.proxy);
|
||||
this.readdirBatch = new ReaddirBatch(this.proxy);
|
||||
}
|
||||
|
||||
public access = (path: fs.PathLike, mode: number | undefined | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof mode === "function") {
|
||||
callback = mode;
|
||||
mode = undefined;
|
||||
}
|
||||
callbackify(this.proxy.access)(path, mode, callback!);
|
||||
}
|
||||
|
||||
public appendFile = (path: fs.PathLike | number, data: any, options?: fs.WriteFileOptions | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
callbackify(this.proxy.appendFile)(path, data, options, callback!);
|
||||
}
|
||||
|
||||
public chmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.chmod)(path, mode, callback!);
|
||||
}
|
||||
|
||||
public chown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.chown)(path, uid, gid, callback!);
|
||||
}
|
||||
|
||||
public close = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.close)(fd, callback!);
|
||||
}
|
||||
|
||||
public copyFile = (src: fs.PathLike, dest: fs.PathLike, flags: number | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof flags === "function") {
|
||||
callback = flags;
|
||||
}
|
||||
callbackify(this.proxy.copyFile)(
|
||||
src, dest, typeof flags !== "function" ? flags : undefined, callback!,
|
||||
);
|
||||
}
|
||||
|
||||
public createWriteStream = (path: fs.PathLike, options?: any): fs.WriteStream => {
|
||||
return new WriteStream(this.proxy.createWriteStream(path, options));
|
||||
}
|
||||
|
||||
public exists = (path: fs.PathLike, callback: (exists: boolean) => void): void => {
|
||||
this.proxy.exists(path).then((exists) => callback(exists)).catch(() => callback(false));
|
||||
}
|
||||
|
||||
public fchmod = (fd: number, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.fchmod)(fd, mode, callback!);
|
||||
}
|
||||
|
||||
public fchown = (fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.fchown)(fd, uid, gid, callback!);
|
||||
}
|
||||
|
||||
public fdatasync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.fdatasync)(fd, callback!);
|
||||
}
|
||||
|
||||
public fstat = (fd: number, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
|
||||
callbackify(this.proxy.fstat)(fd, (error, stats) => {
|
||||
callback(error, stats && new Stats(stats));
|
||||
});
|
||||
}
|
||||
|
||||
public fsync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.fsync)(fd, callback!);
|
||||
}
|
||||
|
||||
public ftruncate = (fd: number, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof len === "function") {
|
||||
callback = len;
|
||||
len = undefined;
|
||||
}
|
||||
callbackify(this.proxy.ftruncate)(fd, len, callback!);
|
||||
}
|
||||
|
||||
public futimes = (fd: number, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.futimes)(fd, atime, mtime, callback!);
|
||||
}
|
||||
|
||||
public lchmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.lchmod)(path, mode, callback!);
|
||||
}
|
||||
|
||||
public lchown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.lchown)(path, uid, gid, callback!);
|
||||
}
|
||||
|
||||
public link = (existingPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.link)(existingPath, newPath, callback!);
|
||||
}
|
||||
|
||||
public lstat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
|
||||
callbackify(this.lstatBatch.add)({ path }, (error, stats) => {
|
||||
callback(error, stats && new Stats(stats));
|
||||
});
|
||||
}
|
||||
|
||||
public mkdir = (path: fs.PathLike, mode: number | string | fs.MakeDirectoryOptions | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof mode === "function") {
|
||||
callback = mode;
|
||||
mode = undefined;
|
||||
}
|
||||
callbackify(this.proxy.mkdir)(path, mode, callback!);
|
||||
}
|
||||
|
||||
public mkdtemp = (prefix: string, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, folder: string | Buffer) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
callbackify(this.proxy.mkdtemp)(prefix, options, callback!);
|
||||
}
|
||||
|
||||
public open = (path: fs.PathLike, flags: string | number, mode: string | number | undefined | null | ((err: NodeJS.ErrnoException, fd: number) => void), callback?: (err: NodeJS.ErrnoException, fd: number) => void): void => {
|
||||
if (typeof mode === "function") {
|
||||
callback = mode;
|
||||
mode = undefined;
|
||||
}
|
||||
callbackify(this.proxy.open)(path, flags, mode, callback!);
|
||||
}
|
||||
|
||||
public read = (fd: number, buffer: Buffer, offset: number, length: number, position: number | null, callback: (err: NodeJS.ErrnoException, bytesRead: number, buffer: Buffer) => void): void => {
|
||||
this.proxy.read(fd, length, position).then((response) => {
|
||||
buffer.set(response.buffer, offset);
|
||||
callback(undefined!, response.bytesRead, response.buffer);
|
||||
}).catch((error) => {
|
||||
callback(error, undefined!, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
public readFile = (path: fs.PathLike | number, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, data: string | Buffer) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
callbackify(this.proxy.readFile)(path, options, callback!);
|
||||
}
|
||||
|
||||
public readdir = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, files: Buffer[] | fs.Dirent[] | string[]) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
callbackify(this.readdirBatch.add)({ path, options }, callback!);
|
||||
}
|
||||
|
||||
public readlink = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, linkString: string | Buffer) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
callbackify(this.proxy.readlink)(path, options, callback!);
|
||||
}
|
||||
|
||||
public realpath = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, resolvedPath: string | Buffer) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
callbackify(this.proxy.realpath)(path, options, callback!);
|
||||
}
|
||||
|
||||
public rename = (oldPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.rename)(oldPath, newPath, callback!);
|
||||
}
|
||||
|
||||
public rmdir = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.rmdir)(path, callback!);
|
||||
}
|
||||
|
||||
public stat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
|
||||
callbackify(this.statBatch.add)({ path }, (error, stats) => {
|
||||
callback(error, stats && new Stats(stats));
|
||||
});
|
||||
}
|
||||
|
||||
public symlink = (target: fs.PathLike, path: fs.PathLike, type: fs.symlink.Type | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof type === "function") {
|
||||
callback = type;
|
||||
type = undefined;
|
||||
}
|
||||
callbackify(this.proxy.symlink)(target, path, type, callback!);
|
||||
}
|
||||
|
||||
public truncate = (path: fs.PathLike, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof len === "function") {
|
||||
callback = len;
|
||||
len = undefined;
|
||||
}
|
||||
callbackify(this.proxy.truncate)(path, len, callback!);
|
||||
}
|
||||
|
||||
public unlink = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.unlink)(path, callback!);
|
||||
}
|
||||
|
||||
public utimes = (path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
|
||||
callbackify(this.proxy.utimes)(path, atime, mtime, callback!);
|
||||
}
|
||||
|
||||
public write = (fd: number, buffer: Buffer, offset: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void), length: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void), position: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void), callback?: (err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void): void => {
|
||||
if (typeof offset === "function") {
|
||||
callback = offset;
|
||||
offset = undefined;
|
||||
}
|
||||
if (typeof length === "function") {
|
||||
callback = length;
|
||||
length = undefined;
|
||||
}
|
||||
if (typeof position === "function") {
|
||||
callback = position;
|
||||
position = undefined;
|
||||
}
|
||||
this.proxy.write(fd, buffer, offset, length, position).then((r) => {
|
||||
callback!(undefined!, r.bytesWritten, r.buffer);
|
||||
}).catch((error) => {
|
||||
callback!(error, undefined!, undefined!);
|
||||
});
|
||||
}
|
||||
|
||||
public writeFile = (path: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
callbackify(this.proxy.writeFile)(path, data, options, callback!);
|
||||
}
|
||||
|
||||
public watch = (filename: fs.PathLike, options?: IEncodingOptions | ((event: string, filename: string | Buffer) => void), listener?: ((event: string, filename: string | Buffer) => void)): fs.FSWatcher => {
|
||||
if (typeof options === "function") {
|
||||
listener = options;
|
||||
options = undefined;
|
||||
}
|
||||
|
||||
const watcher = new Watcher(this.proxy.watch(filename, options));
|
||||
if (listener) {
|
||||
watcher.on("change", listener);
|
||||
}
|
||||
|
||||
return watcher;
|
||||
}
|
||||
}
|
||||
|
||||
class Stats implements fs.Stats {
|
||||
public readonly atime: Date;
|
||||
public readonly mtime: Date;
|
||||
public readonly ctime: Date;
|
||||
public readonly birthtime: Date;
|
||||
|
||||
public constructor(private readonly stats: IStats) {
|
||||
this.atime = new Date(stats.atime);
|
||||
this.mtime = new Date(stats.mtime);
|
||||
this.ctime = new Date(stats.ctime);
|
||||
this.birthtime = new Date(stats.birthtime);
|
||||
}
|
||||
|
||||
public get dev(): number { return this.stats.dev; }
|
||||
public get ino(): number { return this.stats.ino; }
|
||||
public get mode(): number { return this.stats.mode; }
|
||||
public get nlink(): number { return this.stats.nlink; }
|
||||
public get uid(): number { return this.stats.uid; }
|
||||
public get gid(): number { return this.stats.gid; }
|
||||
public get rdev(): number { return this.stats.rdev; }
|
||||
public get size(): number { return this.stats.size; }
|
||||
public get blksize(): number { return this.stats.blksize; }
|
||||
public get blocks(): number { return this.stats.blocks; }
|
||||
public get atimeMs(): number { return this.stats.atimeMs; }
|
||||
public get mtimeMs(): number { return this.stats.mtimeMs; }
|
||||
public get ctimeMs(): number { return this.stats.ctimeMs; }
|
||||
public get birthtimeMs(): number { return this.stats.birthtimeMs; }
|
||||
public isFile(): boolean { return this.stats._isFile; }
|
||||
public isDirectory(): boolean { return this.stats._isDirectory; }
|
||||
public isBlockDevice(): boolean { return this.stats._isBlockDevice; }
|
||||
public isCharacterDevice(): boolean { return this.stats._isCharacterDevice; }
|
||||
public isSymbolicLink(): boolean { return this.stats._isSymbolicLink; }
|
||||
public isFIFO(): boolean { return this.stats._isFIFO; }
|
||||
public isSocket(): boolean { return this.stats._isSocket; }
|
||||
|
||||
public toObject(): object {
|
||||
return JSON.parse(JSON.stringify(this));
|
||||
}
|
||||
}
|
||||
6
packages/protocol/src/browser/modules/index.ts
Normal file
6
packages/protocol/src/browser/modules/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./child_process";
|
||||
export * from "./fs";
|
||||
export * from "./net";
|
||||
export * from "./node-pty";
|
||||
export * from "./spdlog";
|
||||
export * from "./trash";
|
||||
284
packages/protocol/src/browser/modules/net.ts
Normal file
284
packages/protocol/src/browser/modules/net.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import * as net from "net";
|
||||
import { callbackify } from "util";
|
||||
import { ClientProxy } from "../../common/proxy";
|
||||
import { NetModuleProxy, NetServerProxy, NetSocketProxy } from "../../node/modules/net";
|
||||
import { Duplex } from "./stream";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
export class Socket extends Duplex<NetSocketProxy> implements net.Socket {
|
||||
private _connecting: boolean = false;
|
||||
private _destroyed: boolean = false;
|
||||
|
||||
public constructor(proxyPromise: Promise<NetSocketProxy> | NetSocketProxy, connecting?: boolean) {
|
||||
super(proxyPromise);
|
||||
if (connecting) {
|
||||
this._connecting = connecting;
|
||||
}
|
||||
this.on("close", () => {
|
||||
this._destroyed = true;
|
||||
this._connecting = false;
|
||||
});
|
||||
this.on("connect", () => this._connecting = false);
|
||||
}
|
||||
|
||||
public connect(options: number | string | net.SocketConnectOpts, host?: string | Function, callback?: Function): this {
|
||||
if (typeof host === "function") {
|
||||
callback = host;
|
||||
host = undefined;
|
||||
}
|
||||
this._connecting = true;
|
||||
if (callback) {
|
||||
this.on("connect", callback as () => void);
|
||||
}
|
||||
|
||||
return this.catch(this.proxy.connect(options, host));
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public end(data?: any, encoding?: string | Function, callback?: Function): void {
|
||||
if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
|
||||
callbackify(this.proxy.end)(data, encoding, () => {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public write(data: any, encoding?: string | Function, fd?: string | Function): boolean {
|
||||
let callback: undefined | Function;
|
||||
if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
if (typeof fd === "function") {
|
||||
callback = fd;
|
||||
fd = undefined;
|
||||
}
|
||||
if (typeof fd !== "undefined") {
|
||||
throw new Error("fd argument not supported");
|
||||
}
|
||||
|
||||
callbackify(this.proxy.write)(data, encoding, () => {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
return true; // Always true since we can't get this synchronously.
|
||||
}
|
||||
|
||||
public get connecting(): boolean {
|
||||
return this._connecting;
|
||||
}
|
||||
|
||||
public get destroyed(): boolean {
|
||||
return this._destroyed;
|
||||
}
|
||||
|
||||
public get bufferSize(): number {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public get bytesRead(): number {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public get bytesWritten(): number {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public get localAddress(): string {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public get localPort(): number {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public address(): net.AddressInfo | string {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public setTimeout(): this {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public setNoDelay(): this {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public setKeepAlive(): this {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public unref(): void {
|
||||
this.catch(this.proxy.unref());
|
||||
}
|
||||
|
||||
public ref(): void {
|
||||
this.catch(this.proxy.ref());
|
||||
}
|
||||
}
|
||||
|
||||
export class Server extends ClientProxy<NetServerProxy> implements net.Server {
|
||||
private socketId = 0;
|
||||
private readonly sockets = new Map<number, net.Socket>();
|
||||
private _listening: boolean = false;
|
||||
|
||||
public constructor(proxyPromise: Promise<NetServerProxy> | NetServerProxy) {
|
||||
super(proxyPromise);
|
||||
|
||||
this.catch(this.proxy.onConnection((socketProxy) => {
|
||||
const socket = new Socket(socketProxy);
|
||||
const socketId = this.socketId++;
|
||||
this.sockets.set(socketId, socket);
|
||||
socket.on("error", () => this.sockets.delete(socketId));
|
||||
socket.on("close", () => this.sockets.delete(socketId));
|
||||
this.emit("connection", socket);
|
||||
}));
|
||||
|
||||
this.on("listening", () => this._listening = true);
|
||||
this.on("error", () => this._listening = false);
|
||||
this.on("close", () => this._listening = false);
|
||||
}
|
||||
|
||||
public listen(handle?: net.ListenOptions | number | string, hostname?: string | number | Function, backlog?: number | Function, callback?: Function): this {
|
||||
if (typeof hostname === "function") {
|
||||
callback = hostname;
|
||||
hostname = undefined;
|
||||
}
|
||||
if (typeof backlog === "function") {
|
||||
callback = backlog;
|
||||
backlog = undefined;
|
||||
}
|
||||
if (callback) {
|
||||
this.on("listening", callback as () => void);
|
||||
}
|
||||
|
||||
return this.catch(this.proxy.listen(handle, hostname, backlog));
|
||||
}
|
||||
|
||||
public get connections(): number {
|
||||
return this.sockets.size;
|
||||
}
|
||||
|
||||
public get listening(): boolean {
|
||||
return this._listening;
|
||||
}
|
||||
|
||||
public get maxConnections(): number {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public address(): net.AddressInfo | string {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public close(callback?: () => void): this {
|
||||
this._listening = false;
|
||||
if (callback) {
|
||||
this.on("close", callback);
|
||||
}
|
||||
|
||||
return this.catch(this.proxy.close());
|
||||
}
|
||||
|
||||
public ref(): this {
|
||||
return this.catch(this.proxy.ref());
|
||||
}
|
||||
|
||||
public unref(): this {
|
||||
return this.catch(this.proxy.unref());
|
||||
}
|
||||
|
||||
public getConnections(cb: (error: Error | null, count: number) => void): void {
|
||||
cb(null, this.sockets.size);
|
||||
}
|
||||
|
||||
protected handleDisconnect(): void {
|
||||
this.emit("close");
|
||||
}
|
||||
}
|
||||
|
||||
type NodeNet = typeof net;
|
||||
|
||||
export class NetModule implements NodeNet {
|
||||
public readonly Socket: typeof net.Socket;
|
||||
public readonly Server: typeof net.Server;
|
||||
|
||||
public constructor(private readonly proxy: NetModuleProxy) {
|
||||
// @ts-ignore this is because Socket is missing things from the Stream
|
||||
// namespace but I'm unsure how best to provide them (finished,
|
||||
// finished.__promisify__, pipeline, and some others) or if it even matters.
|
||||
this.Socket = class extends Socket {
|
||||
public constructor(options?: net.SocketConstructorOpts) {
|
||||
super(proxy.createSocket(options));
|
||||
}
|
||||
};
|
||||
|
||||
this.Server = class extends Server {
|
||||
public constructor(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: Socket) => void), listener?: (socket: Socket) => void) {
|
||||
super(proxy.createServer(typeof options !== "function" ? options : undefined));
|
||||
if (typeof options === "function") {
|
||||
listener = options;
|
||||
}
|
||||
if (listener) {
|
||||
this.on("connection", listener);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public createConnection = (target: string | number | net.NetConnectOpts, host?: string | Function, callback?: Function): net.Socket => {
|
||||
if (typeof host === "function") {
|
||||
callback = host;
|
||||
host = undefined;
|
||||
}
|
||||
|
||||
const socket = new Socket(this.proxy.createConnection(target, host), true);
|
||||
if (callback) {
|
||||
socket.on("connect", callback as () => void);
|
||||
}
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
public createServer = (
|
||||
options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: net.Socket) => void),
|
||||
callback?: (socket: net.Socket) => void,
|
||||
): net.Server => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
|
||||
const server = new Server(this.proxy.createServer(options));
|
||||
if (callback) {
|
||||
server.on("connection", callback);
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
public connect = (): net.Socket => {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public isIP = (_input: string): number => {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public isIPv4 = (_input: string): boolean => {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public isIPv6 = (_input: string): boolean => {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
}
|
||||
62
packages/protocol/src/browser/modules/node-pty.ts
Normal file
62
packages/protocol/src/browser/modules/node-pty.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as pty from "node-pty";
|
||||
import { ClientProxy } from "../../common/proxy";
|
||||
import { NodePtyModuleProxy, NodePtyProcessProxy } from "../../node/modules/node-pty";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
export class NodePtyProcess extends ClientProxy<NodePtyProcessProxy> implements pty.IPty {
|
||||
private _pid = -1;
|
||||
private _process = "";
|
||||
|
||||
public constructor(
|
||||
private readonly moduleProxy: NodePtyModuleProxy,
|
||||
private readonly file: string,
|
||||
private readonly args: string[] | string,
|
||||
private readonly options: pty.IPtyForkOptions,
|
||||
) {
|
||||
super(moduleProxy.spawn(file, args, options));
|
||||
this.on("process", (process) => this._process = process);
|
||||
}
|
||||
|
||||
protected initialize(proxyPromise: Promise<NodePtyProcessProxy>): void {
|
||||
super.initialize(proxyPromise);
|
||||
this.catch(this.proxy.getPid().then((p) => this._pid = p));
|
||||
this.catch(this.proxy.getProcess().then((p) => this._process = p));
|
||||
}
|
||||
|
||||
public get pid(): number {
|
||||
return this._pid;
|
||||
}
|
||||
|
||||
public get process(): string {
|
||||
return this._process;
|
||||
}
|
||||
|
||||
public resize(columns: number, rows: number): void {
|
||||
this.catch(this.proxy.resize(columns, rows));
|
||||
}
|
||||
|
||||
public write(data: string): void {
|
||||
this.catch(this.proxy.write(data));
|
||||
}
|
||||
|
||||
public kill(signal?: string): void {
|
||||
this.catch(this.proxy.kill(signal));
|
||||
}
|
||||
|
||||
protected handleDisconnect(): void {
|
||||
this._process += " (disconnected)";
|
||||
this.emit("data", "\r\n\nLost connection...\r\n\n");
|
||||
this.initialize(this.moduleProxy.spawn(this.file, this.args, this.options));
|
||||
}
|
||||
}
|
||||
|
||||
type NodePty = typeof pty;
|
||||
|
||||
export class NodePtyModule implements NodePty {
|
||||
public constructor(private readonly proxy: NodePtyModuleProxy) {}
|
||||
|
||||
public spawn = (file: string, args: string[] | string, options: pty.IPtyForkOptions): pty.IPty => {
|
||||
return new NodePtyProcess(this.proxy, file, args, options);
|
||||
}
|
||||
}
|
||||
48
packages/protocol/src/browser/modules/spdlog.ts
Normal file
48
packages/protocol/src/browser/modules/spdlog.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as spdlog from "spdlog";
|
||||
import { ClientProxy } from "../../common/proxy";
|
||||
import { RotatingLoggerProxy, SpdlogModuleProxy } from "../../node/modules/spdlog";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
class RotatingLogger extends ClientProxy<RotatingLoggerProxy> implements spdlog.RotatingLogger {
|
||||
public constructor(
|
||||
private readonly moduleProxy: SpdlogModuleProxy,
|
||||
private readonly name: string,
|
||||
private readonly filename: string,
|
||||
private readonly filesize: number,
|
||||
private readonly filecount: number,
|
||||
) {
|
||||
super(moduleProxy.createLogger(name, filename, filesize, filecount));
|
||||
}
|
||||
|
||||
public trace (message: string): void { this.catch(this.proxy.trace(message)); }
|
||||
public debug (message: string): void { this.catch(this.proxy.debug(message)); }
|
||||
public info (message: string): void { this.catch(this.proxy.info(message)); }
|
||||
public warn (message: string): void { this.catch(this.proxy.warn(message)); }
|
||||
public error (message: string): void { this.catch(this.proxy.error(message)); }
|
||||
public critical (message: string): void { this.catch(this.proxy.critical(message)); }
|
||||
public setLevel (level: number): void { this.catch(this.proxy.setLevel(level)); }
|
||||
public clearFormatters (): void { this.catch(this.proxy.clearFormatters()); }
|
||||
public flush (): void { this.catch(this.proxy.flush()); }
|
||||
public drop (): void { this.catch(this.proxy.drop()); }
|
||||
|
||||
protected handleDisconnect(): void {
|
||||
this.initialize(this.moduleProxy.createLogger(this.name, this.filename, this.filesize, this.filecount));
|
||||
}
|
||||
}
|
||||
|
||||
export class SpdlogModule {
|
||||
public readonly RotatingLogger: typeof spdlog.RotatingLogger;
|
||||
|
||||
public constructor(private readonly proxy: SpdlogModuleProxy) {
|
||||
this.RotatingLogger = class extends RotatingLogger {
|
||||
public constructor(name: string, filename: string, filesize: number, filecount: number) {
|
||||
super(proxy, name, filename, filesize, filecount);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public setAsyncMode = (bufferSize: number, flushInterval: number): Promise<void> => {
|
||||
return this.proxy.setAsyncMode(bufferSize, flushInterval);
|
||||
}
|
||||
}
|
||||
244
packages/protocol/src/browser/modules/stream.ts
Normal file
244
packages/protocol/src/browser/modules/stream.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import * as stream from "stream";
|
||||
import { callbackify } from "util";
|
||||
import { ClientProxy } from "../../common/proxy";
|
||||
import { DuplexProxy, IReadableProxy, WritableProxy } from "../../node/modules/stream";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
export class Writable<T extends WritableProxy = WritableProxy> extends ClientProxy<T> implements stream.Writable {
|
||||
public get writable(): boolean {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public get writableHighWaterMark(): number {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public get writableLength(): number {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public _write(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public _destroy(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public _final(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public pipe<T>(): T {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public cork(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public uncork(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.catch(this.proxy.destroy());
|
||||
}
|
||||
|
||||
public setDefaultEncoding(encoding: string): this {
|
||||
return this.catch(this.proxy.setDefaultEncoding(encoding));
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
|
||||
if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
callbackify(this.proxy.write)(chunk, encoding, (error) => {
|
||||
if (callback) {
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
|
||||
return true; // Always true since we can't get this synchronously.
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public end(data?: any | (() => void), encoding?: string | (() => void), callback?: (() => void)): void {
|
||||
if (typeof data === "function") {
|
||||
callback = data;
|
||||
data = undefined;
|
||||
}
|
||||
if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
callbackify(this.proxy.end)(data, encoding, () => {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected handleDisconnect(): void {
|
||||
this.emit("close");
|
||||
this.emit("finish");
|
||||
}
|
||||
}
|
||||
|
||||
export class Readable<T extends IReadableProxy = IReadableProxy> extends ClientProxy<T> implements stream.Readable {
|
||||
public get readable(): boolean {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public get readableHighWaterMark(): number {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public get readableLength(): number {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public _read(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public read(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public _destroy(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public unpipe(): this {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public pause(): this {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public resume(): this {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public wrap(): this {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public push(): boolean {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public unshift(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public pipe<T>(): T {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public [Symbol.asyncIterator](): AsyncIterableIterator<any> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.catch(this.proxy.destroy());
|
||||
}
|
||||
|
||||
public setEncoding(encoding: string): this {
|
||||
return this.catch(this.proxy.setEncoding(encoding));
|
||||
}
|
||||
|
||||
protected handleDisconnect(): void {
|
||||
this.emit("close");
|
||||
this.emit("end");
|
||||
}
|
||||
}
|
||||
|
||||
export class Duplex<T extends DuplexProxy = DuplexProxy> extends Writable<T> implements stream.Duplex, stream.Readable {
|
||||
private readonly _readable: Readable;
|
||||
|
||||
public constructor(proxyPromise: Promise<T> | T) {
|
||||
super(proxyPromise);
|
||||
this._readable = new Readable(proxyPromise, false);
|
||||
}
|
||||
|
||||
public get readable(): boolean {
|
||||
return this._readable.readable;
|
||||
}
|
||||
|
||||
public get readableHighWaterMark(): number {
|
||||
return this._readable.readableHighWaterMark;
|
||||
}
|
||||
|
||||
public get readableLength(): number {
|
||||
return this._readable.readableLength;
|
||||
}
|
||||
|
||||
public _read(): void {
|
||||
this._readable._read();
|
||||
}
|
||||
|
||||
public read(): void {
|
||||
this._readable.read();
|
||||
}
|
||||
|
||||
public unpipe(): this {
|
||||
this._readable.unpipe();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public pause(): this {
|
||||
this._readable.unpipe();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public resume(): this {
|
||||
this._readable.resume();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
return this._readable.isPaused();
|
||||
}
|
||||
|
||||
public wrap(): this {
|
||||
this._readable.wrap();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public push(): boolean {
|
||||
return this._readable.push();
|
||||
}
|
||||
|
||||
public unshift(): void {
|
||||
this._readable.unshift();
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public [Symbol.asyncIterator](): AsyncIterableIterator<any> {
|
||||
return this._readable[Symbol.asyncIterator]();
|
||||
}
|
||||
|
||||
public setEncoding(encoding: string): this {
|
||||
return this.catch(this.proxy.setEncoding(encoding));
|
||||
}
|
||||
|
||||
protected handleDisconnect(): void {
|
||||
super.handleDisconnect();
|
||||
this.emit("end");
|
||||
}
|
||||
}
|
||||
12
packages/protocol/src/browser/modules/trash.ts
Normal file
12
packages/protocol/src/browser/modules/trash.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as trash from "trash";
|
||||
import { TrashModuleProxy } from "../../node/modules/trash";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
export class TrashModule {
|
||||
public constructor(private readonly proxy: TrashModuleProxy) {}
|
||||
|
||||
public trash = (path: string, options?: trash.Options): Promise<void> => {
|
||||
return this.proxy.trash(path, options);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ export interface SendableConnection {
|
||||
export interface ReadWriteConnection extends SendableConnection {
|
||||
onMessage(cb: (data: Uint8Array | Buffer) => void): void;
|
||||
onClose(cb: () => void): void;
|
||||
onDown(cb: () => void): void;
|
||||
onUp(cb: () => void): void;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
@@ -21,6 +23,7 @@ export interface InitData {
|
||||
readonly homeDirectory: string;
|
||||
readonly tmpDirectory: string;
|
||||
readonly shell: string;
|
||||
readonly extensionsDirectory: string;
|
||||
readonly builtInExtensionsDirectory: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
/// <reference path="../../../../lib/vscode/src/typings/spdlog.d.ts" />
|
||||
/// <reference path="../../node_modules/node-pty-prebuilt/typings/node-pty.d.ts" />
|
||||
import { ChildProcess, SpawnOptions, ForkOptions } from "child_process";
|
||||
import { EventEmitter } from "events";
|
||||
import { Socket } from "net";
|
||||
import { Duplex, Readable, Writable } from "stream";
|
||||
import { IDisposable } from "@coder/disposable";
|
||||
import { logger } from "@coder/logger";
|
||||
|
||||
// tslint:disable no-any
|
||||
|
||||
export type ForkProvider = (modulePath: string, args: string[], options: ForkOptions) => ChildProcess;
|
||||
|
||||
export interface Disposer extends IDisposable {
|
||||
onDidDispose: (cb: () => void) => void;
|
||||
}
|
||||
|
||||
interface ActiveEvalEmitter {
|
||||
removeAllListeners(event?: string): void;
|
||||
emit(event: string, ...args: any[]): void;
|
||||
on(event: string, cb: (...args: any[]) => void): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* For any non-external modules that are not built in, we need to require and
|
||||
* access them server-side. A require on the client-side won't work since that
|
||||
* code won't exist on the server (and bloat the client with an unused import),
|
||||
* and we can't manually import on the server-side and then call
|
||||
* `__webpack_require__` on the client-side because Webpack stores modules by
|
||||
* their paths which would require us to hard-code the path.
|
||||
*/
|
||||
export interface Modules {
|
||||
pty: typeof import("node-pty");
|
||||
spdlog: typeof import("spdlog");
|
||||
trash: typeof import("trash");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for server-side evaluations.
|
||||
*/
|
||||
export class EvalHelper {
|
||||
public constructor(public modules: Modules) {}
|
||||
|
||||
/**
|
||||
* Some spawn code tries to preserve the env (the debug adapter for instance)
|
||||
* but the env is mostly blank (since we're in the browser), so we'll just
|
||||
* always preserve the main process.env here, otherwise it won't have access
|
||||
* to PATH, etc.
|
||||
* TODO: An alternative solution would be to send the env to the browser?
|
||||
*/
|
||||
public preserveEnv(options: SpawnOptions | ForkOptions): void {
|
||||
if (options && options.env) {
|
||||
options.env = { ...process.env, ...options.env };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for client-side active evaluations.
|
||||
*/
|
||||
export class ActiveEvalHelper implements ActiveEvalEmitter {
|
||||
public constructor(private readonly emitter: ActiveEvalEmitter) {}
|
||||
|
||||
public removeAllListeners(event?: string): void {
|
||||
this.emitter.removeAllListeners(event);
|
||||
}
|
||||
|
||||
public emit(event: string, ...args: any[]): void {
|
||||
this.emitter.emit(event, ...args);
|
||||
}
|
||||
|
||||
public on(event: string, cb: (...args: any[]) => void): void {
|
||||
this.emitter.on(event, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new helper to make unique events for an item.
|
||||
*/
|
||||
public createUnique(id: number | "stdout" | "stderr" | "stdin"): ActiveEvalHelper {
|
||||
return new ActiveEvalHelper(this.createUniqueEmitter(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the evaluation emitter to make unique events for an item to prevent
|
||||
* conflicts when it shares that emitter with other items.
|
||||
*/
|
||||
protected createUniqueEmitter(id: number | "stdout" | "stderr" | "stdin"): ActiveEvalEmitter {
|
||||
let events = <string[]>[];
|
||||
|
||||
return {
|
||||
removeAllListeners: (event?: string): void => {
|
||||
if (!event) {
|
||||
events.forEach((e) => this.removeAllListeners(e));
|
||||
events = [];
|
||||
} else {
|
||||
const index = events.indexOf(event);
|
||||
if (index !== -1) {
|
||||
events.splice(index, 1);
|
||||
this.removeAllListeners(`${event}:${id}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
emit: (event: string, ...args: any[]): void => {
|
||||
this.emit(`${event}:${id}`, ...args);
|
||||
},
|
||||
on: (event: string, cb: (...args: any[]) => void): void => {
|
||||
if (!events.includes(event)) {
|
||||
events.push(event);
|
||||
}
|
||||
this.on(`${event}:${id}`, cb);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for server-side active evaluations.
|
||||
*/
|
||||
export class ServerActiveEvalHelper extends ActiveEvalHelper implements EvalHelper {
|
||||
private readonly evalHelper: EvalHelper;
|
||||
|
||||
public constructor(public modules: Modules, emitter: ActiveEvalEmitter, public readonly fork: ForkProvider) {
|
||||
super(emitter);
|
||||
this.evalHelper = new EvalHelper(modules);
|
||||
}
|
||||
|
||||
public preserveEnv(options: SpawnOptions | ForkOptions): void {
|
||||
this.evalHelper.preserveEnv(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a callback ID, return a function that emits the callback event
|
||||
* on the active evaluation with that ID and all arguments passed to it.
|
||||
* Otherwise, return undefined.
|
||||
*/
|
||||
public maybeCallback(callbackId?: number): ((...args: any[]) => void) | undefined {
|
||||
return typeof callbackId !== "undefined" ? (...args: any[]): void => {
|
||||
this.emit("callback", callbackId, ...args);
|
||||
} : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a socket to an active evaluation and returns a disposer.
|
||||
*/
|
||||
public bindSocket(socket: Socket): Disposer {
|
||||
socket.on("connect", () => this.emit("connect"));
|
||||
socket.on("lookup", (error, address, family, host) => this.emit("lookup", error, address, family, host));
|
||||
socket.on("timeout", () => this.emit("timeout"));
|
||||
|
||||
this.on("connect", (options, callbackId) => socket.connect(options, this.maybeCallback(callbackId)));
|
||||
this.on("ref", () => socket.ref());
|
||||
this.on("setKeepAlive", (enable, initialDelay) => socket.setKeepAlive(enable, initialDelay));
|
||||
this.on("setNoDelay", (noDelay) => socket.setNoDelay(noDelay));
|
||||
this.on("setTimeout", (timeout, callbackId) => socket.setTimeout(timeout, this.maybeCallback(callbackId)));
|
||||
this.on("unref", () => socket.unref());
|
||||
|
||||
this.bindReadable(socket);
|
||||
this.bindWritable(socket);
|
||||
|
||||
return {
|
||||
onDidDispose: (cb): Socket => socket.on("close", cb),
|
||||
dispose: (): void => {
|
||||
socket.removeAllListeners();
|
||||
socket.end();
|
||||
socket.destroy();
|
||||
socket.unref();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a writable stream to the active evaluation.
|
||||
*/
|
||||
public bindWritable(writable: Writable | Duplex): void {
|
||||
if (!((writable as Readable).read)) { // To avoid binding twice.
|
||||
writable.on("close", () => this.emit("close"));
|
||||
writable.on("error", (error) => this.emit("error", error));
|
||||
|
||||
this.on("destroy", () => writable.destroy());
|
||||
}
|
||||
|
||||
writable.on("drain", () => this.emit("drain"));
|
||||
writable.on("finish", () => this.emit("finish"));
|
||||
writable.on("pipe", () => this.emit("pipe"));
|
||||
writable.on("unpipe", () => this.emit("unpipe"));
|
||||
|
||||
this.on("cork", () => writable.cork());
|
||||
this.on("end", (chunk, encoding, callbackId) => writable.end(chunk, encoding, this.maybeCallback(callbackId)));
|
||||
this.on("setDefaultEncoding", (encoding) => writable.setDefaultEncoding(encoding));
|
||||
this.on("uncork", () => writable.uncork());
|
||||
// Sockets can pass an fd instead of a callback but streams cannot.
|
||||
this.on("write", (chunk, encoding, fd, callbackId) => writable.write(chunk, encoding, this.maybeCallback(callbackId) || fd));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a readable stream to the active evaluation.
|
||||
*/
|
||||
public bindReadable(readable: Readable): void {
|
||||
// Streams don't have an argument on close but sockets do.
|
||||
readable.on("close", (...args: any[]) => this.emit("close", ...args));
|
||||
readable.on("data", (data) => this.emit("data", data));
|
||||
readable.on("end", () => this.emit("end"));
|
||||
readable.on("error", (error) => this.emit("error", error));
|
||||
readable.on("readable", () => this.emit("readable"));
|
||||
|
||||
this.on("destroy", () => readable.destroy());
|
||||
this.on("pause", () => readable.pause());
|
||||
this.on("push", (chunk, encoding) => readable.push(chunk, encoding));
|
||||
this.on("resume", () => readable.resume());
|
||||
this.on("setEncoding", (encoding) => readable.setEncoding(encoding));
|
||||
this.on("unshift", (chunk) => readable.unshift(chunk));
|
||||
}
|
||||
|
||||
public createUnique(id: number | "stdout" | "stderr" | "stdin"): ServerActiveEvalHelper {
|
||||
return new ServerActiveEvalHelper(this.modules, this.createUniqueEmitter(id), this.fork);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event emitter that can store callbacks with IDs in a map so we can pass
|
||||
* them back and forth through an active evaluation using those IDs.
|
||||
*/
|
||||
export class CallbackEmitter extends EventEmitter {
|
||||
private _ae: ActiveEvalHelper | undefined;
|
||||
private callbackId = 0;
|
||||
private readonly callbacks = new Map<number, Function>();
|
||||
|
||||
public constructor(ae?: ActiveEvalHelper) {
|
||||
super();
|
||||
if (ae) {
|
||||
this.ae = ae;
|
||||
}
|
||||
}
|
||||
|
||||
protected get ae(): ActiveEvalHelper {
|
||||
if (!this._ae) {
|
||||
throw new Error("trying to access active evaluation before it has been set");
|
||||
}
|
||||
|
||||
return this._ae;
|
||||
}
|
||||
|
||||
protected set ae(ae: ActiveEvalHelper) {
|
||||
if (this._ae) {
|
||||
throw new Error("cannot override active evaluation");
|
||||
}
|
||||
this._ae = ae;
|
||||
this.ae.on("callback", (callbackId, ...args: any[]) => this.runCallback(callbackId, ...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the callback and return and ID referencing its location in the map.
|
||||
*/
|
||||
protected storeCallback(callback?: Function): number | undefined {
|
||||
if (!callback) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const callbackId = this.callbackId++;
|
||||
this.callbacks.set(callbackId, callback);
|
||||
|
||||
return callbackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the function with the specified ID and delete it from the map.
|
||||
* If the ID is undefined or doesn't exist, nothing happens.
|
||||
*/
|
||||
private runCallback(callbackId?: number, ...args: any[]): void {
|
||||
const callback = typeof callbackId !== "undefined" && this.callbacks.get(callbackId);
|
||||
if (callback && typeof callbackId !== "undefined") {
|
||||
this.callbacks.delete(callbackId);
|
||||
callback(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A writable stream over an active evaluation.
|
||||
*/
|
||||
export class ActiveEvalWritable extends CallbackEmitter implements Writable {
|
||||
public constructor(ae: ActiveEvalHelper) {
|
||||
super(ae);
|
||||
// Streams don't have an argument on close but sockets do.
|
||||
this.ae.on("close", (...args: any[]) => this.emit("close", ...args));
|
||||
this.ae.on("drain", () => this.emit("drain"));
|
||||
this.ae.on("error", (error) => this.emit("error", error));
|
||||
this.ae.on("finish", () => this.emit("finish"));
|
||||
this.ae.on("pipe", () => logger.warn("pipe is not supported"));
|
||||
this.ae.on("unpipe", () => logger.warn("unpipe is not supported"));
|
||||
}
|
||||
|
||||
public get writable(): boolean { throw new Error("not implemented"); }
|
||||
public get writableHighWaterMark(): number { throw new Error("not implemented"); }
|
||||
public get writableLength(): number { throw new Error("not implemented"); }
|
||||
public _write(): void { throw new Error("not implemented"); }
|
||||
public _destroy(): void { throw new Error("not implemented"); }
|
||||
public _final(): void { throw new Error("not implemented"); }
|
||||
public pipe<T>(): T { throw new Error("not implemented"); }
|
||||
|
||||
public cork(): void { this.ae.emit("cork"); }
|
||||
public destroy(): void { this.ae.emit("destroy"); }
|
||||
public setDefaultEncoding(encoding: string): this {
|
||||
this.ae.emit("setDefaultEncoding", encoding);
|
||||
|
||||
return this;
|
||||
}
|
||||
public uncork(): void { this.ae.emit("uncork"); }
|
||||
|
||||
public write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
|
||||
if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
|
||||
// Sockets can pass an fd instead of a callback but streams cannot..
|
||||
this.ae.emit("write", chunk, encoding, undefined, this.storeCallback(callback));
|
||||
|
||||
// Always true since we can't get this synchronously.
|
||||
return true;
|
||||
}
|
||||
|
||||
public end(data?: any, encoding?: string | Function, callback?: Function): void {
|
||||
if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
this.ae.emit("end", data, encoding, this.storeCallback(callback));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A readable stream over an active evaluation.
|
||||
*/
|
||||
export class ActiveEvalReadable extends CallbackEmitter implements Readable {
|
||||
public constructor(ae: ActiveEvalHelper) {
|
||||
super(ae);
|
||||
this.ae.on("close", () => this.emit("close"));
|
||||
this.ae.on("data", (data) => this.emit("data", data));
|
||||
this.ae.on("end", () => this.emit("end"));
|
||||
this.ae.on("error", (error) => this.emit("error", error));
|
||||
this.ae.on("readable", () => this.emit("readable"));
|
||||
}
|
||||
|
||||
public get readable(): boolean { throw new Error("not implemented"); }
|
||||
public get readableHighWaterMark(): number { throw new Error("not implemented"); }
|
||||
public get readableLength(): number { throw new Error("not implemented"); }
|
||||
public _read(): void { throw new Error("not implemented"); }
|
||||
public read(): any { throw new Error("not implemented"); }
|
||||
public isPaused(): boolean { throw new Error("not implemented"); }
|
||||
public pipe<T>(): T { throw new Error("not implemented"); }
|
||||
public unpipe(): this { throw new Error("not implemented"); }
|
||||
public unshift(): this { throw new Error("not implemented"); }
|
||||
public wrap(): this { throw new Error("not implemented"); }
|
||||
public push(): boolean { throw new Error("not implemented"); }
|
||||
public _destroy(): void { throw new Error("not implemented"); }
|
||||
public [Symbol.asyncIterator](): AsyncIterableIterator<any> { throw new Error("not implemented"); }
|
||||
|
||||
public destroy(): void { this.ae.emit("destroy"); }
|
||||
public pause(): this { return this.emitReturnThis("pause"); }
|
||||
public resume(): this { return this.emitReturnThis("resume"); }
|
||||
public setEncoding(encoding?: string): this { return this.emitReturnThis("setEncoding", encoding); }
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
protected emitReturnThis(event: string, ...args: any[]): this {
|
||||
this.ae.emit(event, ...args);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An duplex stream over an active evaluation.
|
||||
*/
|
||||
export class ActiveEvalDuplex extends ActiveEvalReadable implements Duplex {
|
||||
// Some unfortunate duplication here since we can't have multiple extends.
|
||||
public constructor(ae: ActiveEvalHelper) {
|
||||
super(ae);
|
||||
this.ae.on("drain", () => this.emit("drain"));
|
||||
this.ae.on("finish", () => this.emit("finish"));
|
||||
this.ae.on("pipe", () => logger.warn("pipe is not supported"));
|
||||
this.ae.on("unpipe", () => logger.warn("unpipe is not supported"));
|
||||
}
|
||||
|
||||
public get writable(): boolean { throw new Error("not implemented"); }
|
||||
public get writableHighWaterMark(): number { throw new Error("not implemented"); }
|
||||
public get writableLength(): number { throw new Error("not implemented"); }
|
||||
public _write(): void { throw new Error("not implemented"); }
|
||||
public _destroy(): void { throw new Error("not implemented"); }
|
||||
public _final(): void { throw new Error("not implemented"); }
|
||||
public pipe<T>(): T { throw new Error("not implemented"); }
|
||||
|
||||
public cork(): void { this.ae.emit("cork"); }
|
||||
public destroy(): void { this.ae.emit("destroy"); }
|
||||
public setDefaultEncoding(encoding: string): this {
|
||||
this.ae.emit("setDefaultEncoding", encoding);
|
||||
|
||||
return this;
|
||||
}
|
||||
public uncork(): void { this.ae.emit("uncork"); }
|
||||
|
||||
public write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
|
||||
if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
|
||||
// Sockets can pass an fd instead of a callback but streams cannot..
|
||||
this.ae.emit("write", chunk, encoding, undefined, this.storeCallback(callback));
|
||||
|
||||
// Always true since we can't get this synchronously.
|
||||
return true;
|
||||
}
|
||||
|
||||
public end(data?: any, encoding?: string | Function, callback?: Function): void {
|
||||
if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
this.ae.emit("end", data, encoding, this.storeCallback(callback));
|
||||
}
|
||||
}
|
||||
211
packages/protocol/src/common/proxy.ts
Normal file
211
packages/protocol/src/common/proxy.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { EventEmitter } from "events";
|
||||
import { isPromise } from "./util";
|
||||
|
||||
// tslint:disable no-any
|
||||
|
||||
/**
|
||||
* Allow using a proxy like it's returned synchronously. This only works because
|
||||
* all proxy methods return promises.
|
||||
*/
|
||||
const unpromisify = <T extends ServerProxy>(proxyPromise: Promise<T>): T => {
|
||||
return new Proxy({}, {
|
||||
get: (target: any, name: string): any => {
|
||||
if (typeof target[name] === "undefined") {
|
||||
target[name] = async (...args: any[]): Promise<any> => {
|
||||
const proxy = await proxyPromise;
|
||||
|
||||
return proxy ? (proxy as any)[name](...args) : undefined;
|
||||
};
|
||||
}
|
||||
|
||||
return target[name];
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Client-side emitter that just forwards proxy events to its own emitter.
|
||||
* It also turns a promisified proxy into a non-promisified proxy so we don't
|
||||
* need a bunch of `then` calls everywhere.
|
||||
*/
|
||||
export abstract class ClientProxy<T extends ServerProxy> extends EventEmitter {
|
||||
private _proxy: T | undefined;
|
||||
|
||||
/**
|
||||
* You can specify not to bind events in order to avoid emitting twice for
|
||||
* duplex streams.
|
||||
*/
|
||||
public constructor(
|
||||
proxyPromise: Promise<T> | T,
|
||||
private readonly bindEvents: boolean = true,
|
||||
) {
|
||||
super();
|
||||
this.initialize(proxyPromise);
|
||||
if (this.bindEvents) {
|
||||
this.on("disconnected", (error) => {
|
||||
try {
|
||||
this.emit("error", error);
|
||||
} catch (error) {
|
||||
// If nothing is listening, EventEmitter will throw an error.
|
||||
}
|
||||
this.handleDisconnect();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected get proxy(): T {
|
||||
if (!this._proxy) {
|
||||
throw new Error("not initialized");
|
||||
}
|
||||
|
||||
return this._proxy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the proxy by unpromisifying if necessary and binding to its
|
||||
* events.
|
||||
*/
|
||||
protected initialize(proxyPromise: Promise<T> | T): void {
|
||||
this._proxy = isPromise(proxyPromise) ? unpromisify(proxyPromise) : proxyPromise;
|
||||
if (this.bindEvents) {
|
||||
this.catch(this.proxy.onEvent((event, ...args): void => {
|
||||
this.emit(event, ...args);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform necessary cleanup on disconnect (or reconnect).
|
||||
*/
|
||||
protected abstract handleDisconnect(): void;
|
||||
|
||||
/**
|
||||
* Emit an error event if the promise errors.
|
||||
*/
|
||||
protected catch(promise?: Promise<any>): this {
|
||||
if (promise) {
|
||||
promise.catch((e) => this.emit("error", e));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy to the actual instance on the server. Every method must only accept
|
||||
* serializable arguments and must return promises with serializable values. If
|
||||
* a proxy itself has proxies on creation (like how ChildProcess has stdin),
|
||||
* then it should return all of those at once, otherwise you will miss events
|
||||
* from those child proxies and fail to dispose them properly.
|
||||
*/
|
||||
export interface ServerProxy {
|
||||
/**
|
||||
* Dispose the proxy.
|
||||
*/
|
||||
dispose(): Promise<void>;
|
||||
|
||||
/**
|
||||
* This is used instead of an event to force it to be implemented since there
|
||||
* would be no guarantee the implementation would remember to emit the event.
|
||||
*/
|
||||
onDone(cb: () => void): Promise<void>;
|
||||
|
||||
/**
|
||||
* Listen to all possible events. On the client, this is to reduce boilerplate
|
||||
* that would just be a bunch of error-prone forwarding of each individual
|
||||
* event from the proxy to its own emitter. It also fixes a timing issue
|
||||
* because we just always send all events from the server, so we never miss
|
||||
* any due to listening too late.
|
||||
*/
|
||||
// tslint:disable-next-line no-any
|
||||
onEvent(cb: (event: string, ...args: any[]) => void): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported top-level module proxies.
|
||||
*/
|
||||
export enum Module {
|
||||
Fs = "fs",
|
||||
ChildProcess = "child_process",
|
||||
Net = "net",
|
||||
Spdlog = "spdlog",
|
||||
NodePty = "node-pty",
|
||||
Trash = "trash",
|
||||
}
|
||||
|
||||
interface BatchItem<T, A> {
|
||||
args: A;
|
||||
resolve: (t: T) => void;
|
||||
reject: (e: Error) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch remote calls.
|
||||
*/
|
||||
export abstract class Batch<T, A> {
|
||||
private idleTimeout: number | NodeJS.Timer | undefined;
|
||||
private maxTimeout: number | NodeJS.Timer | undefined;
|
||||
private batch = <BatchItem<T, A>[]>[];
|
||||
|
||||
public constructor(
|
||||
/**
|
||||
* Flush after reaching this amount of time.
|
||||
*/
|
||||
private readonly maxTime: number = 1000,
|
||||
/**
|
||||
* Flush after reaching this count.
|
||||
*/
|
||||
private readonly maxCount: number = 100,
|
||||
/**
|
||||
* Flush after not receiving more requests for this amount of time.
|
||||
*/
|
||||
private readonly idleTime: number = 100,
|
||||
) {}
|
||||
|
||||
public add = (args: A): Promise<T> => {
|
||||
return new Promise((resolve, reject): void => {
|
||||
this.batch.push({
|
||||
args,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
if (this.batch.length >= this.maxCount) {
|
||||
this.flush();
|
||||
} else {
|
||||
clearTimeout(this.idleTimeout as any);
|
||||
this.idleTimeout = setTimeout(this.flush, this.idleTime);
|
||||
if (typeof this.maxTimeout === "undefined") {
|
||||
this.maxTimeout = setTimeout(this.flush, this.maxTime);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform remote call for a batch.
|
||||
*/
|
||||
protected abstract remoteCall(batch: A[]): Promise<(T | Error)[]>;
|
||||
|
||||
/**
|
||||
* Flush out the current batch.
|
||||
*/
|
||||
private readonly flush = (): void => {
|
||||
clearTimeout(this.idleTimeout as any);
|
||||
clearTimeout(this.maxTimeout as any);
|
||||
this.maxTimeout = undefined;
|
||||
|
||||
const batch = this.batch;
|
||||
this.batch = [];
|
||||
|
||||
this.remoteCall(batch.map((q) => q.args)).then((results) => {
|
||||
batch.forEach((item, i) => {
|
||||
const result = results[i];
|
||||
if (result && result instanceof Error) {
|
||||
item.reject(result);
|
||||
} else {
|
||||
item.resolve(result);
|
||||
}
|
||||
});
|
||||
}).catch((error) => batch.forEach((item) => item.reject(error)));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
import { Argument, Module as ProtoModule, WorkingInit } from "../proto";
|
||||
import { OperatingSystem } from "../common/connection";
|
||||
import { Module, ServerProxy } from "./proxy";
|
||||
|
||||
// tslint:disable no-any
|
||||
|
||||
/**
|
||||
* Return true if we're in a browser environment (including web workers).
|
||||
*/
|
||||
@@ -14,86 +20,211 @@ export const escapePath = (path: string): string => {
|
||||
};
|
||||
|
||||
export type IEncodingOptions = {
|
||||
encoding?: string | null;
|
||||
encoding?: BufferEncoding | null;
|
||||
flag?: string;
|
||||
mode?: string;
|
||||
persistent?: boolean;
|
||||
recursive?: boolean;
|
||||
} | string | undefined | null;
|
||||
} | BufferEncoding | undefined | null;
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
export type IEncodingOptionsCallback = IEncodingOptions | ((err: NodeJS.ErrnoException, ...args: any[]) => void);
|
||||
|
||||
/**
|
||||
* Stringify an event argument. isError is because although methods like
|
||||
* `fs.stat` are supposed to throw Error objects, they currently throw regular
|
||||
* objects when running tests through Jest.
|
||||
* Convert an argument to proto.
|
||||
* If sending a function is possible, provide `storeFunction`.
|
||||
* If sending a proxy is possible, provide `storeProxy`.
|
||||
*/
|
||||
export const stringify = (arg: any, isError?: boolean): string => { // tslint:disable-line no-any
|
||||
if (arg instanceof Error || isError) {
|
||||
// Errors don't stringify at all. They just become "{}".
|
||||
return JSON.stringify({
|
||||
type: "Error",
|
||||
data: {
|
||||
message: arg.message,
|
||||
stack: arg.stack,
|
||||
code: (arg as NodeJS.ErrnoException).code,
|
||||
},
|
||||
});
|
||||
} else if (arg instanceof Uint8Array) {
|
||||
// With stringify, these get turned into objects with each index becoming a
|
||||
// key for some reason. Then trying to do something like write that data
|
||||
// results in [object Object] being written. Stringify them like a Buffer
|
||||
// instead.
|
||||
return JSON.stringify({
|
||||
type: "Buffer",
|
||||
data: Array.from(arg),
|
||||
});
|
||||
}
|
||||
export const argumentToProto = (
|
||||
value: any,
|
||||
storeFunction?: (fn: () => void) => number,
|
||||
storeProxy?: (proxy: ServerProxy) => number,
|
||||
): Argument => {
|
||||
const convert = (currentValue: any): Argument => {
|
||||
const message = new Argument();
|
||||
|
||||
return JSON.stringify(arg);
|
||||
};
|
||||
/**
|
||||
* Parse an event argument.
|
||||
*/
|
||||
export const parse = (arg: string): any => { // tslint:disable-line no-any
|
||||
const convert = (value: any): any => { // tslint:disable-line no-any
|
||||
if (value && value.data && value.type) {
|
||||
switch (value.type) {
|
||||
// JSON.stringify turns a Buffer into an object but JSON.parse doesn't
|
||||
// turn it back, it just remains an object.
|
||||
case "Buffer":
|
||||
if (Array.isArray(value.data)) {
|
||||
return Buffer.from(value);
|
||||
}
|
||||
if (currentValue instanceof Error
|
||||
|| (currentValue && typeof currentValue.message !== "undefined"
|
||||
&& typeof currentValue.stack !== "undefined")) {
|
||||
const arg = new Argument.ErrorValue();
|
||||
arg.setMessage(currentValue.message);
|
||||
arg.setStack(currentValue.stack);
|
||||
arg.setCode(currentValue.code);
|
||||
message.setError(arg);
|
||||
} else if (currentValue instanceof Uint8Array || currentValue instanceof Buffer) {
|
||||
const arg = new Argument.BufferValue();
|
||||
arg.setData(currentValue);
|
||||
message.setBuffer(arg);
|
||||
} else if (Array.isArray(currentValue)) {
|
||||
const arg = new Argument.ArrayValue();
|
||||
arg.setDataList(currentValue.map(convert));
|
||||
message.setArray(arg);
|
||||
} else if (isProxy(currentValue)) {
|
||||
if (!storeProxy) {
|
||||
throw new Error("no way to serialize proxy");
|
||||
}
|
||||
const arg = new Argument.ProxyValue();
|
||||
arg.setId(storeProxy(currentValue));
|
||||
message.setProxy(arg);
|
||||
} else if (currentValue !== null && typeof currentValue === "object") {
|
||||
const arg = new Argument.ObjectValue();
|
||||
const map = arg.getDataMap();
|
||||
Object.keys(currentValue).forEach((key) => {
|
||||
map.set(key, convert(currentValue[key]));
|
||||
});
|
||||
message.setObject(arg);
|
||||
} else if (currentValue === null) {
|
||||
message.setNull(new Argument.NullValue());
|
||||
} else {
|
||||
switch (typeof currentValue) {
|
||||
case "undefined":
|
||||
message.setUndefined(new Argument.UndefinedValue());
|
||||
break;
|
||||
// Errors apparently can't be stringified, so we do something similar to
|
||||
// what happens to buffers and stringify them as regular objects.
|
||||
case "Error":
|
||||
if (value.data.message) {
|
||||
const error = new Error(value.data.message);
|
||||
// TODO: Can we set the stack? Doing so seems to make it into an
|
||||
// "invalid object".
|
||||
if (typeof value.data.code !== "undefined") {
|
||||
(error as NodeJS.ErrnoException).code = value.data.code;
|
||||
}
|
||||
// tslint:disable-next-line no-any
|
||||
(error as any).originalStack = value.data.stack;
|
||||
|
||||
return error;
|
||||
case "function":
|
||||
if (!storeFunction) {
|
||||
throw new Error("no way to serialize function");
|
||||
}
|
||||
const arg = new Argument.FunctionValue();
|
||||
arg.setId(storeFunction(currentValue));
|
||||
message.setFunction(arg);
|
||||
break;
|
||||
case "number":
|
||||
message.setNumber(currentValue);
|
||||
break;
|
||||
case "string":
|
||||
message.setString(currentValue);
|
||||
break;
|
||||
case "boolean":
|
||||
message.setBoolean(currentValue);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`cannot convert ${typeof currentValue} to proto`);
|
||||
}
|
||||
}
|
||||
|
||||
if (value && typeof value === "object") {
|
||||
Object.keys(value).forEach((key) => {
|
||||
value[key] = convert(value[key]);
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
return message;
|
||||
};
|
||||
|
||||
return arg ? convert(JSON.parse(arg)) : arg;
|
||||
return convert(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert proto to an argument.
|
||||
* If running a remote callback is supported, provide `runCallback`.
|
||||
* If using a remote proxy is supported, provide `createProxy`.
|
||||
*/
|
||||
export const protoToArgument = (
|
||||
message?: Argument,
|
||||
runCallback?: (id: number, args: any[]) => void,
|
||||
createProxy?: (id: number) => ServerProxy,
|
||||
): any => {
|
||||
const convert = (currentMessage: Argument): any => {
|
||||
switch (currentMessage.getMsgCase()) {
|
||||
case Argument.MsgCase.ERROR:
|
||||
const errorMessage = currentMessage.getError()!;
|
||||
const error = new Error(errorMessage.getMessage());
|
||||
(error as NodeJS.ErrnoException).code = errorMessage.getCode();
|
||||
(error as any).originalStack = errorMessage.getStack();
|
||||
|
||||
return error;
|
||||
case Argument.MsgCase.BUFFER:
|
||||
return Buffer.from(currentMessage.getBuffer()!.getData() as Uint8Array);
|
||||
case Argument.MsgCase.ARRAY:
|
||||
return currentMessage.getArray()!.getDataList().map((a) => convert(a));
|
||||
case Argument.MsgCase.PROXY:
|
||||
if (!createProxy) {
|
||||
throw new Error("no way to create proxy");
|
||||
}
|
||||
|
||||
return createProxy(currentMessage.getProxy()!.getId());
|
||||
case Argument.MsgCase.OBJECT:
|
||||
const obj: { [Key: string]: any } = {};
|
||||
currentMessage.getObject()!.getDataMap().forEach((argument, key) => {
|
||||
obj[key] = convert(argument);
|
||||
});
|
||||
|
||||
return obj;
|
||||
case Argument.MsgCase.UNDEFINED:
|
||||
return undefined;
|
||||
case Argument.MsgCase.NULL:
|
||||
return null;
|
||||
case Argument.MsgCase.FUNCTION:
|
||||
if (!runCallback) {
|
||||
throw new Error("no way to run remote callback");
|
||||
}
|
||||
|
||||
return (...args: any[]): void => {
|
||||
return runCallback(currentMessage.getFunction()!.getId(), args);
|
||||
};
|
||||
case Argument.MsgCase.NUMBER:
|
||||
return currentMessage.getNumber();
|
||||
case Argument.MsgCase.STRING:
|
||||
return currentMessage.getString();
|
||||
case Argument.MsgCase.BOOLEAN:
|
||||
return currentMessage.getBoolean();
|
||||
default:
|
||||
throw new Error("cannot convert unexpected proto to argument");
|
||||
}
|
||||
};
|
||||
|
||||
return message && convert(message);
|
||||
};
|
||||
|
||||
export const protoToModule = (protoModule: ProtoModule): Module => {
|
||||
switch (protoModule) {
|
||||
case ProtoModule.CHILDPROCESS: return Module.ChildProcess;
|
||||
case ProtoModule.FS: return Module.Fs;
|
||||
case ProtoModule.NET: return Module.Net;
|
||||
case ProtoModule.NODEPTY: return Module.NodePty;
|
||||
case ProtoModule.SPDLOG: return Module.Spdlog;
|
||||
case ProtoModule.TRASH: return Module.Trash;
|
||||
default: throw new Error(`invalid module ${protoModule}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const moduleToProto = (moduleName: Module): ProtoModule => {
|
||||
switch (moduleName) {
|
||||
case Module.ChildProcess: return ProtoModule.CHILDPROCESS;
|
||||
case Module.Fs: return ProtoModule.FS;
|
||||
case Module.Net: return ProtoModule.NET;
|
||||
case Module.NodePty: return ProtoModule.NODEPTY;
|
||||
case Module.Spdlog: return ProtoModule.SPDLOG;
|
||||
case Module.Trash: return ProtoModule.TRASH;
|
||||
default: throw new Error(`invalid module "${moduleName}"`);
|
||||
}
|
||||
};
|
||||
|
||||
export const protoToOperatingSystem = (protoOp: WorkingInit.OperatingSystem): OperatingSystem => {
|
||||
switch (protoOp) {
|
||||
case WorkingInit.OperatingSystem.WINDOWS: return OperatingSystem.Windows;
|
||||
case WorkingInit.OperatingSystem.LINUX: return OperatingSystem.Linux;
|
||||
case WorkingInit.OperatingSystem.MAC: return OperatingSystem.Mac;
|
||||
default: throw new Error(`unsupported operating system ${protoOp}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const platformToProto = (platform: NodeJS.Platform): WorkingInit.OperatingSystem => {
|
||||
switch (platform) {
|
||||
case "win32": return WorkingInit.OperatingSystem.WINDOWS;
|
||||
case "linux": return WorkingInit.OperatingSystem.LINUX;
|
||||
case "darwin": return WorkingInit.OperatingSystem.MAC;
|
||||
default: throw new Error(`unrecognized platform "${platform}"`);
|
||||
}
|
||||
};
|
||||
|
||||
export const isProxy = (value: any): value is ServerProxy => {
|
||||
return value && typeof value === "object" && typeof value.onEvent === "function";
|
||||
};
|
||||
|
||||
export const isPromise = (value: any): value is Promise<any> => {
|
||||
return typeof value.then === "function" && typeof value.catch === "function";
|
||||
};
|
||||
|
||||
/**
|
||||
* When spawning VS Code tries to preserve the environment but since it's in
|
||||
* the browser, it doesn't work.
|
||||
*/
|
||||
export const preserveEnv = (options?: { env?: NodeJS.ProcessEnv } | null): void => {
|
||||
if (options && options.env) {
|
||||
options.env = { ...process.env, ...options.env };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from "./browser/client";
|
||||
export * from "./common/connection";
|
||||
export * from "./common/helpers";
|
||||
export * from "./common/proxy";
|
||||
export * from "./common/util";
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
import { fork as cpFork } from "child_process";
|
||||
import { EventEmitter } from "events";
|
||||
import * as vm from "vm";
|
||||
import { logger, field } from "@coder/logger";
|
||||
import { NewEvalMessage, EvalFailedMessage, EvalDoneMessage, ServerMessage, EvalEventMessage } from "../proto";
|
||||
import { SendableConnection } from "../common/connection";
|
||||
import { ServerActiveEvalHelper, EvalHelper, ForkProvider, Modules } from "../common/helpers";
|
||||
import { stringify, parse } from "../common/util";
|
||||
|
||||
export interface ActiveEvaluation {
|
||||
onEvent(msg: EvalEventMessage): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
declare var __non_webpack_require__: typeof require;
|
||||
export const evaluate = (connection: SendableConnection, message: NewEvalMessage, onDispose: () => void, fork?: ForkProvider): ActiveEvaluation | void => {
|
||||
/**
|
||||
* Send the response and call onDispose.
|
||||
*/
|
||||
// tslint:disable-next-line no-any
|
||||
const sendResp = (resp: any): void => {
|
||||
logger.trace(() => [
|
||||
"resolve",
|
||||
field("id", message.getId()),
|
||||
field("response", stringify(resp)),
|
||||
]);
|
||||
|
||||
const evalDone = new EvalDoneMessage();
|
||||
evalDone.setId(message.getId());
|
||||
evalDone.setResponse(stringify(resp));
|
||||
|
||||
const serverMsg = new ServerMessage();
|
||||
serverMsg.setEvalDone(evalDone);
|
||||
connection.send(serverMsg.serializeBinary());
|
||||
|
||||
onDispose();
|
||||
};
|
||||
|
||||
/**
|
||||
* Send an exception and call onDispose.
|
||||
*/
|
||||
const sendException = (error: Error): void => {
|
||||
logger.trace(() => [
|
||||
"reject",
|
||||
field("id", message.getId()),
|
||||
field("response", stringify(error, true)),
|
||||
]);
|
||||
|
||||
const evalFailed = new EvalFailedMessage();
|
||||
evalFailed.setId(message.getId());
|
||||
evalFailed.setResponse(stringify(error, true));
|
||||
|
||||
const serverMsg = new ServerMessage();
|
||||
serverMsg.setEvalFailed(evalFailed);
|
||||
connection.send(serverMsg.serializeBinary());
|
||||
|
||||
onDispose();
|
||||
};
|
||||
|
||||
const modules: Modules = {
|
||||
spdlog: require("spdlog"),
|
||||
pty: require("node-pty-prebuilt"),
|
||||
trash: require("trash"),
|
||||
};
|
||||
|
||||
let eventEmitter = message.getActive() ? new EventEmitter(): undefined;
|
||||
const sandbox = {
|
||||
helper: eventEmitter ? new ServerActiveEvalHelper(modules, {
|
||||
removeAllListeners: (event?: string): void => {
|
||||
eventEmitter!.removeAllListeners(event);
|
||||
},
|
||||
// tslint:disable no-any
|
||||
on: (event: string, cb: (...args: any[]) => void): void => {
|
||||
eventEmitter!.on(event, (...args: any[]) => {
|
||||
logger.trace(() => [
|
||||
`${event}`,
|
||||
field("id", message.getId()),
|
||||
field("args", args.map((a) => stringify(a))),
|
||||
]);
|
||||
cb(...args);
|
||||
});
|
||||
},
|
||||
emit: (event: string, ...args: any[]): void => {
|
||||
logger.trace(() => [
|
||||
`emit ${event}`,
|
||||
field("id", message.getId()),
|
||||
field("args", args.map((a) => stringify(a))),
|
||||
]);
|
||||
const eventMsg = new EvalEventMessage();
|
||||
eventMsg.setEvent(event);
|
||||
eventMsg.setArgsList(args.map((a) => stringify(a)));
|
||||
eventMsg.setId(message.getId());
|
||||
const serverMsg = new ServerMessage();
|
||||
serverMsg.setEvalEvent(eventMsg);
|
||||
connection.send(serverMsg.serializeBinary());
|
||||
},
|
||||
// tslint:enable no-any
|
||||
}, fork || cpFork) : new EvalHelper(modules),
|
||||
_Buffer: Buffer,
|
||||
// When the client is ran from Webpack, it will replace
|
||||
// __non_webpack_require__ with require, which we then need to provide to
|
||||
// the sandbox. Since the server might also be using Webpack, we need to set
|
||||
// it to the non-Webpack version when that's the case. Then we need to also
|
||||
// provide __non_webpack_require__ for when the client doesn't run through
|
||||
// Webpack meaning it doesn't get replaced with require (Jest for example).
|
||||
require: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require,
|
||||
__non_webpack_require__: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require,
|
||||
setTimeout,
|
||||
setInterval,
|
||||
clearTimeout,
|
||||
process: {
|
||||
env: process.env,
|
||||
},
|
||||
args: message.getArgsList().map(parse),
|
||||
};
|
||||
|
||||
let value: any; // tslint:disable-line no-any
|
||||
try {
|
||||
const code = `(${message.getFunction()})(helper, ...args);`;
|
||||
value = vm.runInNewContext(code, sandbox, {
|
||||
// If the code takes longer than this to return, it is killed and throws.
|
||||
timeout: message.getTimeout() || 15000,
|
||||
});
|
||||
} catch (ex) {
|
||||
sendException(ex);
|
||||
}
|
||||
|
||||
// An evaluation completes when the value it returns resolves. An active
|
||||
// evaluation completes when it is disposed. Active evaluations are required
|
||||
// to return disposers so we can know both when it has ended (so we can clean
|
||||
// up on our end) and how to force end it (for example when the client
|
||||
// disconnects).
|
||||
// tslint:disable-next-line no-any
|
||||
const promise = !eventEmitter ? value as Promise<any> : new Promise((resolve): void => {
|
||||
value.onDidDispose(resolve);
|
||||
});
|
||||
if (promise && promise.then) {
|
||||
promise.then(sendResp).catch(sendException);
|
||||
} else {
|
||||
sendResp(value);
|
||||
}
|
||||
|
||||
return eventEmitter ? {
|
||||
onEvent: (eventMsg: EvalEventMessage): void => {
|
||||
eventEmitter!.emit(eventMsg.getEvent(), ...eventMsg.getArgsList().map(parse));
|
||||
},
|
||||
dispose: (): void => {
|
||||
if (eventEmitter) {
|
||||
if (value && value.dispose) {
|
||||
value.dispose();
|
||||
}
|
||||
eventEmitter.removeAllListeners();
|
||||
eventEmitter = undefined;
|
||||
}
|
||||
},
|
||||
} : undefined;
|
||||
};
|
||||
105
packages/protocol/src/node/modules/child_process.ts
Normal file
105
packages/protocol/src/node/modules/child_process.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import * as cp from "child_process";
|
||||
import { ServerProxy } from "../../common/proxy";
|
||||
import { preserveEnv } from "../../common/util";
|
||||
import { WritableProxy, ReadableProxy } from "./stream";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
export type ForkProvider = (modulePath: string, args?: string[], options?: cp.ForkOptions) => cp.ChildProcess;
|
||||
|
||||
export class ChildProcessProxy implements ServerProxy {
|
||||
public constructor(private readonly process: cp.ChildProcess) {}
|
||||
|
||||
public async kill(signal?: string): Promise<void> {
|
||||
this.process.kill(signal);
|
||||
}
|
||||
|
||||
public async disconnect(): Promise<void> {
|
||||
this.process.disconnect();
|
||||
}
|
||||
|
||||
public async ref(): Promise<void> {
|
||||
this.process.ref();
|
||||
}
|
||||
|
||||
public async unref(): Promise<void> {
|
||||
this.process.unref();
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async send(message: any): Promise<void> {
|
||||
return new Promise((resolve, reject): void => {
|
||||
this.process.send(message, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async getPid(): Promise<number> {
|
||||
return this.process.pid;
|
||||
}
|
||||
|
||||
public async onDone(cb: () => void): Promise<void> {
|
||||
this.process.on("close", cb);
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
this.process.kill();
|
||||
setTimeout(() => this.process.kill("SIGKILL"), 5000); // Double tap.
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
|
||||
this.process.on("close", (code, signal) => cb("close", code, signal));
|
||||
this.process.on("disconnect", () => cb("disconnect"));
|
||||
this.process.on("error", (error) => cb("error", error));
|
||||
this.process.on("exit", (exitCode, signal) => cb("exit", exitCode, signal));
|
||||
this.process.on("message", (message) => cb("message", message));
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChildProcessProxies {
|
||||
childProcess: ChildProcessProxy;
|
||||
stdin?: WritableProxy | null;
|
||||
stdout?: ReadableProxy | null;
|
||||
stderr?: ReadableProxy | null;
|
||||
}
|
||||
|
||||
export class ChildProcessModuleProxy {
|
||||
public constructor(private readonly forkProvider?: ForkProvider) {}
|
||||
|
||||
public async exec(
|
||||
command: string,
|
||||
options?: { encoding?: string | null } & cp.ExecOptions | null,
|
||||
callback?: ((error: cp.ExecException | null, stdin: string | Buffer, stdout: string | Buffer) => void),
|
||||
): Promise<ChildProcessProxies> {
|
||||
preserveEnv(options);
|
||||
|
||||
return this.returnProxies(cp.exec(command, options, callback));
|
||||
}
|
||||
|
||||
public async fork(modulePath: string, args?: string[], options?: cp.ForkOptions): Promise<ChildProcessProxies> {
|
||||
preserveEnv(options);
|
||||
|
||||
return this.returnProxies((this.forkProvider || cp.fork)(modulePath, args, options));
|
||||
}
|
||||
|
||||
public async spawn(command: string, args?: string[], options?: cp.SpawnOptions): Promise<ChildProcessProxies> {
|
||||
preserveEnv(options);
|
||||
|
||||
return this.returnProxies(cp.spawn(command, args, options));
|
||||
}
|
||||
|
||||
private returnProxies(process: cp.ChildProcess): ChildProcessProxies {
|
||||
return {
|
||||
childProcess: new ChildProcessProxy(process),
|
||||
stdin: process.stdin && new WritableProxy(process.stdin),
|
||||
stdout: process.stdout && new ReadableProxy(process.stdout),
|
||||
stderr: process.stderr && new ReadableProxy(process.stderr),
|
||||
};
|
||||
}
|
||||
}
|
||||
264
packages/protocol/src/node/modules/fs.ts
Normal file
264
packages/protocol/src/node/modules/fs.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import * as fs from "fs";
|
||||
import { promisify } from "util";
|
||||
import { ServerProxy } from "../../common/proxy";
|
||||
import { IEncodingOptions } from "../../common/util";
|
||||
import { WritableProxy } from "./stream";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
/**
|
||||
* A serializable version of fs.Stats.
|
||||
*/
|
||||
export interface Stats {
|
||||
dev: number;
|
||||
ino: number;
|
||||
mode: number;
|
||||
nlink: number;
|
||||
uid: number;
|
||||
gid: number;
|
||||
rdev: number;
|
||||
size: number;
|
||||
blksize: number;
|
||||
blocks: number;
|
||||
atimeMs: number;
|
||||
mtimeMs: number;
|
||||
ctimeMs: number;
|
||||
birthtimeMs: number;
|
||||
atime: Date | string;
|
||||
mtime: Date | string;
|
||||
ctime: Date | string;
|
||||
birthtime: Date | string;
|
||||
_isFile: boolean;
|
||||
_isDirectory: boolean;
|
||||
_isBlockDevice: boolean;
|
||||
_isCharacterDevice: boolean;
|
||||
_isSymbolicLink: boolean;
|
||||
_isFIFO: boolean;
|
||||
_isSocket: boolean;
|
||||
}
|
||||
|
||||
export class WriteStreamProxy extends WritableProxy<fs.WriteStream> {
|
||||
public async close(): Promise<void> {
|
||||
this.stream.close();
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
await super.dispose();
|
||||
this.stream.close();
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
|
||||
await super.onEvent(cb);
|
||||
this.stream.on("open", (fd) => cb("open", fd));
|
||||
}
|
||||
}
|
||||
|
||||
export class WatcherProxy implements ServerProxy {
|
||||
public constructor(private readonly watcher: fs.FSWatcher) {}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
this.watcher.close();
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
this.watcher.close();
|
||||
this.watcher.removeAllListeners();
|
||||
}
|
||||
|
||||
public async onDone(cb: () => void): Promise<void> {
|
||||
this.watcher.on("close", cb);
|
||||
this.watcher.on("error", cb);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
|
||||
this.watcher.on("change", (event, filename) => cb("change", event, filename));
|
||||
this.watcher.on("close", () => cb("close"));
|
||||
this.watcher.on("error", (error) => cb("error", error));
|
||||
}
|
||||
}
|
||||
|
||||
export class FsModuleProxy {
|
||||
public access(path: fs.PathLike, mode?: number): Promise<void> {
|
||||
return promisify(fs.access)(path, mode);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public appendFile(file: fs.PathLike | number, data: any, options?: fs.WriteFileOptions): Promise<void> {
|
||||
return promisify(fs.appendFile)(file, data, options);
|
||||
}
|
||||
|
||||
public chmod(path: fs.PathLike, mode: string | number): Promise<void> {
|
||||
return promisify(fs.chmod)(path, mode);
|
||||
}
|
||||
|
||||
public chown(path: fs.PathLike, uid: number, gid: number): Promise<void> {
|
||||
return promisify(fs.chown)(path, uid, gid);
|
||||
}
|
||||
|
||||
public close(fd: number): Promise<void> {
|
||||
return promisify(fs.close)(fd);
|
||||
}
|
||||
|
||||
public copyFile(src: fs.PathLike, dest: fs.PathLike, flags?: number): Promise<void> {
|
||||
return promisify(fs.copyFile)(src, dest, flags);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async createWriteStream(path: fs.PathLike, options?: any): Promise<WriteStreamProxy> {
|
||||
return new WriteStreamProxy(fs.createWriteStream(path, options));
|
||||
}
|
||||
|
||||
public exists(path: fs.PathLike): Promise<boolean> {
|
||||
return promisify(fs.exists)(path); // tslint:disable-line deprecation
|
||||
}
|
||||
|
||||
public fchmod(fd: number, mode: string | number): Promise<void> {
|
||||
return promisify(fs.fchmod)(fd, mode);
|
||||
}
|
||||
|
||||
public fchown(fd: number, uid: number, gid: number): Promise<void> {
|
||||
return promisify(fs.fchown)(fd, uid, gid);
|
||||
}
|
||||
|
||||
public fdatasync(fd: number): Promise<void> {
|
||||
return promisify(fs.fdatasync)(fd);
|
||||
}
|
||||
|
||||
public async fstat(fd: number): Promise<Stats> {
|
||||
return this.makeStatsSerializable(await promisify(fs.fstat)(fd));
|
||||
}
|
||||
|
||||
public fsync(fd: number): Promise<void> {
|
||||
return promisify(fs.fsync)(fd);
|
||||
}
|
||||
|
||||
public ftruncate(fd: number, len?: number | null): Promise<void> {
|
||||
return promisify(fs.ftruncate)(fd, len);
|
||||
}
|
||||
|
||||
public futimes(fd: number, atime: string | number | Date, mtime: string | number | Date): Promise<void> {
|
||||
return promisify(fs.futimes)(fd, atime, mtime);
|
||||
}
|
||||
|
||||
public lchmod(path: fs.PathLike, mode: string | number): Promise<void> {
|
||||
return promisify(fs.lchmod)(path, mode);
|
||||
}
|
||||
|
||||
public lchown(path: fs.PathLike, uid: number, gid: number): Promise<void> {
|
||||
return promisify(fs.lchown)(path, uid, gid);
|
||||
}
|
||||
|
||||
public link(existingPath: fs.PathLike, newPath: fs.PathLike): Promise<void> {
|
||||
return promisify(fs.link)(existingPath, newPath);
|
||||
}
|
||||
|
||||
public async lstat(path: fs.PathLike): Promise<Stats> {
|
||||
return this.makeStatsSerializable(await promisify(fs.lstat)(path));
|
||||
}
|
||||
|
||||
public async lstatBatch(args: { path: fs.PathLike }[]): Promise<(Stats | Error)[]> {
|
||||
return Promise.all(args.map((a) => this.lstat(a.path).catch((e) => e)));
|
||||
}
|
||||
|
||||
public mkdir(path: fs.PathLike, mode: number | string | fs.MakeDirectoryOptions | undefined | null): Promise<void> {
|
||||
return promisify(fs.mkdir)(path, mode);
|
||||
}
|
||||
|
||||
public mkdtemp(prefix: string, options: IEncodingOptions): Promise<string | Buffer> {
|
||||
return promisify(fs.mkdtemp)(prefix, options);
|
||||
}
|
||||
|
||||
public open(path: fs.PathLike, flags: string | number, mode: string | number | undefined | null): Promise<number> {
|
||||
return promisify(fs.open)(path, flags, mode);
|
||||
}
|
||||
|
||||
public read(fd: number, length: number, position: number | null): Promise<{ bytesRead: number, buffer: Buffer }> {
|
||||
const buffer = Buffer.alloc(length);
|
||||
|
||||
return promisify(fs.read)(fd, buffer, 0, length, position);
|
||||
}
|
||||
|
||||
public readFile(path: fs.PathLike | number, options: IEncodingOptions): Promise<string | Buffer> {
|
||||
return promisify(fs.readFile)(path, options);
|
||||
}
|
||||
|
||||
public readdir(path: fs.PathLike, options: IEncodingOptions): Promise<Buffer[] | fs.Dirent[] | string[]> {
|
||||
return promisify(fs.readdir)(path, options);
|
||||
}
|
||||
|
||||
public readdirBatch(args: { path: fs.PathLike, options: IEncodingOptions }[]): Promise<(Buffer[] | fs.Dirent[] | string[] | Error)[]> {
|
||||
return Promise.all(args.map((a) => this.readdir(a.path, a.options).catch((e) => e)));
|
||||
}
|
||||
|
||||
public readlink(path: fs.PathLike, options: IEncodingOptions): Promise<string | Buffer> {
|
||||
return promisify(fs.readlink)(path, options);
|
||||
}
|
||||
|
||||
public realpath(path: fs.PathLike, options: IEncodingOptions): Promise<string | Buffer> {
|
||||
return promisify(fs.realpath)(path, options);
|
||||
}
|
||||
|
||||
public rename(oldPath: fs.PathLike, newPath: fs.PathLike): Promise<void> {
|
||||
return promisify(fs.rename)(oldPath, newPath);
|
||||
}
|
||||
|
||||
public rmdir(path: fs.PathLike): Promise<void> {
|
||||
return promisify(fs.rmdir)(path);
|
||||
}
|
||||
|
||||
public async stat(path: fs.PathLike): Promise<Stats> {
|
||||
return this.makeStatsSerializable(await promisify(fs.stat)(path));
|
||||
}
|
||||
|
||||
public async statBatch(args: { path: fs.PathLike }[]): Promise<(Stats | Error)[]> {
|
||||
return Promise.all(args.map((a) => this.stat(a.path).catch((e) => e)));
|
||||
}
|
||||
|
||||
public symlink(target: fs.PathLike, path: fs.PathLike, type?: fs.symlink.Type | null): Promise<void> {
|
||||
return promisify(fs.symlink)(target, path, type);
|
||||
}
|
||||
|
||||
public truncate(path: fs.PathLike, len?: number | null): Promise<void> {
|
||||
return promisify(fs.truncate)(path, len);
|
||||
}
|
||||
|
||||
public unlink(path: fs.PathLike): Promise<void> {
|
||||
return promisify(fs.unlink)(path);
|
||||
}
|
||||
|
||||
public utimes(path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date): Promise<void> {
|
||||
return promisify(fs.utimes)(path, atime, mtime);
|
||||
}
|
||||
|
||||
public async write(fd: number, buffer: Buffer, offset?: number, length?: number, position?: number): Promise<{ bytesWritten: number, buffer: Buffer }> {
|
||||
return promisify(fs.write)(fd, buffer, offset, length, position);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public writeFile (path: fs.PathLike | number, data: any, options: IEncodingOptions): Promise<void> {
|
||||
return promisify(fs.writeFile)(path, data, options);
|
||||
}
|
||||
|
||||
public async watch(filename: fs.PathLike, options?: IEncodingOptions): Promise<WatcherProxy> {
|
||||
return new WatcherProxy(fs.watch(filename, options));
|
||||
}
|
||||
|
||||
private makeStatsSerializable(stats: fs.Stats): Stats {
|
||||
return {
|
||||
...stats,
|
||||
/**
|
||||
* We need to check if functions exist because nexe's implemented FS
|
||||
* lib doesnt implement fs.stats properly.
|
||||
*/
|
||||
_isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false,
|
||||
_isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false,
|
||||
_isDirectory: stats.isDirectory(),
|
||||
_isFIFO: stats.isFIFO ? stats.isFIFO() : false,
|
||||
_isFile: stats.isFile(),
|
||||
_isSocket: stats.isSocket ? stats.isSocket() : false,
|
||||
_isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false,
|
||||
};
|
||||
}
|
||||
}
|
||||
6
packages/protocol/src/node/modules/index.ts
Normal file
6
packages/protocol/src/node/modules/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./child_process";
|
||||
export * from "./fs";
|
||||
export * from "./net";
|
||||
export * from "./node-pty";
|
||||
export * from "./spdlog";
|
||||
export * from "./trash";
|
||||
92
packages/protocol/src/node/modules/net.ts
Normal file
92
packages/protocol/src/node/modules/net.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as net from "net";
|
||||
import { ServerProxy } from "../../common/proxy";
|
||||
import { DuplexProxy } from "./stream";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
export class NetSocketProxy extends DuplexProxy<net.Socket> {
|
||||
public async connect(options: number | string | net.SocketConnectOpts, host?: string): Promise<void> {
|
||||
this.stream.connect(options as any, host as any); // tslint:disable-line no-any this works fine
|
||||
}
|
||||
|
||||
public async unref(): Promise<void> {
|
||||
this.stream.unref();
|
||||
}
|
||||
|
||||
public async ref(): Promise<void> {
|
||||
this.stream.ref();
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
this.stream.removeAllListeners();
|
||||
this.stream.end();
|
||||
this.stream.destroy();
|
||||
this.stream.unref();
|
||||
}
|
||||
|
||||
public async onDone(cb: () => void): Promise<void> {
|
||||
this.stream.on("close", cb);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
|
||||
await super.onEvent(cb);
|
||||
this.stream.on("connect", () => cb("connect"));
|
||||
this.stream.on("lookup", (error, address, family, host) => cb("lookup", error, address, family, host));
|
||||
this.stream.on("timeout", () => cb("timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
export class NetServerProxy implements ServerProxy {
|
||||
public constructor(private readonly server: net.Server) {}
|
||||
|
||||
public async listen(handle?: net.ListenOptions | number | string, hostname?: string | number, backlog?: number): Promise<void> {
|
||||
this.server.listen(handle, hostname as any, backlog as any); // tslint:disable-line no-any this is fine
|
||||
}
|
||||
|
||||
public async ref(): Promise<void> {
|
||||
this.server.ref();
|
||||
}
|
||||
|
||||
public async unref(): Promise<void> {
|
||||
this.server.unref();
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
this.server.close();
|
||||
}
|
||||
|
||||
public async onConnection(cb: (proxy: NetSocketProxy) => void): Promise<void> {
|
||||
this.server.on("connection", (socket) => cb(new NetSocketProxy(socket)));
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
this.server.close();
|
||||
this.server.removeAllListeners();
|
||||
}
|
||||
|
||||
public async onDone(cb: () => void): Promise<void> {
|
||||
this.server.on("close", cb);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
|
||||
this.server.on("close", () => cb("close"));
|
||||
this.server.on("error", (error) => cb("error", error));
|
||||
this.server.on("listening", () => cb("listening"));
|
||||
}
|
||||
}
|
||||
|
||||
export class NetModuleProxy {
|
||||
public async createSocket(options?: net.SocketConstructorOpts): Promise<NetSocketProxy> {
|
||||
return new NetSocketProxy(new net.Socket(options));
|
||||
}
|
||||
|
||||
public async createConnection(target: string | number | net.NetConnectOpts, host?: string): Promise<NetSocketProxy> {
|
||||
return new NetSocketProxy(net.createConnection(target as any, host)); // tslint:disable-line no-any defeat stubborness
|
||||
}
|
||||
|
||||
public async createServer(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean }): Promise<NetServerProxy> {
|
||||
return new NetServerProxy(net.createServer(options));
|
||||
}
|
||||
}
|
||||
77
packages/protocol/src/node/modules/node-pty.ts
Normal file
77
packages/protocol/src/node/modules/node-pty.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/// <reference path="../../../../../lib/vscode/src/typings/node-pty.d.ts" />
|
||||
import { EventEmitter } from "events";
|
||||
import * as pty from "node-pty";
|
||||
import { ServerProxy } from "../../common/proxy";
|
||||
import { preserveEnv } from "../../common/util";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
/**
|
||||
* Server-side IPty proxy.
|
||||
*/
|
||||
export class NodePtyProcessProxy implements ServerProxy {
|
||||
private readonly emitter = new EventEmitter();
|
||||
|
||||
public constructor(private readonly process: pty.IPty) {
|
||||
let name = process.process;
|
||||
setTimeout(() => { // Need to wait for the caller to listen to the event.
|
||||
this.emitter.emit("process", name);
|
||||
}, 1);
|
||||
const timer = setInterval(() => {
|
||||
if (process.process !== name) {
|
||||
name = process.process;
|
||||
this.emitter.emit("process", name);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
this.process.on("exit", () => clearInterval(timer));
|
||||
}
|
||||
|
||||
public async getPid(): Promise<number> {
|
||||
return this.process.pid;
|
||||
}
|
||||
|
||||
public async getProcess(): Promise<string> {
|
||||
return this.process.process;
|
||||
}
|
||||
|
||||
public async kill(signal?: string): Promise<void> {
|
||||
this.process.kill(signal);
|
||||
}
|
||||
|
||||
public async resize(columns: number, rows: number): Promise<void> {
|
||||
this.process.resize(columns, rows);
|
||||
}
|
||||
|
||||
public async write(data: string): Promise<void> {
|
||||
this.process.write(data);
|
||||
}
|
||||
|
||||
public async onDone(cb: () => void): Promise<void> {
|
||||
this.process.on("exit", cb);
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
this.process.kill();
|
||||
setTimeout(() => this.process.kill("SIGKILL"), 5000); // Double tap.
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
|
||||
this.emitter.on("process", (process) => cb("process", process));
|
||||
this.process.on("data", (data) => cb("data", data));
|
||||
this.process.on("exit", (exitCode, signal) => cb("exit", exitCode, signal));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side node-pty proxy.
|
||||
*/
|
||||
export class NodePtyModuleProxy {
|
||||
public async spawn(file: string, args: string[] | string, options: pty.IPtyForkOptions): Promise<NodePtyProcessProxy> {
|
||||
preserveEnv(options);
|
||||
|
||||
return new NodePtyProcessProxy(require("node-pty").spawn(file, args, options));
|
||||
}
|
||||
}
|
||||
48
packages/protocol/src/node/modules/spdlog.ts
Normal file
48
packages/protocol/src/node/modules/spdlog.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/// <reference path="../../../../../lib/vscode/src/typings/spdlog.d.ts" />
|
||||
import { EventEmitter } from "events";
|
||||
import * as spdlog from "spdlog";
|
||||
import { ServerProxy } from "../../common/proxy";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
export class RotatingLoggerProxy implements ServerProxy {
|
||||
private readonly emitter = new EventEmitter();
|
||||
|
||||
public constructor(private readonly logger: spdlog.RotatingLogger) {}
|
||||
|
||||
public async trace (message: string): Promise<void> { this.logger.trace(message); }
|
||||
public async debug (message: string): Promise<void> { this.logger.debug(message); }
|
||||
public async info (message: string): Promise<void> { this.logger.info(message); }
|
||||
public async warn (message: string): Promise<void> { this.logger.warn(message); }
|
||||
public async error (message: string): Promise<void> { this.logger.error(message); }
|
||||
public async critical (message: string): Promise<void> { this.logger.critical(message); }
|
||||
public async setLevel (level: number): Promise<void> { this.logger.setLevel(level); }
|
||||
public async clearFormatters (): Promise<void> { this.logger.clearFormatters(); }
|
||||
public async flush (): Promise<void> { this.logger.flush(); }
|
||||
public async drop (): Promise<void> { this.logger.drop(); }
|
||||
|
||||
public async onDone(cb: () => void): Promise<void> {
|
||||
this.emitter.on("dispose", cb);
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
await this.flush();
|
||||
this.emitter.emit("dispose");
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async onEvent(_cb: (event: string, ...args: any[]) => void): Promise<void> {
|
||||
// No events.
|
||||
}
|
||||
}
|
||||
|
||||
export class SpdlogModuleProxy {
|
||||
public async createLogger(name: string, filePath: string, fileSize: number, fileCount: number): Promise<RotatingLoggerProxy> {
|
||||
return new RotatingLoggerProxy(new (require("spdlog") as typeof import("spdlog")).RotatingLogger(name, filePath, fileSize, fileCount));
|
||||
}
|
||||
|
||||
public async setAsyncMode(bufferSize: number, flushInterval: number): Promise<void> {
|
||||
require("spdlog").setAsyncMode(bufferSize, flushInterval);
|
||||
}
|
||||
}
|
||||
109
packages/protocol/src/node/modules/stream.ts
Normal file
109
packages/protocol/src/node/modules/stream.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import * as stream from "stream";
|
||||
import { ServerProxy } from "../../common/proxy";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
export class WritableProxy<T extends stream.Writable = stream.Writable> implements ServerProxy {
|
||||
public constructor(protected readonly stream: T) {}
|
||||
|
||||
public async destroy(): Promise<void> {
|
||||
this.stream.destroy();
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async end(data?: any, encoding?: string): Promise<void> {
|
||||
return new Promise((resolve): void => {
|
||||
this.stream.end(data, encoding, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async setDefaultEncoding(encoding: string): Promise<void> {
|
||||
this.stream.setDefaultEncoding(encoding);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async write(data: any, encoding?: string): Promise<void> {
|
||||
return new Promise((resolve, reject): void => {
|
||||
this.stream.write(data, encoding, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
this.stream.end();
|
||||
this.stream.removeAllListeners();
|
||||
}
|
||||
|
||||
public async onDone(cb: () => void): Promise<void> {
|
||||
this.stream.on("close", cb);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
|
||||
// Sockets have an extra argument on "close".
|
||||
// tslint:disable-next-line no-any
|
||||
this.stream.on("close", (...args: any[]) => cb("close", ...args));
|
||||
this.stream.on("drain", () => cb("drain"));
|
||||
this.stream.on("error", (error) => cb("error", error));
|
||||
this.stream.on("finish", () => cb("finish"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This noise is because we can't do multiple extends and we also can't seem to
|
||||
* do `extends WritableProxy<T> implement ReadableProxy<T>` (for `DuplexProxy`).
|
||||
*/
|
||||
export interface IReadableProxy extends ServerProxy {
|
||||
destroy(): Promise<void>;
|
||||
setEncoding(encoding: string): Promise<void>;
|
||||
dispose(): Promise<void>;
|
||||
onDone(cb: () => void): Promise<void>;
|
||||
}
|
||||
|
||||
export class ReadableProxy<T extends stream.Readable = stream.Readable> implements IReadableProxy {
|
||||
public constructor(protected readonly stream: T) {}
|
||||
|
||||
public async destroy(): Promise<void> {
|
||||
this.stream.destroy();
|
||||
}
|
||||
|
||||
public async setEncoding(encoding: string): Promise<void> {
|
||||
this.stream.setEncoding(encoding);
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
this.stream.destroy();
|
||||
}
|
||||
|
||||
public async onDone(cb: () => void): Promise<void> {
|
||||
this.stream.on("close", cb);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
|
||||
this.stream.on("close", () => cb("close"));
|
||||
this.stream.on("data", (chunk) => cb("data", chunk));
|
||||
this.stream.on("end", () => cb("end"));
|
||||
this.stream.on("error", (error) => cb("error", error));
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplexProxy<T extends stream.Duplex = stream.Duplex> extends WritableProxy<T> implements IReadableProxy {
|
||||
public async setEncoding(encoding: string): Promise<void> {
|
||||
this.stream.setEncoding(encoding);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
|
||||
await super.onEvent(cb);
|
||||
this.stream.on("data", (chunk) => cb("data", chunk));
|
||||
this.stream.on("end", () => cb("end"));
|
||||
}
|
||||
}
|
||||
9
packages/protocol/src/node/modules/trash.ts
Normal file
9
packages/protocol/src/node/modules/trash.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as trash from "trash";
|
||||
|
||||
// tslint:disable completed-docs
|
||||
|
||||
export class TrashModuleProxy {
|
||||
public async trash(path: string, options?: trash.Options): Promise<void> {
|
||||
return trash(path, options);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,81 @@
|
||||
import { mkdirp } from "fs-extra";
|
||||
import * as os from "os";
|
||||
import { logger, field } from "@coder/logger";
|
||||
import { Pong, ClientMessage, WorkingInitMessage, ServerMessage } from "../proto";
|
||||
import { evaluate, ActiveEvaluation } from "./evaluate";
|
||||
import { ForkProvider } from "../common/helpers";
|
||||
import { field, logger} from "@coder/logger";
|
||||
import { ReadWriteConnection } from "../common/connection";
|
||||
import { Module, ServerProxy } from "../common/proxy";
|
||||
import { isPromise, isProxy, moduleToProto, protoToArgument, platformToProto, protoToModule, argumentToProto } from "../common/util";
|
||||
import { Argument, Callback, ClientMessage, Event, Method, Pong, ServerMessage, WorkingInit } from "../proto";
|
||||
import { ChildProcessModuleProxy, ForkProvider, FsModuleProxy, NetModuleProxy, NodePtyModuleProxy, SpdlogModuleProxy, TrashModuleProxy } from "./modules";
|
||||
|
||||
// tslint:disable no-any
|
||||
|
||||
export interface ServerOptions {
|
||||
readonly workingDirectory: string;
|
||||
readonly dataDirectory: string;
|
||||
readonly cacheDirectory: string;
|
||||
readonly builtInExtensionsDirectory: string;
|
||||
readonly extensionsDirectory: string;
|
||||
readonly fork?: ForkProvider;
|
||||
}
|
||||
|
||||
interface ProxyData {
|
||||
disposeTimeout?: number | NodeJS.Timer;
|
||||
instance: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle messages from the client.
|
||||
*/
|
||||
export class Server {
|
||||
private readonly evals = new Map<number, ActiveEvaluation>();
|
||||
private proxyId = 0;
|
||||
private readonly proxies = new Map<number | Module, ProxyData>();
|
||||
private disconnected: boolean = false;
|
||||
private readonly responseTimeout = 10000;
|
||||
|
||||
public constructor(
|
||||
private readonly connection: ReadWriteConnection,
|
||||
private readonly options?: ServerOptions,
|
||||
) {
|
||||
connection.onMessage((data) => {
|
||||
connection.onMessage(async (data) => {
|
||||
try {
|
||||
this.handleMessage(ClientMessage.deserializeBinary(data));
|
||||
await this.handleMessage(ClientMessage.deserializeBinary(data));
|
||||
} catch (ex) {
|
||||
logger.error("Failed to handle client message", field("length", data.byteLength), field("exception", {
|
||||
message: ex.message,
|
||||
stack: ex.stack,
|
||||
}));
|
||||
logger.error(
|
||||
"Failed to handle client message",
|
||||
field("length", data.byteLength),
|
||||
field("exception", {
|
||||
message: ex.message,
|
||||
stack: ex.stack,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
connection.onClose(() => {
|
||||
this.evals.forEach((e) => e.dispose());
|
||||
this.disconnected = true;
|
||||
|
||||
logger.trace(() => [
|
||||
"disconnected from client",
|
||||
field("proxies", this.proxies.size),
|
||||
]);
|
||||
|
||||
this.proxies.forEach((proxy, proxyId) => {
|
||||
if (isProxy(proxy.instance)) {
|
||||
proxy.instance.dispose().catch((error) => {
|
||||
logger.error(error.message);
|
||||
});
|
||||
}
|
||||
this.removeProxy(proxyId);
|
||||
});
|
||||
});
|
||||
|
||||
this.storeProxy(new ChildProcessModuleProxy(this.options ? this.options.fork : undefined), Module.ChildProcess);
|
||||
this.storeProxy(new FsModuleProxy(), Module.Fs);
|
||||
this.storeProxy(new NetModuleProxy(), Module.Net);
|
||||
this.storeProxy(new NodePtyModuleProxy(), Module.NodePty);
|
||||
this.storeProxy(new SpdlogModuleProxy(), Module.Spdlog);
|
||||
this.storeProxy(new TrashModuleProxy(), Module.Trash);
|
||||
|
||||
if (!this.options) {
|
||||
logger.warn("No server options provided. InitMessage will not be sent.");
|
||||
|
||||
@@ -49,66 +90,273 @@ export class Server {
|
||||
logger.error(error.message, field("error", error));
|
||||
});
|
||||
|
||||
const initMsg = new WorkingInitMessage();
|
||||
const initMsg = new WorkingInit();
|
||||
initMsg.setDataDirectory(this.options.dataDirectory);
|
||||
initMsg.setWorkingDirectory(this.options.workingDirectory);
|
||||
initMsg.setBuiltinExtensionsDir(this.options.builtInExtensionsDirectory);
|
||||
initMsg.setExtensionsDirectory(this.options.extensionsDirectory);
|
||||
initMsg.setHomeDirectory(os.homedir());
|
||||
initMsg.setTmpDirectory(os.tmpdir());
|
||||
const platform = os.platform();
|
||||
let operatingSystem: WorkingInitMessage.OperatingSystem;
|
||||
switch (platform) {
|
||||
case "win32":
|
||||
operatingSystem = WorkingInitMessage.OperatingSystem.WINDOWS;
|
||||
break;
|
||||
case "linux":
|
||||
operatingSystem = WorkingInitMessage.OperatingSystem.LINUX;
|
||||
break;
|
||||
case "darwin":
|
||||
operatingSystem = WorkingInitMessage.OperatingSystem.MAC;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unrecognized platform "${platform}"`);
|
||||
}
|
||||
initMsg.setOperatingSystem(operatingSystem);
|
||||
initMsg.setShell(os.userInfo().shell || global.process.env.SHELL);
|
||||
initMsg.setOperatingSystem(platformToProto(os.platform()));
|
||||
initMsg.setShell(os.userInfo().shell || global.process.env.SHELL || "");
|
||||
const srvMsg = new ServerMessage();
|
||||
srvMsg.setInit(initMsg);
|
||||
connection.send(srvMsg.serializeBinary());
|
||||
}
|
||||
|
||||
private handleMessage(message: ClientMessage): void {
|
||||
if (message.hasNewEval()) {
|
||||
const evalMessage = message.getNewEval()!;
|
||||
logger.trace(() => [
|
||||
"EvalMessage",
|
||||
field("id", evalMessage.getId()),
|
||||
field("args", evalMessage.getArgsList()),
|
||||
field("function", evalMessage.getFunction()),
|
||||
]);
|
||||
const resp = evaluate(this.connection, evalMessage, () => {
|
||||
this.evals.delete(evalMessage.getId());
|
||||
logger.trace(() => [
|
||||
`dispose ${evalMessage.getId()}, ${this.evals.size} left`,
|
||||
]);
|
||||
}, this.options ? this.options.fork : undefined);
|
||||
if (resp) {
|
||||
this.evals.set(evalMessage.getId(), resp);
|
||||
}
|
||||
} else if (message.hasEvalEvent()) {
|
||||
const evalEventMessage = message.getEvalEvent()!;
|
||||
const e = this.evals.get(evalEventMessage.getId());
|
||||
if (!e) {
|
||||
return;
|
||||
}
|
||||
e.onEvent(evalEventMessage);
|
||||
} else if (message.hasPing()) {
|
||||
logger.trace("ping");
|
||||
const srvMsg = new ServerMessage();
|
||||
srvMsg.setPong(new Pong());
|
||||
this.connection.send(srvMsg.serializeBinary());
|
||||
} else {
|
||||
throw new Error("unknown message type");
|
||||
/**
|
||||
* Handle all messages from the client.
|
||||
*/
|
||||
private async handleMessage(message: ClientMessage): Promise<void> {
|
||||
switch (message.getMsgCase()) {
|
||||
case ClientMessage.MsgCase.METHOD:
|
||||
await this.runMethod(message.getMethod()!);
|
||||
break;
|
||||
case ClientMessage.MsgCase.PING:
|
||||
logger.trace("ping");
|
||||
const srvMsg = new ServerMessage();
|
||||
srvMsg.setPong(new Pong());
|
||||
this.connection.send(srvMsg.serializeBinary());
|
||||
break;
|
||||
default:
|
||||
throw new Error("unknown message type");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a method on a proxy.
|
||||
*/
|
||||
private async runMethod(message: Method): Promise<void> {
|
||||
const proxyMessage = message.getNamedProxy()! || message.getNumberedProxy()!;
|
||||
const id = proxyMessage.getId();
|
||||
const proxyId = message.hasNamedProxy()
|
||||
? protoToModule(message.getNamedProxy()!.getModule())
|
||||
: message.getNumberedProxy()!.getProxyId();
|
||||
const method = proxyMessage.getMethod();
|
||||
const args = proxyMessage.getArgsList().map((a) => protoToArgument(
|
||||
a,
|
||||
(id, args) => this.sendCallback(proxyId, id, args),
|
||||
));
|
||||
|
||||
logger.trace(() => [
|
||||
"received",
|
||||
field("id", id),
|
||||
field("proxyId", proxyId),
|
||||
field("method", method),
|
||||
]);
|
||||
|
||||
let response: any;
|
||||
try {
|
||||
const proxy = this.getProxy(proxyId);
|
||||
if (typeof proxy.instance[method] !== "function") {
|
||||
throw new Error(`"${method}" is not a function on proxy ${proxyId}`);
|
||||
}
|
||||
|
||||
response = proxy.instance[method](...args);
|
||||
|
||||
// We wait for the client to call "dispose" instead of doing it onDone to
|
||||
// ensure all the messages it sent get processed before we get rid of it.
|
||||
if (method === "dispose") {
|
||||
this.removeProxy(proxyId);
|
||||
}
|
||||
|
||||
// Proxies must always return promises.
|
||||
if (!isPromise(response)) {
|
||||
throw new Error(`"${method}" must return a promise`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error.message,
|
||||
field("type", typeof response),
|
||||
field("proxyId", proxyId),
|
||||
);
|
||||
this.sendException(id, error);
|
||||
}
|
||||
|
||||
try {
|
||||
this.sendResponse(id, await response);
|
||||
} catch (error) {
|
||||
this.sendException(id, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a callback to the client.
|
||||
*/
|
||||
private sendCallback(proxyId: number | Module, callbackId: number, args: any[]): void {
|
||||
logger.trace(() => [
|
||||
"sending callback",
|
||||
field("proxyId", proxyId),
|
||||
field("callbackId", callbackId),
|
||||
]);
|
||||
|
||||
const message = new Callback();
|
||||
let callbackMessage: Callback.Named | Callback.Numbered;
|
||||
if (typeof proxyId === "string") {
|
||||
callbackMessage = new Callback.Named();
|
||||
callbackMessage.setModule(moduleToProto(proxyId));
|
||||
message.setNamedCallback(callbackMessage);
|
||||
} else {
|
||||
callbackMessage = new Callback.Numbered();
|
||||
callbackMessage.setProxyId(proxyId);
|
||||
message.setNumberedCallback(callbackMessage);
|
||||
}
|
||||
callbackMessage.setCallbackId(callbackId);
|
||||
callbackMessage.setArgsList(args.map((a) => this.argumentToProto(a)));
|
||||
|
||||
const serverMessage = new ServerMessage();
|
||||
serverMessage.setCallback(message);
|
||||
this.connection.send(serverMessage.serializeBinary());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a numbered proxy and bind events to send them back to the client.
|
||||
*/
|
||||
private storeProxy(instance: ServerProxy): number;
|
||||
/**
|
||||
* Store a unique proxy and bind events to send them back to the client.
|
||||
*/
|
||||
private storeProxy(instance: any, moduleProxyId: Module): Module;
|
||||
/**
|
||||
* Store a proxy and bind events to send them back to the client.
|
||||
*/
|
||||
private storeProxy(instance: ServerProxy | any, moduleProxyId?: Module): number | Module {
|
||||
// In case we disposed while waiting for a function to return.
|
||||
if (this.disconnected) {
|
||||
if (isProxy(instance)) {
|
||||
instance.dispose().catch((error) => {
|
||||
logger.error(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error("disposed");
|
||||
}
|
||||
|
||||
const proxyId = moduleProxyId || this.proxyId++;
|
||||
logger.trace(() => [
|
||||
"storing proxy",
|
||||
field("proxyId", proxyId),
|
||||
]);
|
||||
|
||||
this.proxies.set(proxyId, { instance });
|
||||
|
||||
if (isProxy(instance)) {
|
||||
instance.onEvent((event, ...args) => this.sendEvent(proxyId, event, ...args)).catch((error) => {
|
||||
logger.error(error.message);
|
||||
});
|
||||
instance.onDone(() => {
|
||||
// It might have finished because we disposed it due to a disconnect.
|
||||
if (!this.disconnected) {
|
||||
this.sendEvent(proxyId, "done");
|
||||
this.getProxy(proxyId).disposeTimeout = setTimeout(() => {
|
||||
instance.dispose().catch((error) => {
|
||||
logger.error(error.message);
|
||||
});
|
||||
this.removeProxy(proxyId);
|
||||
}, this.responseTimeout);
|
||||
}
|
||||
}).catch((error) => {
|
||||
logger.error(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
return proxyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an event to the client.
|
||||
*/
|
||||
private sendEvent(proxyId: number | Module, event: string, ...args: any[]): void {
|
||||
logger.trace(() => [
|
||||
"sending event",
|
||||
field("proxyId", proxyId),
|
||||
field("event", event),
|
||||
]);
|
||||
|
||||
const message = new Event();
|
||||
let eventMessage: Event.Named | Event.Numbered;
|
||||
if (typeof proxyId === "string") {
|
||||
eventMessage = new Event.Named();
|
||||
eventMessage.setModule(moduleToProto(proxyId));
|
||||
message.setNamedEvent(eventMessage);
|
||||
} else {
|
||||
eventMessage = new Event.Numbered();
|
||||
eventMessage.setProxyId(proxyId);
|
||||
message.setNumberedEvent(eventMessage);
|
||||
}
|
||||
eventMessage.setEvent(event);
|
||||
eventMessage.setArgsList(args.map((a) => this.argumentToProto(a)));
|
||||
|
||||
const serverMessage = new ServerMessage();
|
||||
serverMessage.setEvent(message);
|
||||
this.connection.send(serverMessage.serializeBinary());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a response back to the client.
|
||||
*/
|
||||
private sendResponse(id: number, response: any): void {
|
||||
logger.trace(() => [
|
||||
"sending resolve",
|
||||
field("id", id),
|
||||
]);
|
||||
|
||||
const successMessage = new Method.Success();
|
||||
successMessage.setId(id);
|
||||
successMessage.setResponse(this.argumentToProto(response));
|
||||
|
||||
const serverMessage = new ServerMessage();
|
||||
serverMessage.setSuccess(successMessage);
|
||||
this.connection.send(serverMessage.serializeBinary());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an exception back to the client.
|
||||
*/
|
||||
private sendException(id: number, error: Error): void {
|
||||
logger.trace(() => [
|
||||
"sending reject",
|
||||
field("id", id) ,
|
||||
]);
|
||||
|
||||
const failedMessage = new Method.Fail();
|
||||
failedMessage.setId(id);
|
||||
failedMessage.setResponse(argumentToProto(error));
|
||||
|
||||
const serverMessage = new ServerMessage();
|
||||
serverMessage.setFail(failedMessage);
|
||||
this.connection.send(serverMessage.serializeBinary());
|
||||
}
|
||||
|
||||
/**
|
||||
* Call after disposing a proxy.
|
||||
*/
|
||||
private removeProxy(proxyId: number | Module): void {
|
||||
clearTimeout(this.getProxy(proxyId).disposeTimeout as any);
|
||||
this.proxies.delete(proxyId);
|
||||
|
||||
logger.trace(() => [
|
||||
"disposed and removed proxy",
|
||||
field("proxyId", proxyId),
|
||||
field("proxies", this.proxies.size),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as argumentToProto but provides storeProxy.
|
||||
*/
|
||||
private argumentToProto(value: any): Argument {
|
||||
return argumentToProto(value, undefined, (p) => this.storeProxy(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a proxy. Error if it doesn't exist.
|
||||
*/
|
||||
private getProxy(proxyId: number | Module): ProxyData {
|
||||
if (!this.proxies.has(proxyId)) {
|
||||
throw new Error(`proxy ${proxyId} disposed too early`);
|
||||
}
|
||||
|
||||
return this.proxies.get(proxyId)!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,33 +2,33 @@ syntax = "proto3";
|
||||
import "node.proto";
|
||||
import "vscode.proto";
|
||||
|
||||
// Messages that the client can send to the server.
|
||||
message ClientMessage {
|
||||
oneof msg {
|
||||
// node.proto
|
||||
NewEvalMessage new_eval = 11;
|
||||
EvalEventMessage eval_event = 12;
|
||||
|
||||
Ping ping = 13;
|
||||
Method method = 20;
|
||||
Ping ping = 21;
|
||||
}
|
||||
}
|
||||
|
||||
// Messages that the server can send to the client.
|
||||
message ServerMessage {
|
||||
oneof msg {
|
||||
// node.proto
|
||||
EvalFailedMessage eval_failed = 13;
|
||||
EvalDoneMessage eval_done = 14;
|
||||
EvalEventMessage eval_event = 15;
|
||||
Method.Fail fail = 13;
|
||||
Method.Success success = 14;
|
||||
Event event = 19;
|
||||
Callback callback = 22;
|
||||
Pong pong = 18;
|
||||
|
||||
WorkingInitMessage init = 16;
|
||||
WorkingInit init = 16;
|
||||
|
||||
// vscode.proto
|
||||
SharedProcessActiveMessage shared_process_active = 17;
|
||||
|
||||
Pong pong = 18;
|
||||
SharedProcessActive shared_process_active = 17;
|
||||
}
|
||||
}
|
||||
|
||||
message WorkingInitMessage {
|
||||
message WorkingInit {
|
||||
string home_directory = 1;
|
||||
string tmp_directory = 2;
|
||||
string data_directory = 3;
|
||||
@@ -41,4 +41,5 @@ message WorkingInitMessage {
|
||||
OperatingSystem operating_system = 5;
|
||||
string shell = 6;
|
||||
string builtin_extensions_dir = 7;
|
||||
string extensions_directory = 8;
|
||||
}
|
||||
|
||||
112
packages/protocol/src/proto/client_pb.d.ts
vendored
112
packages/protocol/src/proto/client_pb.d.ts
vendored
@@ -6,15 +6,10 @@ import * as node_pb from "./node_pb";
|
||||
import * as vscode_pb from "./vscode_pb";
|
||||
|
||||
export class ClientMessage extends jspb.Message {
|
||||
hasNewEval(): boolean;
|
||||
clearNewEval(): void;
|
||||
getNewEval(): node_pb.NewEvalMessage | undefined;
|
||||
setNewEval(value?: node_pb.NewEvalMessage): void;
|
||||
|
||||
hasEvalEvent(): boolean;
|
||||
clearEvalEvent(): void;
|
||||
getEvalEvent(): node_pb.EvalEventMessage | undefined;
|
||||
setEvalEvent(value?: node_pb.EvalEventMessage): void;
|
||||
hasMethod(): boolean;
|
||||
clearMethod(): void;
|
||||
getMethod(): node_pb.Method | undefined;
|
||||
setMethod(value?: node_pb.Method): void;
|
||||
|
||||
hasPing(): boolean;
|
||||
clearPing(): void;
|
||||
@@ -34,50 +29,53 @@ export class ClientMessage extends jspb.Message {
|
||||
|
||||
export namespace ClientMessage {
|
||||
export type AsObject = {
|
||||
newEval?: node_pb.NewEvalMessage.AsObject,
|
||||
evalEvent?: node_pb.EvalEventMessage.AsObject,
|
||||
method?: node_pb.Method.AsObject,
|
||||
ping?: node_pb.Ping.AsObject,
|
||||
}
|
||||
|
||||
export enum MsgCase {
|
||||
MSG_NOT_SET = 0,
|
||||
NEW_EVAL = 11,
|
||||
EVAL_EVENT = 12,
|
||||
PING = 13,
|
||||
METHOD = 20,
|
||||
PING = 21,
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerMessage extends jspb.Message {
|
||||
hasEvalFailed(): boolean;
|
||||
clearEvalFailed(): void;
|
||||
getEvalFailed(): node_pb.EvalFailedMessage | undefined;
|
||||
setEvalFailed(value?: node_pb.EvalFailedMessage): void;
|
||||
hasFail(): boolean;
|
||||
clearFail(): void;
|
||||
getFail(): node_pb.Method.Fail | undefined;
|
||||
setFail(value?: node_pb.Method.Fail): void;
|
||||
|
||||
hasEvalDone(): boolean;
|
||||
clearEvalDone(): void;
|
||||
getEvalDone(): node_pb.EvalDoneMessage | undefined;
|
||||
setEvalDone(value?: node_pb.EvalDoneMessage): void;
|
||||
hasSuccess(): boolean;
|
||||
clearSuccess(): void;
|
||||
getSuccess(): node_pb.Method.Success | undefined;
|
||||
setSuccess(value?: node_pb.Method.Success): void;
|
||||
|
||||
hasEvalEvent(): boolean;
|
||||
clearEvalEvent(): void;
|
||||
getEvalEvent(): node_pb.EvalEventMessage | undefined;
|
||||
setEvalEvent(value?: node_pb.EvalEventMessage): void;
|
||||
hasEvent(): boolean;
|
||||
clearEvent(): void;
|
||||
getEvent(): node_pb.Event | undefined;
|
||||
setEvent(value?: node_pb.Event): void;
|
||||
|
||||
hasInit(): boolean;
|
||||
clearInit(): void;
|
||||
getInit(): WorkingInitMessage | undefined;
|
||||
setInit(value?: WorkingInitMessage): void;
|
||||
|
||||
hasSharedProcessActive(): boolean;
|
||||
clearSharedProcessActive(): void;
|
||||
getSharedProcessActive(): vscode_pb.SharedProcessActiveMessage | undefined;
|
||||
setSharedProcessActive(value?: vscode_pb.SharedProcessActiveMessage): void;
|
||||
hasCallback(): boolean;
|
||||
clearCallback(): void;
|
||||
getCallback(): node_pb.Callback | undefined;
|
||||
setCallback(value?: node_pb.Callback): void;
|
||||
|
||||
hasPong(): boolean;
|
||||
clearPong(): void;
|
||||
getPong(): node_pb.Pong | undefined;
|
||||
setPong(value?: node_pb.Pong): void;
|
||||
|
||||
hasInit(): boolean;
|
||||
clearInit(): void;
|
||||
getInit(): WorkingInit | undefined;
|
||||
setInit(value?: WorkingInit): void;
|
||||
|
||||
hasSharedProcessActive(): boolean;
|
||||
clearSharedProcessActive(): void;
|
||||
getSharedProcessActive(): vscode_pb.SharedProcessActive | undefined;
|
||||
setSharedProcessActive(value?: vscode_pb.SharedProcessActive): void;
|
||||
|
||||
getMsgCase(): ServerMessage.MsgCase;
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): ServerMessage.AsObject;
|
||||
@@ -91,26 +89,28 @@ export class ServerMessage extends jspb.Message {
|
||||
|
||||
export namespace ServerMessage {
|
||||
export type AsObject = {
|
||||
evalFailed?: node_pb.EvalFailedMessage.AsObject,
|
||||
evalDone?: node_pb.EvalDoneMessage.AsObject,
|
||||
evalEvent?: node_pb.EvalEventMessage.AsObject,
|
||||
init?: WorkingInitMessage.AsObject,
|
||||
sharedProcessActive?: vscode_pb.SharedProcessActiveMessage.AsObject,
|
||||
fail?: node_pb.Method.Fail.AsObject,
|
||||
success?: node_pb.Method.Success.AsObject,
|
||||
event?: node_pb.Event.AsObject,
|
||||
callback?: node_pb.Callback.AsObject,
|
||||
pong?: node_pb.Pong.AsObject,
|
||||
init?: WorkingInit.AsObject,
|
||||
sharedProcessActive?: vscode_pb.SharedProcessActive.AsObject,
|
||||
}
|
||||
|
||||
export enum MsgCase {
|
||||
MSG_NOT_SET = 0,
|
||||
EVAL_FAILED = 13,
|
||||
EVAL_DONE = 14,
|
||||
EVAL_EVENT = 15,
|
||||
FAIL = 13,
|
||||
SUCCESS = 14,
|
||||
EVENT = 19,
|
||||
CALLBACK = 22,
|
||||
PONG = 18,
|
||||
INIT = 16,
|
||||
SHARED_PROCESS_ACTIVE = 17,
|
||||
PONG = 18,
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkingInitMessage extends jspb.Message {
|
||||
export class WorkingInit extends jspb.Message {
|
||||
getHomeDirectory(): string;
|
||||
setHomeDirectory(value: string): void;
|
||||
|
||||
@@ -123,8 +123,8 @@ export class WorkingInitMessage extends jspb.Message {
|
||||
getWorkingDirectory(): string;
|
||||
setWorkingDirectory(value: string): void;
|
||||
|
||||
getOperatingSystem(): WorkingInitMessage.OperatingSystem;
|
||||
setOperatingSystem(value: WorkingInitMessage.OperatingSystem): void;
|
||||
getOperatingSystem(): WorkingInit.OperatingSystem;
|
||||
setOperatingSystem(value: WorkingInit.OperatingSystem): void;
|
||||
|
||||
getShell(): string;
|
||||
setShell(value: string): void;
|
||||
@@ -132,25 +132,29 @@ export class WorkingInitMessage extends jspb.Message {
|
||||
getBuiltinExtensionsDir(): string;
|
||||
setBuiltinExtensionsDir(value: string): void;
|
||||
|
||||
getExtensionsDirectory(): string;
|
||||
setExtensionsDirectory(value: string): void;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): WorkingInitMessage.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: WorkingInitMessage): WorkingInitMessage.AsObject;
|
||||
toObject(includeInstance?: boolean): WorkingInit.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: WorkingInit): WorkingInit.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: WorkingInitMessage, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): WorkingInitMessage;
|
||||
static deserializeBinaryFromReader(message: WorkingInitMessage, reader: jspb.BinaryReader): WorkingInitMessage;
|
||||
static serializeBinaryToWriter(message: WorkingInit, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): WorkingInit;
|
||||
static deserializeBinaryFromReader(message: WorkingInit, reader: jspb.BinaryReader): WorkingInit;
|
||||
}
|
||||
|
||||
export namespace WorkingInitMessage {
|
||||
export namespace WorkingInit {
|
||||
export type AsObject = {
|
||||
homeDirectory: string,
|
||||
tmpDirectory: string,
|
||||
dataDirectory: string,
|
||||
workingDirectory: string,
|
||||
operatingSystem: WorkingInitMessage.OperatingSystem,
|
||||
operatingSystem: WorkingInit.OperatingSystem,
|
||||
shell: string,
|
||||
builtinExtensionsDir: string,
|
||||
extensionsDirectory: string,
|
||||
}
|
||||
|
||||
export enum OperatingSystem {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,136 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message NewEvalMessage {
|
||||
uint64 id = 1;
|
||||
string function = 2;
|
||||
repeated string args = 3;
|
||||
// Timeout in ms
|
||||
uint32 timeout = 4;
|
||||
// Create active eval message.
|
||||
// Allows for dynamic communication for an eval
|
||||
bool active = 5;
|
||||
enum Module {
|
||||
ChildProcess = 0;
|
||||
Fs = 1;
|
||||
Net = 2;
|
||||
NodePty = 3;
|
||||
Spdlog = 4;
|
||||
Trash = 5;
|
||||
}
|
||||
|
||||
message EvalEventMessage {
|
||||
uint64 id = 1;
|
||||
string event = 2;
|
||||
repeated string args = 3;
|
||||
message Argument {
|
||||
message ErrorValue {
|
||||
string message = 1;
|
||||
string stack = 2;
|
||||
string code = 3;
|
||||
}
|
||||
|
||||
message BufferValue {
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
message ObjectValue {
|
||||
map<string, Argument> data = 1;
|
||||
}
|
||||
|
||||
message ArrayValue {
|
||||
repeated Argument data = 1;
|
||||
}
|
||||
|
||||
message ProxyValue {
|
||||
uint64 id = 1;
|
||||
}
|
||||
|
||||
message FunctionValue {
|
||||
uint64 id = 1;
|
||||
}
|
||||
|
||||
message NullValue {}
|
||||
|
||||
message UndefinedValue {}
|
||||
|
||||
oneof msg {
|
||||
ErrorValue error = 1;
|
||||
BufferValue buffer = 2;
|
||||
ObjectValue object = 3;
|
||||
ArrayValue array = 4;
|
||||
ProxyValue proxy = 5;
|
||||
FunctionValue function = 6;
|
||||
NullValue null = 7;
|
||||
UndefinedValue undefined = 8;
|
||||
double number = 9;
|
||||
string string = 10;
|
||||
bool boolean = 11;
|
||||
}
|
||||
}
|
||||
|
||||
message EvalFailedMessage {
|
||||
uint64 id = 1;
|
||||
string response = 2;
|
||||
// Call a remote method.
|
||||
message Method {
|
||||
// A proxy identified by a unique name like "fs".
|
||||
message Named {
|
||||
uint64 id = 1;
|
||||
Module module = 2;
|
||||
string method = 3;
|
||||
repeated Argument args = 4;
|
||||
}
|
||||
|
||||
// A general proxy identified by an ID like WriteStream.
|
||||
message Numbered {
|
||||
uint64 id = 1;
|
||||
uint64 proxy_id = 2;
|
||||
string method = 3;
|
||||
repeated Argument args = 4;
|
||||
}
|
||||
|
||||
// Remote method failed.
|
||||
message Fail {
|
||||
uint64 id = 1;
|
||||
Argument response = 2;
|
||||
}
|
||||
|
||||
// Remote method succeeded.
|
||||
message Success {
|
||||
uint64 id = 1;
|
||||
Argument response = 2;
|
||||
}
|
||||
|
||||
oneof msg {
|
||||
Method.Named named_proxy = 1;
|
||||
Method.Numbered numbered_proxy = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message EvalDoneMessage {
|
||||
uint64 id = 1;
|
||||
string response = 2;
|
||||
message Callback {
|
||||
// A remote callback for uniquely named proxy.
|
||||
message Named {
|
||||
Module module = 1;
|
||||
uint64 callback_id = 2;
|
||||
repeated Argument args = 3;
|
||||
}
|
||||
|
||||
// A remote callback for a numbered proxy.
|
||||
message Numbered {
|
||||
uint64 proxy_id = 1;
|
||||
uint64 callback_id = 2;
|
||||
repeated Argument args = 3;
|
||||
}
|
||||
|
||||
oneof msg {
|
||||
Callback.Named named_callback = 1;
|
||||
Callback.Numbered numbered_callback = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message Event {
|
||||
// Emit an event on a uniquely named proxy.
|
||||
message Named {
|
||||
Module module = 1;
|
||||
string event = 2;
|
||||
repeated Argument args = 3;
|
||||
}
|
||||
|
||||
// Emit an event on a numbered proxy.
|
||||
message Numbered {
|
||||
uint64 proxy_id = 1;
|
||||
string event = 2;
|
||||
repeated Argument args = 3;
|
||||
}
|
||||
|
||||
oneof msg {
|
||||
Event.Named named_event = 1;
|
||||
Event.Numbered numbered_event = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message Ping {}
|
||||
|
||||
637
packages/protocol/src/proto/node_pb.d.ts
vendored
637
packages/protocol/src/proto/node_pb.d.ts
vendored
@@ -3,119 +3,609 @@
|
||||
|
||||
import * as jspb from "google-protobuf";
|
||||
|
||||
export class NewEvalMessage extends jspb.Message {
|
||||
getId(): number;
|
||||
setId(value: number): void;
|
||||
export class Argument extends jspb.Message {
|
||||
hasError(): boolean;
|
||||
clearError(): void;
|
||||
getError(): Argument.ErrorValue | undefined;
|
||||
setError(value?: Argument.ErrorValue): void;
|
||||
|
||||
getFunction(): string;
|
||||
setFunction(value: string): void;
|
||||
hasBuffer(): boolean;
|
||||
clearBuffer(): void;
|
||||
getBuffer(): Argument.BufferValue | undefined;
|
||||
setBuffer(value?: Argument.BufferValue): void;
|
||||
|
||||
clearArgsList(): void;
|
||||
getArgsList(): Array<string>;
|
||||
setArgsList(value: Array<string>): void;
|
||||
addArgs(value: string, index?: number): string;
|
||||
hasObject(): boolean;
|
||||
clearObject(): void;
|
||||
getObject(): Argument.ObjectValue | undefined;
|
||||
setObject(value?: Argument.ObjectValue): void;
|
||||
|
||||
getTimeout(): number;
|
||||
setTimeout(value: number): void;
|
||||
hasArray(): boolean;
|
||||
clearArray(): void;
|
||||
getArray(): Argument.ArrayValue | undefined;
|
||||
setArray(value?: Argument.ArrayValue): void;
|
||||
|
||||
getActive(): boolean;
|
||||
setActive(value: boolean): void;
|
||||
hasProxy(): boolean;
|
||||
clearProxy(): void;
|
||||
getProxy(): Argument.ProxyValue | undefined;
|
||||
setProxy(value?: Argument.ProxyValue): void;
|
||||
|
||||
hasFunction(): boolean;
|
||||
clearFunction(): void;
|
||||
getFunction(): Argument.FunctionValue | undefined;
|
||||
setFunction(value?: Argument.FunctionValue): void;
|
||||
|
||||
hasNull(): boolean;
|
||||
clearNull(): void;
|
||||
getNull(): Argument.NullValue | undefined;
|
||||
setNull(value?: Argument.NullValue): void;
|
||||
|
||||
hasUndefined(): boolean;
|
||||
clearUndefined(): void;
|
||||
getUndefined(): Argument.UndefinedValue | undefined;
|
||||
setUndefined(value?: Argument.UndefinedValue): void;
|
||||
|
||||
hasNumber(): boolean;
|
||||
clearNumber(): void;
|
||||
getNumber(): number;
|
||||
setNumber(value: number): void;
|
||||
|
||||
hasString(): boolean;
|
||||
clearString(): void;
|
||||
getString(): string;
|
||||
setString(value: string): void;
|
||||
|
||||
hasBoolean(): boolean;
|
||||
clearBoolean(): void;
|
||||
getBoolean(): boolean;
|
||||
setBoolean(value: boolean): void;
|
||||
|
||||
getMsgCase(): Argument.MsgCase;
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): NewEvalMessage.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: NewEvalMessage): NewEvalMessage.AsObject;
|
||||
toObject(includeInstance?: boolean): Argument.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Argument): Argument.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: NewEvalMessage, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): NewEvalMessage;
|
||||
static deserializeBinaryFromReader(message: NewEvalMessage, reader: jspb.BinaryReader): NewEvalMessage;
|
||||
static serializeBinaryToWriter(message: Argument, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Argument;
|
||||
static deserializeBinaryFromReader(message: Argument, reader: jspb.BinaryReader): Argument;
|
||||
}
|
||||
|
||||
export namespace NewEvalMessage {
|
||||
export namespace Argument {
|
||||
export type AsObject = {
|
||||
id: number,
|
||||
pb_function: string,
|
||||
argsList: Array<string>,
|
||||
timeout: number,
|
||||
active: boolean,
|
||||
error?: Argument.ErrorValue.AsObject,
|
||||
buffer?: Argument.BufferValue.AsObject,
|
||||
object?: Argument.ObjectValue.AsObject,
|
||||
array?: Argument.ArrayValue.AsObject,
|
||||
proxy?: Argument.ProxyValue.AsObject,
|
||||
pb_function?: Argument.FunctionValue.AsObject,
|
||||
pb_null?: Argument.NullValue.AsObject,
|
||||
undefined?: Argument.UndefinedValue.AsObject,
|
||||
number: number,
|
||||
string: string,
|
||||
pb_boolean: boolean,
|
||||
}
|
||||
|
||||
export class ErrorValue extends jspb.Message {
|
||||
getMessage(): string;
|
||||
setMessage(value: string): void;
|
||||
|
||||
getStack(): string;
|
||||
setStack(value: string): void;
|
||||
|
||||
getCode(): string;
|
||||
setCode(value: string): void;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): ErrorValue.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: ErrorValue): ErrorValue.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: ErrorValue, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): ErrorValue;
|
||||
static deserializeBinaryFromReader(message: ErrorValue, reader: jspb.BinaryReader): ErrorValue;
|
||||
}
|
||||
|
||||
export namespace ErrorValue {
|
||||
export type AsObject = {
|
||||
message: string,
|
||||
stack: string,
|
||||
code: string,
|
||||
}
|
||||
}
|
||||
|
||||
export class BufferValue extends jspb.Message {
|
||||
getData(): Uint8Array | string;
|
||||
getData_asU8(): Uint8Array;
|
||||
getData_asB64(): string;
|
||||
setData(value: Uint8Array | string): void;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): BufferValue.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: BufferValue): BufferValue.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: BufferValue, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): BufferValue;
|
||||
static deserializeBinaryFromReader(message: BufferValue, reader: jspb.BinaryReader): BufferValue;
|
||||
}
|
||||
|
||||
export namespace BufferValue {
|
||||
export type AsObject = {
|
||||
data: Uint8Array | string,
|
||||
}
|
||||
}
|
||||
|
||||
export class ObjectValue extends jspb.Message {
|
||||
getDataMap(): jspb.Map<string, Argument>;
|
||||
clearDataMap(): void;
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): ObjectValue.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: ObjectValue): ObjectValue.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: ObjectValue, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): ObjectValue;
|
||||
static deserializeBinaryFromReader(message: ObjectValue, reader: jspb.BinaryReader): ObjectValue;
|
||||
}
|
||||
|
||||
export namespace ObjectValue {
|
||||
export type AsObject = {
|
||||
dataMap: Array<[string, Argument.AsObject]>,
|
||||
}
|
||||
}
|
||||
|
||||
export class ArrayValue extends jspb.Message {
|
||||
clearDataList(): void;
|
||||
getDataList(): Array<Argument>;
|
||||
setDataList(value: Array<Argument>): void;
|
||||
addData(value?: Argument, index?: number): Argument;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): ArrayValue.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: ArrayValue): ArrayValue.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: ArrayValue, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): ArrayValue;
|
||||
static deserializeBinaryFromReader(message: ArrayValue, reader: jspb.BinaryReader): ArrayValue;
|
||||
}
|
||||
|
||||
export namespace ArrayValue {
|
||||
export type AsObject = {
|
||||
dataList: Array<Argument.AsObject>,
|
||||
}
|
||||
}
|
||||
|
||||
export class ProxyValue extends jspb.Message {
|
||||
getId(): number;
|
||||
setId(value: number): void;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): ProxyValue.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: ProxyValue): ProxyValue.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: ProxyValue, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): ProxyValue;
|
||||
static deserializeBinaryFromReader(message: ProxyValue, reader: jspb.BinaryReader): ProxyValue;
|
||||
}
|
||||
|
||||
export namespace ProxyValue {
|
||||
export type AsObject = {
|
||||
id: number,
|
||||
}
|
||||
}
|
||||
|
||||
export class FunctionValue extends jspb.Message {
|
||||
getId(): number;
|
||||
setId(value: number): void;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): FunctionValue.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: FunctionValue): FunctionValue.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: FunctionValue, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): FunctionValue;
|
||||
static deserializeBinaryFromReader(message: FunctionValue, reader: jspb.BinaryReader): FunctionValue;
|
||||
}
|
||||
|
||||
export namespace FunctionValue {
|
||||
export type AsObject = {
|
||||
id: number,
|
||||
}
|
||||
}
|
||||
|
||||
export class NullValue extends jspb.Message {
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): NullValue.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: NullValue): NullValue.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: NullValue, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): NullValue;
|
||||
static deserializeBinaryFromReader(message: NullValue, reader: jspb.BinaryReader): NullValue;
|
||||
}
|
||||
|
||||
export namespace NullValue {
|
||||
export type AsObject = {
|
||||
}
|
||||
}
|
||||
|
||||
export class UndefinedValue extends jspb.Message {
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): UndefinedValue.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: UndefinedValue): UndefinedValue.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: UndefinedValue, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): UndefinedValue;
|
||||
static deserializeBinaryFromReader(message: UndefinedValue, reader: jspb.BinaryReader): UndefinedValue;
|
||||
}
|
||||
|
||||
export namespace UndefinedValue {
|
||||
export type AsObject = {
|
||||
}
|
||||
}
|
||||
|
||||
export enum MsgCase {
|
||||
MSG_NOT_SET = 0,
|
||||
ERROR = 1,
|
||||
BUFFER = 2,
|
||||
OBJECT = 3,
|
||||
ARRAY = 4,
|
||||
PROXY = 5,
|
||||
FUNCTION = 6,
|
||||
NULL = 7,
|
||||
UNDEFINED = 8,
|
||||
NUMBER = 9,
|
||||
STRING = 10,
|
||||
BOOLEAN = 11,
|
||||
}
|
||||
}
|
||||
|
||||
export class EvalEventMessage extends jspb.Message {
|
||||
getId(): number;
|
||||
setId(value: number): void;
|
||||
export class Method extends jspb.Message {
|
||||
hasNamedProxy(): boolean;
|
||||
clearNamedProxy(): void;
|
||||
getNamedProxy(): Method.Named | undefined;
|
||||
setNamedProxy(value?: Method.Named): void;
|
||||
|
||||
getEvent(): string;
|
||||
setEvent(value: string): void;
|
||||
|
||||
clearArgsList(): void;
|
||||
getArgsList(): Array<string>;
|
||||
setArgsList(value: Array<string>): void;
|
||||
addArgs(value: string, index?: number): string;
|
||||
hasNumberedProxy(): boolean;
|
||||
clearNumberedProxy(): void;
|
||||
getNumberedProxy(): Method.Numbered | undefined;
|
||||
setNumberedProxy(value?: Method.Numbered): void;
|
||||
|
||||
getMsgCase(): Method.MsgCase;
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): EvalEventMessage.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: EvalEventMessage): EvalEventMessage.AsObject;
|
||||
toObject(includeInstance?: boolean): Method.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Method): Method.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: EvalEventMessage, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): EvalEventMessage;
|
||||
static deserializeBinaryFromReader(message: EvalEventMessage, reader: jspb.BinaryReader): EvalEventMessage;
|
||||
static serializeBinaryToWriter(message: Method, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Method;
|
||||
static deserializeBinaryFromReader(message: Method, reader: jspb.BinaryReader): Method;
|
||||
}
|
||||
|
||||
export namespace EvalEventMessage {
|
||||
export namespace Method {
|
||||
export type AsObject = {
|
||||
id: number,
|
||||
event: string,
|
||||
argsList: Array<string>,
|
||||
namedProxy?: Method.Named.AsObject,
|
||||
numberedProxy?: Method.Numbered.AsObject,
|
||||
}
|
||||
|
||||
export class Named extends jspb.Message {
|
||||
getId(): number;
|
||||
setId(value: number): void;
|
||||
|
||||
getModule(): Module;
|
||||
setModule(value: Module): void;
|
||||
|
||||
getMethod(): string;
|
||||
setMethod(value: string): void;
|
||||
|
||||
clearArgsList(): void;
|
||||
getArgsList(): Array<Argument>;
|
||||
setArgsList(value: Array<Argument>): void;
|
||||
addArgs(value?: Argument, index?: number): Argument;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): Named.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Named): Named.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: Named, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Named;
|
||||
static deserializeBinaryFromReader(message: Named, reader: jspb.BinaryReader): Named;
|
||||
}
|
||||
|
||||
export namespace Named {
|
||||
export type AsObject = {
|
||||
id: number,
|
||||
module: Module,
|
||||
method: string,
|
||||
argsList: Array<Argument.AsObject>,
|
||||
}
|
||||
}
|
||||
|
||||
export class Numbered extends jspb.Message {
|
||||
getId(): number;
|
||||
setId(value: number): void;
|
||||
|
||||
getProxyId(): number;
|
||||
setProxyId(value: number): void;
|
||||
|
||||
getMethod(): string;
|
||||
setMethod(value: string): void;
|
||||
|
||||
clearArgsList(): void;
|
||||
getArgsList(): Array<Argument>;
|
||||
setArgsList(value: Array<Argument>): void;
|
||||
addArgs(value?: Argument, index?: number): Argument;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): Numbered.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Numbered): Numbered.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: Numbered, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Numbered;
|
||||
static deserializeBinaryFromReader(message: Numbered, reader: jspb.BinaryReader): Numbered;
|
||||
}
|
||||
|
||||
export namespace Numbered {
|
||||
export type AsObject = {
|
||||
id: number,
|
||||
proxyId: number,
|
||||
method: string,
|
||||
argsList: Array<Argument.AsObject>,
|
||||
}
|
||||
}
|
||||
|
||||
export class Fail extends jspb.Message {
|
||||
getId(): number;
|
||||
setId(value: number): void;
|
||||
|
||||
hasResponse(): boolean;
|
||||
clearResponse(): void;
|
||||
getResponse(): Argument | undefined;
|
||||
setResponse(value?: Argument): void;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): Fail.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Fail): Fail.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: Fail, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Fail;
|
||||
static deserializeBinaryFromReader(message: Fail, reader: jspb.BinaryReader): Fail;
|
||||
}
|
||||
|
||||
export namespace Fail {
|
||||
export type AsObject = {
|
||||
id: number,
|
||||
response?: Argument.AsObject,
|
||||
}
|
||||
}
|
||||
|
||||
export class Success extends jspb.Message {
|
||||
getId(): number;
|
||||
setId(value: number): void;
|
||||
|
||||
hasResponse(): boolean;
|
||||
clearResponse(): void;
|
||||
getResponse(): Argument | undefined;
|
||||
setResponse(value?: Argument): void;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): Success.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Success): Success.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: Success, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Success;
|
||||
static deserializeBinaryFromReader(message: Success, reader: jspb.BinaryReader): Success;
|
||||
}
|
||||
|
||||
export namespace Success {
|
||||
export type AsObject = {
|
||||
id: number,
|
||||
response?: Argument.AsObject,
|
||||
}
|
||||
}
|
||||
|
||||
export enum MsgCase {
|
||||
MSG_NOT_SET = 0,
|
||||
NAMED_PROXY = 1,
|
||||
NUMBERED_PROXY = 2,
|
||||
}
|
||||
}
|
||||
|
||||
export class EvalFailedMessage extends jspb.Message {
|
||||
getId(): number;
|
||||
setId(value: number): void;
|
||||
export class Callback extends jspb.Message {
|
||||
hasNamedCallback(): boolean;
|
||||
clearNamedCallback(): void;
|
||||
getNamedCallback(): Callback.Named | undefined;
|
||||
setNamedCallback(value?: Callback.Named): void;
|
||||
|
||||
getResponse(): string;
|
||||
setResponse(value: string): void;
|
||||
hasNumberedCallback(): boolean;
|
||||
clearNumberedCallback(): void;
|
||||
getNumberedCallback(): Callback.Numbered | undefined;
|
||||
setNumberedCallback(value?: Callback.Numbered): void;
|
||||
|
||||
getMsgCase(): Callback.MsgCase;
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): EvalFailedMessage.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: EvalFailedMessage): EvalFailedMessage.AsObject;
|
||||
toObject(includeInstance?: boolean): Callback.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Callback): Callback.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: EvalFailedMessage, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): EvalFailedMessage;
|
||||
static deserializeBinaryFromReader(message: EvalFailedMessage, reader: jspb.BinaryReader): EvalFailedMessage;
|
||||
static serializeBinaryToWriter(message: Callback, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Callback;
|
||||
static deserializeBinaryFromReader(message: Callback, reader: jspb.BinaryReader): Callback;
|
||||
}
|
||||
|
||||
export namespace EvalFailedMessage {
|
||||
export namespace Callback {
|
||||
export type AsObject = {
|
||||
id: number,
|
||||
response: string,
|
||||
namedCallback?: Callback.Named.AsObject,
|
||||
numberedCallback?: Callback.Numbered.AsObject,
|
||||
}
|
||||
|
||||
export class Named extends jspb.Message {
|
||||
getModule(): Module;
|
||||
setModule(value: Module): void;
|
||||
|
||||
getCallbackId(): number;
|
||||
setCallbackId(value: number): void;
|
||||
|
||||
clearArgsList(): void;
|
||||
getArgsList(): Array<Argument>;
|
||||
setArgsList(value: Array<Argument>): void;
|
||||
addArgs(value?: Argument, index?: number): Argument;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): Named.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Named): Named.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: Named, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Named;
|
||||
static deserializeBinaryFromReader(message: Named, reader: jspb.BinaryReader): Named;
|
||||
}
|
||||
|
||||
export namespace Named {
|
||||
export type AsObject = {
|
||||
module: Module,
|
||||
callbackId: number,
|
||||
argsList: Array<Argument.AsObject>,
|
||||
}
|
||||
}
|
||||
|
||||
export class Numbered extends jspb.Message {
|
||||
getProxyId(): number;
|
||||
setProxyId(value: number): void;
|
||||
|
||||
getCallbackId(): number;
|
||||
setCallbackId(value: number): void;
|
||||
|
||||
clearArgsList(): void;
|
||||
getArgsList(): Array<Argument>;
|
||||
setArgsList(value: Array<Argument>): void;
|
||||
addArgs(value?: Argument, index?: number): Argument;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): Numbered.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Numbered): Numbered.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: Numbered, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Numbered;
|
||||
static deserializeBinaryFromReader(message: Numbered, reader: jspb.BinaryReader): Numbered;
|
||||
}
|
||||
|
||||
export namespace Numbered {
|
||||
export type AsObject = {
|
||||
proxyId: number,
|
||||
callbackId: number,
|
||||
argsList: Array<Argument.AsObject>,
|
||||
}
|
||||
}
|
||||
|
||||
export enum MsgCase {
|
||||
MSG_NOT_SET = 0,
|
||||
NAMED_CALLBACK = 1,
|
||||
NUMBERED_CALLBACK = 2,
|
||||
}
|
||||
}
|
||||
|
||||
export class EvalDoneMessage extends jspb.Message {
|
||||
getId(): number;
|
||||
setId(value: number): void;
|
||||
export class Event extends jspb.Message {
|
||||
hasNamedEvent(): boolean;
|
||||
clearNamedEvent(): void;
|
||||
getNamedEvent(): Event.Named | undefined;
|
||||
setNamedEvent(value?: Event.Named): void;
|
||||
|
||||
getResponse(): string;
|
||||
setResponse(value: string): void;
|
||||
hasNumberedEvent(): boolean;
|
||||
clearNumberedEvent(): void;
|
||||
getNumberedEvent(): Event.Numbered | undefined;
|
||||
setNumberedEvent(value?: Event.Numbered): void;
|
||||
|
||||
getMsgCase(): Event.MsgCase;
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): EvalDoneMessage.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: EvalDoneMessage): EvalDoneMessage.AsObject;
|
||||
toObject(includeInstance?: boolean): Event.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Event): Event.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: EvalDoneMessage, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): EvalDoneMessage;
|
||||
static deserializeBinaryFromReader(message: EvalDoneMessage, reader: jspb.BinaryReader): EvalDoneMessage;
|
||||
static serializeBinaryToWriter(message: Event, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Event;
|
||||
static deserializeBinaryFromReader(message: Event, reader: jspb.BinaryReader): Event;
|
||||
}
|
||||
|
||||
export namespace EvalDoneMessage {
|
||||
export namespace Event {
|
||||
export type AsObject = {
|
||||
id: number,
|
||||
response: string,
|
||||
namedEvent?: Event.Named.AsObject,
|
||||
numberedEvent?: Event.Numbered.AsObject,
|
||||
}
|
||||
|
||||
export class Named extends jspb.Message {
|
||||
getModule(): Module;
|
||||
setModule(value: Module): void;
|
||||
|
||||
getEvent(): string;
|
||||
setEvent(value: string): void;
|
||||
|
||||
clearArgsList(): void;
|
||||
getArgsList(): Array<Argument>;
|
||||
setArgsList(value: Array<Argument>): void;
|
||||
addArgs(value?: Argument, index?: number): Argument;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): Named.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Named): Named.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: Named, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Named;
|
||||
static deserializeBinaryFromReader(message: Named, reader: jspb.BinaryReader): Named;
|
||||
}
|
||||
|
||||
export namespace Named {
|
||||
export type AsObject = {
|
||||
module: Module,
|
||||
event: string,
|
||||
argsList: Array<Argument.AsObject>,
|
||||
}
|
||||
}
|
||||
|
||||
export class Numbered extends jspb.Message {
|
||||
getProxyId(): number;
|
||||
setProxyId(value: number): void;
|
||||
|
||||
getEvent(): string;
|
||||
setEvent(value: string): void;
|
||||
|
||||
clearArgsList(): void;
|
||||
getArgsList(): Array<Argument>;
|
||||
setArgsList(value: Array<Argument>): void;
|
||||
addArgs(value?: Argument, index?: number): Argument;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): Numbered.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Numbered): Numbered.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: Numbered, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Numbered;
|
||||
static deserializeBinaryFromReader(message: Numbered, reader: jspb.BinaryReader): Numbered;
|
||||
}
|
||||
|
||||
export namespace Numbered {
|
||||
export type AsObject = {
|
||||
proxyId: number,
|
||||
event: string,
|
||||
argsList: Array<Argument.AsObject>,
|
||||
}
|
||||
}
|
||||
|
||||
export enum MsgCase {
|
||||
MSG_NOT_SET = 0,
|
||||
NAMED_EVENT = 1,
|
||||
NUMBERED_EVENT = 2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,3 +641,12 @@ export namespace Pong {
|
||||
}
|
||||
}
|
||||
|
||||
export enum Module {
|
||||
CHILDPROCESS = 0,
|
||||
FS = 1,
|
||||
NET = 2,
|
||||
NODEPTY = 3,
|
||||
SPDLOG = 4,
|
||||
TRASH = 5,
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
// Sent when a shared process becomes active
|
||||
message SharedProcessActiveMessage {
|
||||
message SharedProcessActive {
|
||||
string socket_path = 1;
|
||||
string log_path = 2;
|
||||
}
|
||||
}
|
||||
|
||||
14
packages/protocol/src/proto/vscode_pb.d.ts
vendored
14
packages/protocol/src/proto/vscode_pb.d.ts
vendored
@@ -3,7 +3,7 @@
|
||||
|
||||
import * as jspb from "google-protobuf";
|
||||
|
||||
export class SharedProcessActiveMessage extends jspb.Message {
|
||||
export class SharedProcessActive extends jspb.Message {
|
||||
getSocketPath(): string;
|
||||
setSocketPath(value: string): void;
|
||||
|
||||
@@ -11,16 +11,16 @@ export class SharedProcessActiveMessage extends jspb.Message {
|
||||
setLogPath(value: string): void;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): SharedProcessActiveMessage.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: SharedProcessActiveMessage): SharedProcessActiveMessage.AsObject;
|
||||
toObject(includeInstance?: boolean): SharedProcessActive.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: SharedProcessActive): SharedProcessActive.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: SharedProcessActiveMessage, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): SharedProcessActiveMessage;
|
||||
static deserializeBinaryFromReader(message: SharedProcessActiveMessage, reader: jspb.BinaryReader): SharedProcessActiveMessage;
|
||||
static serializeBinaryToWriter(message: SharedProcessActive, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): SharedProcessActive;
|
||||
static deserializeBinaryFromReader(message: SharedProcessActive, reader: jspb.BinaryReader): SharedProcessActive;
|
||||
}
|
||||
|
||||
export namespace SharedProcessActiveMessage {
|
||||
export namespace SharedProcessActive {
|
||||
export type AsObject = {
|
||||
socketPath: string,
|
||||
logPath: string,
|
||||
|
||||
@@ -11,8 +11,7 @@ var jspb = require('google-protobuf');
|
||||
var goog = jspb;
|
||||
var global = Function('return this')();
|
||||
|
||||
goog.exportSymbol('proto.SharedProcessActiveMessage', null, global);
|
||||
|
||||
goog.exportSymbol('proto.SharedProcessActive', null, global);
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
@@ -23,15 +22,20 @@ goog.exportSymbol('proto.SharedProcessActiveMessage', null, global);
|
||||
* @extends {jspb.Message}
|
||||
* @constructor
|
||||
*/
|
||||
proto.SharedProcessActiveMessage = function(opt_data) {
|
||||
proto.SharedProcessActive = function(opt_data) {
|
||||
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
|
||||
};
|
||||
goog.inherits(proto.SharedProcessActiveMessage, jspb.Message);
|
||||
goog.inherits(proto.SharedProcessActive, jspb.Message);
|
||||
if (goog.DEBUG && !COMPILED) {
|
||||
proto.SharedProcessActiveMessage.displayName = 'proto.SharedProcessActiveMessage';
|
||||
/**
|
||||
* @public
|
||||
* @override
|
||||
*/
|
||||
proto.SharedProcessActive.displayName = 'proto.SharedProcessActive';
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* Creates an object representation of this proto suitable for use in Soy templates.
|
||||
@@ -43,8 +47,8 @@ if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
* for transitional soy proto support: http://goto/soy-param-migration
|
||||
* @return {!Object}
|
||||
*/
|
||||
proto.SharedProcessActiveMessage.prototype.toObject = function(opt_includeInstance) {
|
||||
return proto.SharedProcessActiveMessage.toObject(opt_includeInstance, this);
|
||||
proto.SharedProcessActive.prototype.toObject = function(opt_includeInstance) {
|
||||
return proto.SharedProcessActive.toObject(opt_includeInstance, this);
|
||||
};
|
||||
|
||||
|
||||
@@ -53,12 +57,12 @@ proto.SharedProcessActiveMessage.prototype.toObject = function(opt_includeInstan
|
||||
* @param {boolean|undefined} includeInstance Whether to include the JSPB
|
||||
* instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @param {!proto.SharedProcessActiveMessage} msg The msg instance to transform.
|
||||
* @param {!proto.SharedProcessActive} msg The msg instance to transform.
|
||||
* @return {!Object}
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.SharedProcessActiveMessage.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
proto.SharedProcessActive.toObject = function(includeInstance, msg) {
|
||||
var obj = {
|
||||
socketPath: jspb.Message.getFieldWithDefault(msg, 1, ""),
|
||||
logPath: jspb.Message.getFieldWithDefault(msg, 2, "")
|
||||
};
|
||||
@@ -74,23 +78,23 @@ proto.SharedProcessActiveMessage.toObject = function(includeInstance, msg) {
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format).
|
||||
* @param {jspb.ByteSource} bytes The bytes to deserialize.
|
||||
* @return {!proto.SharedProcessActiveMessage}
|
||||
* @return {!proto.SharedProcessActive}
|
||||
*/
|
||||
proto.SharedProcessActiveMessage.deserializeBinary = function(bytes) {
|
||||
proto.SharedProcessActive.deserializeBinary = function(bytes) {
|
||||
var reader = new jspb.BinaryReader(bytes);
|
||||
var msg = new proto.SharedProcessActiveMessage;
|
||||
return proto.SharedProcessActiveMessage.deserializeBinaryFromReader(msg, reader);
|
||||
var msg = new proto.SharedProcessActive;
|
||||
return proto.SharedProcessActive.deserializeBinaryFromReader(msg, reader);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format) from the
|
||||
* given reader into the given message object.
|
||||
* @param {!proto.SharedProcessActiveMessage} msg The message object to deserialize into.
|
||||
* @param {!proto.SharedProcessActive} msg The message object to deserialize into.
|
||||
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
|
||||
* @return {!proto.SharedProcessActiveMessage}
|
||||
* @return {!proto.SharedProcessActive}
|
||||
*/
|
||||
proto.SharedProcessActiveMessage.deserializeBinaryFromReader = function(msg, reader) {
|
||||
proto.SharedProcessActive.deserializeBinaryFromReader = function(msg, reader) {
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup()) {
|
||||
break;
|
||||
@@ -118,9 +122,9 @@ proto.SharedProcessActiveMessage.deserializeBinaryFromReader = function(msg, rea
|
||||
* Serializes the message to binary data (in protobuf wire format).
|
||||
* @return {!Uint8Array}
|
||||
*/
|
||||
proto.SharedProcessActiveMessage.prototype.serializeBinary = function() {
|
||||
proto.SharedProcessActive.prototype.serializeBinary = function() {
|
||||
var writer = new jspb.BinaryWriter();
|
||||
proto.SharedProcessActiveMessage.serializeBinaryToWriter(this, writer);
|
||||
proto.SharedProcessActive.serializeBinaryToWriter(this, writer);
|
||||
return writer.getResultBuffer();
|
||||
};
|
||||
|
||||
@@ -128,11 +132,11 @@ proto.SharedProcessActiveMessage.prototype.serializeBinary = function() {
|
||||
/**
|
||||
* Serializes the given message to binary data (in protobuf wire
|
||||
* format), writing to the given BinaryWriter.
|
||||
* @param {!proto.SharedProcessActiveMessage} message
|
||||
* @param {!proto.SharedProcessActive} message
|
||||
* @param {!jspb.BinaryWriter} writer
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.SharedProcessActiveMessage.serializeBinaryToWriter = function(message, writer) {
|
||||
proto.SharedProcessActive.serializeBinaryToWriter = function(message, writer) {
|
||||
var f = undefined;
|
||||
f = message.getSocketPath();
|
||||
if (f.length > 0) {
|
||||
@@ -155,13 +159,13 @@ proto.SharedProcessActiveMessage.serializeBinaryToWriter = function(message, wri
|
||||
* optional string socket_path = 1;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.SharedProcessActiveMessage.prototype.getSocketPath = function() {
|
||||
proto.SharedProcessActive.prototype.getSocketPath = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
|
||||
};
|
||||
|
||||
|
||||
/** @param {string} value */
|
||||
proto.SharedProcessActiveMessage.prototype.setSocketPath = function(value) {
|
||||
proto.SharedProcessActive.prototype.setSocketPath = function(value) {
|
||||
jspb.Message.setProto3StringField(this, 1, value);
|
||||
};
|
||||
|
||||
@@ -170,13 +174,13 @@ proto.SharedProcessActiveMessage.prototype.setSocketPath = function(value) {
|
||||
* optional string log_path = 2;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.SharedProcessActiveMessage.prototype.getLogPath = function() {
|
||||
proto.SharedProcessActive.prototype.getLogPath = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
|
||||
};
|
||||
|
||||
|
||||
/** @param {string} value */
|
||||
proto.SharedProcessActiveMessage.prototype.setLogPath = function(value) {
|
||||
proto.SharedProcessActive.prototype.setLogPath = function(value) {
|
||||
jspb.Message.setProto3StringField(this, 2, value);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ import * as path from "path";
|
||||
import { Readable } from "stream";
|
||||
import * as util from "util";
|
||||
import { createClient } from "@coder/protocol/test";
|
||||
|
||||
const client = createClient();
|
||||
jest.mock("../src/fill/client", () => ({ client }));
|
||||
const cp = require("../src/fill/child_process") as typeof import("child_process");
|
||||
import { Module } from "../src/common/proxy";
|
||||
|
||||
describe("child_process", () => {
|
||||
const client = createClient();
|
||||
const cp = client.modules[Module.ChildProcess];
|
||||
|
||||
const getStdout = async (proc: ChildProcess): Promise<string> => {
|
||||
return new Promise((r): Readable => proc.stdout.on("data", r))
|
||||
return new Promise((r): Readable => proc.stdout!.on("data", r))
|
||||
.then((s) => s.toString());
|
||||
};
|
||||
|
||||
@@ -36,10 +36,10 @@ describe("child_process", () => {
|
||||
it("should cat", async () => {
|
||||
const proc = cp.spawn("cat", []);
|
||||
expect(proc.pid).toBe(-1);
|
||||
proc.stdin.write("banana");
|
||||
proc.stdin!.write("banana");
|
||||
await expect(getStdout(proc)).resolves.toBe("banana");
|
||||
|
||||
proc.stdin.end();
|
||||
proc.stdin!.end();
|
||||
proc.kill();
|
||||
|
||||
expect(proc.pid).toBeGreaterThan(-1);
|
||||
@@ -71,4 +71,28 @@ describe("child_process", () => {
|
||||
await new Promise((r): ChildProcess => proc.on("exit", r));
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispose", (done) => {
|
||||
setTimeout(() => {
|
||||
client.dispose();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it("should disconnect", async () => {
|
||||
const client = createClient();
|
||||
const cp = client.modules[Module.ChildProcess];
|
||||
const proc = cp.fork(path.join(__dirname, "forker.js"));
|
||||
const fn = jest.fn();
|
||||
proc.on("error", fn);
|
||||
|
||||
proc.send({ bananas: true });
|
||||
await expect(new Promise((r): ChildProcess => proc.on("message", r)))
|
||||
.resolves.toMatchObject({
|
||||
bananas: true,
|
||||
});
|
||||
|
||||
client.dispose();
|
||||
expect(fn).toHaveBeenCalledWith(new Error("disconnected"));
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
import { createClient } from "./helpers";
|
||||
|
||||
describe("Evaluate", () => {
|
||||
const client = createClient();
|
||||
|
||||
it("should transfer string", async () => {
|
||||
const value = await client.evaluate(() => {
|
||||
return "hi";
|
||||
});
|
||||
|
||||
expect(value).toEqual("hi");
|
||||
}, 100);
|
||||
|
||||
it("should compute from string", async () => {
|
||||
const start = "ban\%\$\"``a,,,,asdasd";
|
||||
const value = await client.evaluate((_helper, a) => {
|
||||
return a;
|
||||
}, start);
|
||||
|
||||
expect(value).toEqual(start);
|
||||
}, 100);
|
||||
|
||||
it("should compute from object", async () => {
|
||||
const value = await client.evaluate((_helper, arg) => {
|
||||
return arg.bananas * 2;
|
||||
}, { bananas: 1 });
|
||||
|
||||
expect(value).toEqual(2);
|
||||
}, 100);
|
||||
|
||||
it("should transfer object", async () => {
|
||||
const value = await client.evaluate(() => {
|
||||
return { alpha: "beta" };
|
||||
});
|
||||
|
||||
expect(value.alpha).toEqual("beta");
|
||||
}, 100);
|
||||
|
||||
it("should require", async () => {
|
||||
const value = await client.evaluate(() => {
|
||||
const fs = require("fs") as typeof import("fs");
|
||||
|
||||
return Object.keys(fs).filter((f) => f === "readFileSync");
|
||||
});
|
||||
|
||||
expect(value[0]).toEqual("readFileSync");
|
||||
}, 100);
|
||||
|
||||
it("should resolve with promise", async () => {
|
||||
const value = await client.evaluate(async () => {
|
||||
await new Promise((r): number => setTimeout(r, 100));
|
||||
|
||||
return "donkey";
|
||||
});
|
||||
|
||||
expect(value).toEqual("donkey");
|
||||
}, 250);
|
||||
|
||||
it("should do active process", (done) => {
|
||||
const runner = client.run((ae) => {
|
||||
ae.on("first", () => {
|
||||
ae.emit("first:response");
|
||||
ae.on("second", () => ae.emit("second:response"));
|
||||
});
|
||||
|
||||
const disposeCallbacks = <Array<() => void>>[];
|
||||
const dispose = (): void => {
|
||||
disposeCallbacks.forEach((cb) => cb());
|
||||
ae.emit("disposed");
|
||||
};
|
||||
|
||||
return {
|
||||
onDidDispose: (cb: () => void): number => disposeCallbacks.push(cb),
|
||||
dispose,
|
||||
};
|
||||
});
|
||||
|
||||
runner.emit("first");
|
||||
runner.on("first:response", () => runner.emit("second"));
|
||||
runner.on("second:response", () => client.dispose());
|
||||
|
||||
runner.on("disposed", () => done());
|
||||
});
|
||||
});
|
||||
@@ -1,53 +1,34 @@
|
||||
import * as nativeFs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as util from "util";
|
||||
import * as rimraf from "rimraf";
|
||||
import { createClient } from "@coder/protocol/test";
|
||||
|
||||
const client = createClient();
|
||||
jest.mock("../src/fill/client", () => ({ client }));
|
||||
const fs = require("../src/fill/fs") as typeof import("fs");
|
||||
import { Module } from "../src/common/proxy";
|
||||
import { createClient, Helper } from "./helpers";
|
||||
|
||||
describe("fs", () => {
|
||||
let i = 0;
|
||||
const coderDir = path.join(os.tmpdir(), "coder", "fs");
|
||||
const testFile = path.join(__dirname, "fs.test.ts");
|
||||
const tmpFile = (): string => path.join(coderDir, `${i++}`);
|
||||
const createTmpFile = async (): Promise<string> => {
|
||||
const tf = tmpFile();
|
||||
await util.promisify(nativeFs.writeFile)(tf, "");
|
||||
|
||||
return tf;
|
||||
};
|
||||
const client = createClient();
|
||||
// tslint:disable-next-line no-any
|
||||
const fs = client.modules[Module.Fs] as any as typeof import("fs");
|
||||
const helper = new Helper("fs");
|
||||
|
||||
beforeAll(async () => {
|
||||
try {
|
||||
await util.promisify(nativeFs.mkdir)(path.dirname(coderDir));
|
||||
} catch (error) {
|
||||
if (error.code !== "EEXIST" && error.code !== "EISDIR") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await util.promisify(rimraf)(coderDir);
|
||||
await util.promisify(nativeFs.mkdir)(coderDir);
|
||||
await helper.prepare();
|
||||
});
|
||||
|
||||
describe("access", () => {
|
||||
it("should access existing file", async () => {
|
||||
await expect(util.promisify(fs.access)(testFile))
|
||||
await expect(util.promisify(fs.access)(__filename))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to access nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.access)(tmpFile()))
|
||||
await expect(util.promisify(fs.access)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("append", () => {
|
||||
it("should append to existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.appendFile)(file, "howdy"))
|
||||
.resolves.toBeUndefined();
|
||||
expect(await util.promisify(nativeFs.readFile)(file, "utf8"))
|
||||
@@ -55,7 +36,7 @@ describe("fs", () => {
|
||||
});
|
||||
|
||||
it("should create then append to nonexistent file", async () => {
|
||||
const file = tmpFile();
|
||||
const file = helper.tmpFile();
|
||||
await expect(util.promisify(fs.appendFile)(file, "howdy"))
|
||||
.resolves.toBeUndefined();
|
||||
expect(await util.promisify(nativeFs.readFile)(file, "utf8"))
|
||||
@@ -63,7 +44,7 @@ describe("fs", () => {
|
||||
});
|
||||
|
||||
it("should fail to append to file in nonexistent directory", async () => {
|
||||
const file = path.join(tmpFile(), "nope");
|
||||
const file = path.join(helper.tmpFile(), "nope");
|
||||
await expect(util.promisify(fs.appendFile)(file, "howdy"))
|
||||
.rejects.toThrow("ENOENT");
|
||||
expect(await util.promisify(nativeFs.exists)(file))
|
||||
@@ -73,33 +54,33 @@ describe("fs", () => {
|
||||
|
||||
describe("chmod", () => {
|
||||
it("should chmod existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.chmod)(file, "755"))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to chmod nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.chmod)(tmpFile(), "755"))
|
||||
await expect(util.promisify(fs.chmod)(helper.tmpFile(), "755"))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("chown", () => {
|
||||
it("should chown existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.chown)(file, 1, 1))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to chown nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.chown)(tmpFile(), 1, 1))
|
||||
await expect(util.promisify(fs.chown)(helper.tmpFile(), 1, 1))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("close", () => {
|
||||
it("should close opened file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "r");
|
||||
await expect(util.promisify(fs.close)(fd))
|
||||
.resolves.toBeUndefined();
|
||||
@@ -113,8 +94,8 @@ describe("fs", () => {
|
||||
|
||||
describe("copyFile", () => {
|
||||
it("should copy existing file", async () => {
|
||||
const source = await createTmpFile();
|
||||
const destination = tmpFile();
|
||||
const source = await helper.createTmpFile();
|
||||
const destination = helper.tmpFile();
|
||||
await expect(util.promisify(fs.copyFile)(source, destination))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(util.promisify(fs.exists)(destination))
|
||||
@@ -122,44 +103,47 @@ describe("fs", () => {
|
||||
});
|
||||
|
||||
it("should fail to copy nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.copyFile)(tmpFile(), tmpFile()))
|
||||
await expect(util.promisify(fs.copyFile)(helper.tmpFile(), helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createWriteStream", () => {
|
||||
it("should write to file", async () => {
|
||||
const file = tmpFile();
|
||||
const file = helper.tmpFile();
|
||||
const content = "howdy\nhow\nr\nu";
|
||||
const stream = fs.createWriteStream(file);
|
||||
stream.on("open", (fd) => {
|
||||
expect(fd).toBeDefined();
|
||||
stream.write(content);
|
||||
stream.close();
|
||||
stream.end();
|
||||
});
|
||||
await expect(new Promise((resolve): void => {
|
||||
stream.on("close", async () => {
|
||||
resolve(await util.promisify(nativeFs.readFile)(file, "utf8"));
|
||||
});
|
||||
})).resolves.toBe(content);
|
||||
|
||||
await Promise.all([
|
||||
new Promise((resolve): nativeFs.WriteStream => stream.on("close", resolve)),
|
||||
new Promise((resolve): nativeFs.WriteStream => stream.on("finish", resolve)),
|
||||
]);
|
||||
|
||||
await expect(util.promisify(nativeFs.readFile)(file, "utf8")).resolves.toBe(content);
|
||||
});
|
||||
});
|
||||
|
||||
describe("exists", () => {
|
||||
it("should output file exists", async () => {
|
||||
await expect(util.promisify(fs.exists)(testFile))
|
||||
await expect(util.promisify(fs.exists)(__filename))
|
||||
.resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should output file does not exist", async () => {
|
||||
await expect(util.promisify(fs.exists)(tmpFile()))
|
||||
await expect(util.promisify(fs.exists)(helper.tmpFile()))
|
||||
.resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fchmod", () => {
|
||||
it("should fchmod existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "r");
|
||||
await expect(util.promisify(fs.fchmod)(fd, "755"))
|
||||
.resolves.toBeUndefined();
|
||||
@@ -174,7 +158,7 @@ describe("fs", () => {
|
||||
|
||||
describe("fchown", () => {
|
||||
it("should fchown existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "r");
|
||||
await expect(util.promisify(fs.fchown)(fd, 1, 1))
|
||||
.resolves.toBeUndefined();
|
||||
@@ -189,7 +173,7 @@ describe("fs", () => {
|
||||
|
||||
describe("fdatasync", () => {
|
||||
it("should fdatasync existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "r");
|
||||
await expect(util.promisify(fs.fdatasync)(fd))
|
||||
.resolves.toBeUndefined();
|
||||
@@ -204,7 +188,7 @@ describe("fs", () => {
|
||||
|
||||
describe("fstat", () => {
|
||||
it("should fstat existing file", async () => {
|
||||
const fd = await util.promisify(nativeFs.open)(testFile, "r");
|
||||
const fd = await util.promisify(nativeFs.open)(__filename, "r");
|
||||
const stat = await util.promisify(nativeFs.fstat)(fd);
|
||||
await expect(util.promisify(fs.fstat)(fd))
|
||||
.resolves.toMatchObject({
|
||||
@@ -221,7 +205,7 @@ describe("fs", () => {
|
||||
|
||||
describe("fsync", () => {
|
||||
it("should fsync existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "r");
|
||||
await expect(util.promisify(fs.fsync)(fd))
|
||||
.resolves.toBeUndefined();
|
||||
@@ -236,7 +220,7 @@ describe("fs", () => {
|
||||
|
||||
describe("ftruncate", () => {
|
||||
it("should ftruncate existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "w");
|
||||
await expect(util.promisify(fs.ftruncate)(fd, 1))
|
||||
.resolves.toBeUndefined();
|
||||
@@ -251,7 +235,7 @@ describe("fs", () => {
|
||||
|
||||
describe("futimes", () => {
|
||||
it("should futimes existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "w");
|
||||
await expect(util.promisify(fs.futimes)(fd, 1, 1))
|
||||
.resolves.toBeUndefined();
|
||||
@@ -266,36 +250,36 @@ describe("fs", () => {
|
||||
|
||||
describe("lchmod", () => {
|
||||
it("should lchmod existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.lchmod)(file, "755"))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
// TODO: Doesn't fail on my system?
|
||||
it("should fail to lchmod nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.lchmod)(tmpFile(), "755"))
|
||||
await expect(util.promisify(fs.lchmod)(helper.tmpFile(), "755"))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("lchown", () => {
|
||||
it("should lchown existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.lchown)(file, 1, 1))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
// TODO: Doesn't fail on my system?
|
||||
it("should fail to lchown nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.lchown)(tmpFile(), 1, 1))
|
||||
await expect(util.promisify(fs.lchown)(helper.tmpFile(), 1, 1))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("link", () => {
|
||||
it("should link existing file", async () => {
|
||||
const source = await createTmpFile();
|
||||
const destination = tmpFile();
|
||||
const source = await helper.createTmpFile();
|
||||
const destination = helper.tmpFile();
|
||||
await expect(util.promisify(fs.link)(source, destination))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(util.promisify(fs.exists)(destination))
|
||||
@@ -303,29 +287,30 @@ describe("fs", () => {
|
||||
});
|
||||
|
||||
it("should fail to link nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.link)(tmpFile(), tmpFile()))
|
||||
await expect(util.promisify(fs.link)(helper.tmpFile(), helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("lstat", () => {
|
||||
it("should lstat existing file", async () => {
|
||||
const stat = await util.promisify(nativeFs.lstat)(testFile);
|
||||
await expect(util.promisify(fs.lstat)(testFile))
|
||||
const stat = await util.promisify(nativeFs.lstat)(__filename);
|
||||
await expect(util.promisify(fs.lstat)(__filename))
|
||||
.resolves.toMatchObject({
|
||||
size: stat.size,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to lstat non-existent file", async () => {
|
||||
await expect(util.promisify(fs.lstat)(tmpFile()))
|
||||
await expect(util.promisify(fs.lstat)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mkdir", () => {
|
||||
const target = tmpFile();
|
||||
let target: string;
|
||||
it("should create nonexistent directory", async () => {
|
||||
target = helper.tmpFile();
|
||||
await expect(util.promisify(fs.mkdir)(target))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
@@ -338,28 +323,28 @@ describe("fs", () => {
|
||||
|
||||
describe("mkdtemp", () => {
|
||||
it("should create temp dir", async () => {
|
||||
await expect(util.promisify(fs.mkdtemp)(coderDir + "/"))
|
||||
await expect(util.promisify(fs.mkdtemp)(helper.coderDir + "/"))
|
||||
.resolves.toMatch(/^\/tmp\/coder\/fs\/[a-zA-Z0-9]{6}/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("open", () => {
|
||||
it("should open existing file", async () => {
|
||||
const fd = await util.promisify(fs.open)(testFile, "r");
|
||||
const fd = await util.promisify(fs.open)(__filename, "r");
|
||||
expect(fd).not.toBeNaN();
|
||||
await expect(util.promisify(fs.close)(fd))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to open nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.open)(tmpFile(), "r"))
|
||||
await expect(util.promisify(fs.open)(helper.tmpFile(), "r"))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("read", () => {
|
||||
it("should read existing file", async () => {
|
||||
const fd = await util.promisify(nativeFs.open)(testFile, "r");
|
||||
const fd = await util.promisify(nativeFs.open)(__filename, "r");
|
||||
const stat = await util.promisify(nativeFs.fstat)(fd);
|
||||
const buffer = new Buffer(stat.size);
|
||||
let bytesRead = 0;
|
||||
@@ -373,7 +358,7 @@ describe("fs", () => {
|
||||
bytesRead += chunkSize;
|
||||
}
|
||||
|
||||
const content = await util.promisify(nativeFs.readFile)(testFile, "utf8");
|
||||
const content = await util.promisify(nativeFs.readFile)(__filename, "utf8");
|
||||
expect(buffer.toString()).toEqual(content);
|
||||
await util.promisify(nativeFs.close)(fd);
|
||||
});
|
||||
@@ -386,64 +371,64 @@ describe("fs", () => {
|
||||
|
||||
describe("readFile", () => {
|
||||
it("should read existing file", async () => {
|
||||
const content = await util.promisify(nativeFs.readFile)(testFile, "utf8");
|
||||
await expect(util.promisify(fs.readFile)(testFile, "utf8"))
|
||||
const content = await util.promisify(nativeFs.readFile)(__filename, "utf8");
|
||||
await expect(util.promisify(fs.readFile)(__filename, "utf8"))
|
||||
.resolves.toEqual(content);
|
||||
});
|
||||
|
||||
it("should fail to read nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.readFile)(tmpFile()))
|
||||
await expect(util.promisify(fs.readFile)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("readdir", () => {
|
||||
it("should read existing directory", async () => {
|
||||
const paths = await util.promisify(nativeFs.readdir)(coderDir);
|
||||
await expect(util.promisify(fs.readdir)(coderDir))
|
||||
const paths = await util.promisify(nativeFs.readdir)(helper.coderDir);
|
||||
await expect(util.promisify(fs.readdir)(helper.coderDir))
|
||||
.resolves.toEqual(paths);
|
||||
});
|
||||
|
||||
it("should fail to read nonexistent directory", async () => {
|
||||
await expect(util.promisify(fs.readdir)(tmpFile()))
|
||||
await expect(util.promisify(fs.readdir)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("readlink", () => {
|
||||
it("should read existing link", async () => {
|
||||
const source = await createTmpFile();
|
||||
const destination = tmpFile();
|
||||
const source = await helper.createTmpFile();
|
||||
const destination = helper.tmpFile();
|
||||
await util.promisify(nativeFs.symlink)(source, destination);
|
||||
await expect(util.promisify(fs.readlink)(destination))
|
||||
.resolves.toBe(source);
|
||||
});
|
||||
|
||||
it("should fail to read nonexistent link", async () => {
|
||||
await expect(util.promisify(fs.readlink)(tmpFile()))
|
||||
await expect(util.promisify(fs.readlink)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("realpath", () => {
|
||||
it("should read real path of existing file", async () => {
|
||||
const source = await createTmpFile();
|
||||
const destination = tmpFile();
|
||||
const source = await helper.createTmpFile();
|
||||
const destination = helper.tmpFile();
|
||||
nativeFs.symlinkSync(source, destination);
|
||||
await expect(util.promisify(fs.realpath)(destination))
|
||||
.resolves.toBe(source);
|
||||
});
|
||||
|
||||
it("should fail to read real path of nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.realpath)(tmpFile()))
|
||||
await expect(util.promisify(fs.realpath)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rename", () => {
|
||||
it("should rename existing file", async () => {
|
||||
const source = await createTmpFile();
|
||||
const destination = tmpFile();
|
||||
const source = await helper.createTmpFile();
|
||||
const destination = helper.tmpFile();
|
||||
await expect(util.promisify(fs.rename)(source, destination))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(util.promisify(nativeFs.exists)(source))
|
||||
@@ -453,14 +438,14 @@ describe("fs", () => {
|
||||
});
|
||||
|
||||
it("should fail to rename nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.rename)(tmpFile(), tmpFile()))
|
||||
await expect(util.promisify(fs.rename)(helper.tmpFile(), helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rmdir", () => {
|
||||
it("should rmdir existing directory", async () => {
|
||||
const dir = tmpFile();
|
||||
const dir = helper.tmpFile();
|
||||
await util.promisify(nativeFs.mkdir)(dir);
|
||||
await expect(util.promisify(fs.rmdir)(dir))
|
||||
.resolves.toBeUndefined();
|
||||
@@ -469,15 +454,15 @@ describe("fs", () => {
|
||||
});
|
||||
|
||||
it("should fail to rmdir nonexistent directory", async () => {
|
||||
await expect(util.promisify(fs.rmdir)(tmpFile()))
|
||||
await expect(util.promisify(fs.rmdir)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stat", () => {
|
||||
it("should stat existing file", async () => {
|
||||
const nativeStat = await util.promisify(nativeFs.stat)(testFile);
|
||||
const stat = await util.promisify(fs.stat)(testFile);
|
||||
const nativeStat = await util.promisify(nativeFs.stat)(__filename);
|
||||
const stat = await util.promisify(fs.stat)(__filename);
|
||||
expect(stat).toMatchObject({
|
||||
size: nativeStat.size,
|
||||
});
|
||||
@@ -485,7 +470,7 @@ describe("fs", () => {
|
||||
});
|
||||
|
||||
it("should stat existing folder", async () => {
|
||||
const dir = tmpFile();
|
||||
const dir = helper.tmpFile();
|
||||
await util.promisify(nativeFs.mkdir)(dir);
|
||||
const nativeStat = await util.promisify(nativeFs.stat)(dir);
|
||||
const stat = await util.promisify(fs.stat)(dir);
|
||||
@@ -496,7 +481,7 @@ describe("fs", () => {
|
||||
});
|
||||
|
||||
it("should fail to stat nonexistent file", async () => {
|
||||
const error = await util.promisify(fs.stat)(tmpFile()).catch((e) => e);
|
||||
const error = await util.promisify(fs.stat)(helper.tmpFile()).catch((e) => e);
|
||||
expect(error.message).toContain("ENOENT");
|
||||
expect(error.code).toBe("ENOENT");
|
||||
});
|
||||
@@ -504,8 +489,8 @@ describe("fs", () => {
|
||||
|
||||
describe("symlink", () => {
|
||||
it("should symlink existing file", async () => {
|
||||
const source = await createTmpFile();
|
||||
const destination = tmpFile();
|
||||
const source = await helper.createTmpFile();
|
||||
const destination = helper.tmpFile();
|
||||
await expect(util.promisify(fs.symlink)(source, destination))
|
||||
.resolves.toBeUndefined();
|
||||
expect(util.promisify(nativeFs.exists)(source))
|
||||
@@ -514,14 +499,14 @@ describe("fs", () => {
|
||||
|
||||
// TODO: Seems to be happy to do this on my system?
|
||||
it("should fail to symlink nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.symlink)(tmpFile(), tmpFile()))
|
||||
await expect(util.promisify(fs.symlink)(helper.tmpFile(), helper.tmpFile()))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("truncate", () => {
|
||||
it("should truncate existing file", async () => {
|
||||
const file = tmpFile();
|
||||
const file = helper.tmpFile();
|
||||
await util.promisify(nativeFs.writeFile)(file, "hiiiiii");
|
||||
await expect(util.promisify(fs.truncate)(file, 2))
|
||||
.resolves.toBeUndefined();
|
||||
@@ -530,14 +515,14 @@ describe("fs", () => {
|
||||
});
|
||||
|
||||
it("should fail to truncate nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.truncate)(tmpFile(), 0))
|
||||
await expect(util.promisify(fs.truncate)(helper.tmpFile(), 0))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unlink", () => {
|
||||
it("should unlink existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.unlink)(file))
|
||||
.resolves.toBeUndefined();
|
||||
expect(util.promisify(nativeFs.exists)(file))
|
||||
@@ -545,27 +530,27 @@ describe("fs", () => {
|
||||
});
|
||||
|
||||
it("should fail to unlink nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.unlink)(tmpFile()))
|
||||
await expect(util.promisify(fs.unlink)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("utimes", () => {
|
||||
it("should update times on existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.utimes)(file, 100, 100))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to update times on nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.utimes)(tmpFile(), 100, 100))
|
||||
await expect(util.promisify(fs.utimes)(helper.tmpFile(), 100, 100))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("write", () => {
|
||||
it("should write to existing file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "w");
|
||||
await expect(util.promisify(fs.write)(fd, Buffer.from("hi")))
|
||||
.resolves.toBe(2);
|
||||
@@ -582,11 +567,15 @@ describe("fs", () => {
|
||||
|
||||
describe("writeFile", () => {
|
||||
it("should write file", async () => {
|
||||
const file = await createTmpFile();
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.writeFile)(file, "howdy"))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(util.promisify(nativeFs.readFile)(file, "utf8"))
|
||||
.resolves.toBe("howdy");
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispose", () => {
|
||||
client.dispose();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,54 @@
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as rimraf from "rimraf";
|
||||
import * as util from "util";
|
||||
import { IDisposable } from "@coder/disposable";
|
||||
import { Emitter } from "@coder/events";
|
||||
import { Client } from "../src/browser/client";
|
||||
import { Server, ServerOptions } from "../src/node/server";
|
||||
|
||||
// So we only make the directory once when running multiple tests.
|
||||
let mkdirPromise: Promise<void> | undefined;
|
||||
|
||||
export class Helper {
|
||||
private i = 0;
|
||||
public coderDir: string;
|
||||
private baseDir = path.join(os.tmpdir(), "coder");
|
||||
|
||||
public constructor(directoryName: string) {
|
||||
if (!directoryName.trim()) {
|
||||
throw new Error("no directory name");
|
||||
}
|
||||
|
||||
this.coderDir = path.join(this.baseDir, directoryName);
|
||||
}
|
||||
|
||||
public tmpFile(): string {
|
||||
return path.join(this.coderDir, `${this.i++}`);
|
||||
}
|
||||
|
||||
public async createTmpFile(): Promise<string> {
|
||||
const tf = this.tmpFile();
|
||||
await util.promisify(fs.writeFile)(tf, "");
|
||||
|
||||
return tf;
|
||||
}
|
||||
|
||||
public async prepare(): Promise<void> {
|
||||
if (!mkdirPromise) {
|
||||
mkdirPromise = util.promisify(fs.mkdir)(this.baseDir).catch((error) => {
|
||||
if (error.code !== "EEXIST" && error.code !== "EISDIR") {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
await mkdirPromise;
|
||||
await util.promisify(rimraf)(this.coderDir);
|
||||
await util.promisify(fs.mkdir)(this.coderDir);
|
||||
}
|
||||
}
|
||||
|
||||
export const createClient = (serverOptions?: ServerOptions): Client => {
|
||||
const s2c = new Emitter<Uint8Array | Buffer>();
|
||||
const c2s = new Emitter<Uint8Array | Buffer>();
|
||||
@@ -10,19 +57,19 @@ export const createClient = (serverOptions?: ServerOptions): Client => {
|
||||
// tslint:disable-next-line no-unused-expression
|
||||
new Server({
|
||||
close: (): void => closeCallbacks.forEach((cb) => cb()),
|
||||
onDown: (_cb: () => void): void => undefined,
|
||||
onUp: (_cb: () => void): void => undefined,
|
||||
onClose: (cb: () => void): number => closeCallbacks.push(cb),
|
||||
onMessage: (cb): void => {
|
||||
c2s.event((d) => cb(d));
|
||||
},
|
||||
onMessage: (cb): IDisposable => c2s.event((d) => cb(d)),
|
||||
send: (data): NodeJS.Timer => setTimeout(() => s2c.emit(data), 0),
|
||||
}, serverOptions);
|
||||
|
||||
const client = new Client({
|
||||
close: (): void => closeCallbacks.forEach((cb) => cb()),
|
||||
onDown: (_cb: () => void): void => undefined,
|
||||
onUp: (_cb: () => void): void => undefined,
|
||||
onClose: (cb: () => void): number => closeCallbacks.push(cb),
|
||||
onMessage: (cb): void => {
|
||||
s2c.event((d) => cb(d));
|
||||
},
|
||||
onMessage: (cb): IDisposable => s2c.event((d) => cb(d)),
|
||||
send: (data): NodeJS.Timer => setTimeout(() => c2s.emit(data), 0),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,34 +1,18 @@
|
||||
import * as fs from "fs";
|
||||
import * as nativeNet from "net";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as util from "util";
|
||||
import * as rimraf from "rimraf";
|
||||
import { createClient } from "@coder/protocol/test";
|
||||
|
||||
const client = createClient();
|
||||
jest.mock("../src/fill/client", () => ({ client }));
|
||||
const net = require("../src/fill/net") as typeof import("net");
|
||||
import { Module } from "../src/common/proxy";
|
||||
import { createClient, Helper } from "./helpers";
|
||||
|
||||
describe("net", () => {
|
||||
let i = 0;
|
||||
const coderDir = path.join(os.tmpdir(), "coder", "net");
|
||||
const tmpFile = (): string => path.join(coderDir, `socket.${i++}`);
|
||||
const client = createClient();
|
||||
const net = client.modules[Module.Net];
|
||||
const helper = new Helper("net");
|
||||
|
||||
beforeAll(async () => {
|
||||
try {
|
||||
await util.promisify(fs.mkdir)(path.dirname(coderDir));
|
||||
} catch (error) {
|
||||
if (error.code !== "EEXIST" && error.code !== "EISDIR") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await util.promisify(rimraf)(coderDir);
|
||||
await util.promisify(fs.mkdir)(coderDir);
|
||||
await helper.prepare();
|
||||
});
|
||||
|
||||
describe("Socket", () => {
|
||||
const socketPath = tmpFile();
|
||||
const socketPath = helper.tmpFile();
|
||||
let server: nativeNet.Server;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -41,6 +25,19 @@ describe("net", () => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
it("should fail to connect", async () => {
|
||||
const socket = new net.Socket();
|
||||
|
||||
const fn = jest.fn();
|
||||
socket.on("error", fn);
|
||||
|
||||
socket.connect("/tmp/t/e/s/t/d/o/e/s/n/o/t/e/x/i/s/t");
|
||||
|
||||
await new Promise((r): nativeNet.Socket => socket.on("close", r));
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should connect", async () => {
|
||||
await new Promise((resolve): void => {
|
||||
const socket = net.createConnection(socketPath, () => {
|
||||
@@ -98,7 +95,7 @@ describe("net", () => {
|
||||
const s = net.createServer();
|
||||
s.on("listening", () => s.close());
|
||||
s.on("close", () => done());
|
||||
s.listen(tmpFile());
|
||||
s.listen(helper.tmpFile());
|
||||
});
|
||||
|
||||
it("should get connection", async () => {
|
||||
@@ -109,9 +106,13 @@ describe("net", () => {
|
||||
}
|
||||
});
|
||||
|
||||
const socketPath = tmpFile();
|
||||
const socketPath = helper.tmpFile();
|
||||
s.listen(socketPath);
|
||||
|
||||
await new Promise((resolve): void => {
|
||||
s.on("listening", resolve);
|
||||
});
|
||||
|
||||
const makeConnection = async (): Promise<void> => {
|
||||
net.createConnection(socketPath);
|
||||
await Promise.all([
|
||||
@@ -134,4 +135,11 @@ describe("net", () => {
|
||||
await new Promise((r): nativeNet.Server => s.on("close", r));
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispose", (done) => {
|
||||
setTimeout(() => {
|
||||
client.dispose();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import { IPty } from "node-pty";
|
||||
import { createClient } from "@coder/protocol/test";
|
||||
|
||||
const client = createClient();
|
||||
jest.mock("../../ide/src/fill/client", () => ({ client }));
|
||||
const pty = require("../src/fill/node-pty") as typeof import("node-pty");
|
||||
import { Module } from "../src/common/proxy";
|
||||
import { createClient } from "./helpers";
|
||||
|
||||
describe("node-pty", () => {
|
||||
const client = createClient();
|
||||
const pty = client.modules[Module.NodePty];
|
||||
|
||||
/**
|
||||
* Returns a function that when called returns a promise that resolves with
|
||||
* the next chunk of data from the process.
|
||||
@@ -47,12 +47,11 @@ describe("node-pty", () => {
|
||||
|
||||
const getData = promisifyData(proc);
|
||||
|
||||
// First it outputs @hostname:cwd
|
||||
expect((await getData()).length).toBeGreaterThan(1);
|
||||
|
||||
// Then it seems to overwrite that with a shorter prompt in the format of
|
||||
// [hostname@user]$
|
||||
expect((await getData())).toContain("$");
|
||||
// Wait for [hostname@user]$
|
||||
let data = "";
|
||||
while (!data.includes("$")) {
|
||||
data = await getData();
|
||||
}
|
||||
|
||||
proc.kill();
|
||||
|
||||
@@ -67,33 +66,34 @@ describe("node-pty", () => {
|
||||
// isn't affected by custom configuration.
|
||||
const proc = pty.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], {
|
||||
cols: 10,
|
||||
rows: 10,
|
||||
rows: 912,
|
||||
});
|
||||
|
||||
const getData = promisifyData(proc);
|
||||
|
||||
// We've already tested these first two bits of output; see shell test.
|
||||
await getData();
|
||||
await getData();
|
||||
|
||||
proc.write("tput lines\n");
|
||||
expect(await getData()).toContain("tput");
|
||||
|
||||
expect((await getData()).trim()).toContain("10");
|
||||
proc.resize(10, 50);
|
||||
|
||||
// The prompt again.
|
||||
await getData();
|
||||
await getData();
|
||||
|
||||
let data = "";
|
||||
while (!data.includes("912")) {
|
||||
data = await getData();
|
||||
}
|
||||
proc.resize(10, 219);
|
||||
proc.write("tput lines\n");
|
||||
expect(await getData()).toContain("tput");
|
||||
|
||||
expect((await getData())).toContain("50");
|
||||
while (!data.includes("219")) {
|
||||
data = await getData();
|
||||
}
|
||||
|
||||
proc.kill();
|
||||
await new Promise((resolve): void => {
|
||||
proc.on("exit", resolve);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispose", (done) => {
|
||||
setTimeout(() => {
|
||||
client.dispose();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
@@ -12,12 +12,10 @@ describe("Server", () => {
|
||||
workingDirectory,
|
||||
});
|
||||
|
||||
it("should get init msg", (done) => {
|
||||
client.initData.then((data) => {
|
||||
expect(data.dataDirectory).toEqual(dataDirectory);
|
||||
expect(data.workingDirectory).toEqual(workingDirectory);
|
||||
expect(data.builtInExtensionsDirectory).toEqual(builtInExtensionsDirectory);
|
||||
done();
|
||||
});
|
||||
it("should get init msg", async () => {
|
||||
const data = await client.initData;
|
||||
expect(data.dataDirectory).toEqual(dataDirectory);
|
||||
expect(data.workingDirectory).toEqual(workingDirectory);
|
||||
expect(data.builtInExtensionsDirectory).toEqual(builtInExtensionsDirectory);
|
||||
});
|
||||
});
|
||||
|
||||
35
packages/protocol/test/spdlog.test.ts
Normal file
35
packages/protocol/test/spdlog.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as fs from "fs";
|
||||
import * as util from "util";
|
||||
import { Module } from "../src/common/proxy";
|
||||
import { createClient, Helper } from "./helpers";
|
||||
|
||||
describe("spdlog", () => {
|
||||
const client = createClient();
|
||||
const spdlog = client.modules[Module.Spdlog];
|
||||
const helper = new Helper("spdlog");
|
||||
|
||||
beforeAll(async () => {
|
||||
await helper.prepare();
|
||||
});
|
||||
|
||||
it("should log to a file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
const logger = new spdlog.RotatingLogger("test logger", file, 10000, 10);
|
||||
logger.trace("trace");
|
||||
logger.debug("debug");
|
||||
logger.info("info");
|
||||
logger.warn("warn");
|
||||
logger.error("error");
|
||||
logger.critical("critical");
|
||||
logger.flush();
|
||||
await new Promise((resolve): number | NodeJS.Timer => setTimeout(resolve, 1000));
|
||||
expect(await util.promisify(fs.readFile)(file, "utf8"))
|
||||
.toContain("critical");
|
||||
});
|
||||
|
||||
it("should dispose", () => {
|
||||
setTimeout(() => {
|
||||
client.dispose();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
26
packages/protocol/test/trash.test.ts
Normal file
26
packages/protocol/test/trash.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as fs from "fs";
|
||||
import * as util from "util";
|
||||
import { Module } from "../src/common/proxy";
|
||||
import { createClient, Helper } from "./helpers";
|
||||
|
||||
describe("trash", () => {
|
||||
const client = createClient();
|
||||
const trash = client.modules[Module.Trash];
|
||||
const helper = new Helper("trash");
|
||||
|
||||
beforeAll(async () => {
|
||||
await helper.prepare();
|
||||
});
|
||||
|
||||
it("should trash a file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
await trash.trash(file);
|
||||
expect(await util.promisify(fs.exists)(file)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should dispose", () => {
|
||||
setTimeout(() => {
|
||||
client.dispose();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
101
packages/protocol/test/util.test.ts
Normal file
101
packages/protocol/test/util.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import * as fs from "fs";
|
||||
import * as util from "util";
|
||||
import { argumentToProto, protoToArgument } from "../src/common/util";
|
||||
|
||||
describe("Convert", () => {
|
||||
it("should convert nothing", () => {
|
||||
expect(protoToArgument()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should convert null", () => {
|
||||
expect(protoToArgument(argumentToProto(null))).toBeNull();
|
||||
});
|
||||
|
||||
it("should convert undefined", () => {
|
||||
expect(protoToArgument(argumentToProto(undefined))).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should convert string", () => {
|
||||
expect(protoToArgument(argumentToProto("test"))).toBe("test");
|
||||
});
|
||||
|
||||
it("should convert number", () => {
|
||||
expect(protoToArgument(argumentToProto(10))).toBe(10);
|
||||
});
|
||||
|
||||
it("should convert boolean", () => {
|
||||
expect(protoToArgument(argumentToProto(true))).toBe(true);
|
||||
expect(protoToArgument(argumentToProto(false))).toBe(false);
|
||||
});
|
||||
|
||||
it("should convert error", () => {
|
||||
const error = new Error("message");
|
||||
const convertedError = protoToArgument(argumentToProto(error));
|
||||
|
||||
expect(convertedError instanceof Error).toBeTruthy();
|
||||
expect(convertedError.message).toBe("message");
|
||||
});
|
||||
|
||||
it("should convert buffer", async () => {
|
||||
const buffer = await util.promisify(fs.readFile)(__filename);
|
||||
expect(buffer instanceof Buffer).toBeTruthy();
|
||||
|
||||
const convertedBuffer = protoToArgument(argumentToProto(buffer));
|
||||
expect(convertedBuffer instanceof Buffer).toBeTruthy();
|
||||
expect(convertedBuffer.toString()).toBe(buffer.toString());
|
||||
});
|
||||
|
||||
it("should convert proxy", () => {
|
||||
let i = 0;
|
||||
const proto = argumentToProto(
|
||||
{ onEvent: (): void => undefined },
|
||||
undefined,
|
||||
() => i++,
|
||||
);
|
||||
|
||||
const proxy = protoToArgument(proto, undefined, (id) => {
|
||||
return {
|
||||
id: `created: ${id}`,
|
||||
dispose: (): Promise<void> => Promise.resolve(),
|
||||
onDone: (): Promise<void> => Promise.resolve(),
|
||||
onEvent: (): Promise<void> => Promise.resolve(),
|
||||
};
|
||||
});
|
||||
|
||||
expect(proxy.id).toBe("created: 0");
|
||||
});
|
||||
|
||||
it("should convert function", () => {
|
||||
const fn = jest.fn();
|
||||
// tslint:disable-next-line no-any
|
||||
const map = new Map<number, (...args: any[]) => void>();
|
||||
let i = 0;
|
||||
const proto = argumentToProto(
|
||||
fn,
|
||||
(f) => {
|
||||
map.set(i++, f);
|
||||
|
||||
return i - 1;
|
||||
},
|
||||
);
|
||||
|
||||
const remoteFn = protoToArgument(proto, (id, args) => {
|
||||
map.get(id)!(...args);
|
||||
});
|
||||
|
||||
remoteFn("a", "b", 1);
|
||||
|
||||
expect(fn).toHaveBeenCalledWith("a", "b", 1);
|
||||
});
|
||||
|
||||
it("should convert array", () => {
|
||||
const array = ["a", "b", 1, [1, "a"], null, undefined];
|
||||
expect(protoToArgument(argumentToProto(array))).toEqual(array);
|
||||
});
|
||||
|
||||
it("should convert object", () => {
|
||||
const obj = { a: "test" };
|
||||
// const obj = { "a": 1, "b": [1, "a"], test: null, test2: undefined };
|
||||
expect(protoToArgument(argumentToProto(obj))).toEqual(obj);
|
||||
});
|
||||
});
|
||||
@@ -14,11 +14,43 @@
|
||||
dependencies:
|
||||
execa "^0.2.2"
|
||||
|
||||
"@types/events@*":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
||||
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
|
||||
|
||||
"@types/glob@*":
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
|
||||
integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
|
||||
dependencies:
|
||||
"@types/events" "*"
|
||||
"@types/minimatch" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/google-protobuf@^3.2.7":
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.2.7.tgz#9576ed5dd62cdb1c9f952522028a03b7cb2b69b5"
|
||||
integrity sha512-Pb9wl5qDEwfnJeeu6Zpn5Y+waLrKETStqLZXHMGCTbkNuBBudPy4qOGN6veamyeoUBwTm2knOVeP/FlHHhhmzA==
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
|
||||
|
||||
"@types/node@*":
|
||||
version "11.11.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.3.tgz#7c6b0f8eaf16ae530795de2ad1b85d34bf2f5c58"
|
||||
integrity sha512-wp6IOGu1lxsfnrD+5mX6qwSwWuqsdkKKxTN4aQc4wByHAKZJf9/D4KXPQ1POUjEbnCP5LMggB0OEFNY9OTsMqg==
|
||||
|
||||
"@types/rimraf@^2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e"
|
||||
integrity sha512-Hm/bnWq0TCy7jmjeN5bKYij9vw5GrDFWME4IuxV08278NtU/VdGbzsBohcCUJ7+QMqmUq5hpRKB39HeQWJjztQ==
|
||||
dependencies:
|
||||
"@types/glob" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/text-encoding@^0.0.35":
|
||||
version "0.0.35"
|
||||
resolved "https://registry.yarnpkg.com/@types/text-encoding/-/text-encoding-0.0.35.tgz#6f14474e0b232bc70c59677aadc65dcc5a99c3a9"
|
||||
@@ -898,7 +930,7 @@ readable-stream@^2.0.6, readable-stream@^2.3.0, readable-stream@^2.3.5:
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
rimraf@^2.2.8:
|
||||
rimraf@^2.2.8, rimraf@^2.6.3:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
||||
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
|
||||
@@ -1124,11 +1156,6 @@ ts-protoc-gen@^0.8.0:
|
||||
dependencies:
|
||||
google-protobuf "^3.6.1"
|
||||
|
||||
tslib@^1.9.3:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
|
||||
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
|
||||
|
||||
tunnel-agent@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
||||
|
||||
@@ -6,12 +6,11 @@
|
||||
"scripts": {
|
||||
"start": "node --max-old-space-size=32384 --require ts-node/register --require tsconfig-paths/register src/cli.ts",
|
||||
"build": "rm -rf ./out && ../../node_modules/.bin/cross-env CLI=true UV_THREADPOOL_SIZE=100 node --max-old-space-size=32384 ../../node_modules/webpack/bin/webpack.js --config ./webpack.config.js",
|
||||
"build:nexe": "node scripts/nexe.js"
|
||||
"build:binary": "ts-node scripts/nbin.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oclif/config": "^1.10.4",
|
||||
"@oclif/errors": "^1.2.2",
|
||||
"@oclif/plugin-help": "^2.1.4",
|
||||
"@coder/nbin": "^1.0.6",
|
||||
"commander": "^2.19.0",
|
||||
"express": "^4.16.4",
|
||||
"express-static-gzip": "^1.1.3",
|
||||
"httpolyglot": "^0.1.2",
|
||||
@@ -24,6 +23,7 @@
|
||||
"xhr2": "^0.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/commander": "^2.12.2",
|
||||
"@types/express": "^4.16.0",
|
||||
"@types/fs-extra": "^5.0.4",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
@@ -32,7 +32,6 @@
|
||||
"@types/safe-compare": "^1.1.0",
|
||||
"@types/ws": "^6.0.1",
|
||||
"fs-extra": "^7.0.1",
|
||||
"nexe": "^2.0.0-rc.34",
|
||||
"opn": "^5.4.0",
|
||||
"string-replace-webpack-plugin": "^0.1.3",
|
||||
"ts-node": "^7.0.1",
|
||||
|
||||
21
packages/server/scripts/nbin.ts
Normal file
21
packages/server/scripts/nbin.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Binary } from "@coder/nbin";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
|
||||
const target = `${os.platform()}-${os.arch()}`;
|
||||
const rootDir = path.join(__dirname, "..");
|
||||
const bin = new Binary({
|
||||
mainFile: path.join(rootDir, "out", "cli.js"),
|
||||
});
|
||||
bin.writeFiles(path.join(rootDir, "build", "**"));
|
||||
bin.writeFiles(path.join(rootDir, "out", "**"));
|
||||
bin.build().then((binaryData) => {
|
||||
const outputPath = path.join(__dirname, "..", `cli-${target}`);
|
||||
fs.writeFileSync(outputPath, binaryData);
|
||||
fs.chmodSync(outputPath, "755");
|
||||
}).catch((ex) => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(ex);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const fse = require("fs-extra");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const nexePath = require.resolve("nexe");
|
||||
const shimPath = path.join(path.dirname(nexePath), "lib/steps/shim.js");
|
||||
let shimContent = fs.readFileSync(shimPath).toString();
|
||||
const replaceString = `global.nativeFs = { existsSync: originalExistsSync, readFile: originalReadFile, readFileSync: originalReadFileSync, createReadStream: originalCreateReadStream, readdir: originalReaddir, readdirSync: originalReaddirSync, statSync: originalStatSync, stat: originalStat, realpath: originalRealpath, realpathSync: originalRealpathSync };`;
|
||||
shimContent = shimContent.replace(/compiler\.options\.resources\.length[\s\S]*wrap\("(.*\\n)"/g, (om, a) => {
|
||||
return om.replace(a, `${a}${replaceString}`);
|
||||
});
|
||||
fs.writeFileSync(shimPath, shimContent);
|
||||
|
||||
const nexe = require("nexe");
|
||||
|
||||
const target = `${os.platform()}-${os.arch()}`;
|
||||
nexe.compile({
|
||||
debugBundle: true,
|
||||
input: path.join(__dirname, "../out/cli.js"),
|
||||
output: `cli-${target}`,
|
||||
targets: [target],
|
||||
/**
|
||||
* To include native extensions, do NOT install node_modules for each one. They
|
||||
* are not required as each extension is built using webpack.
|
||||
*/
|
||||
resources: [
|
||||
path.join(__dirname, "../package.json"),
|
||||
path.join(__dirname, "../build/**/*"),
|
||||
],
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { field, logger } from "@coder/logger";
|
||||
import { ServerMessage, SharedProcessActiveMessage } from "@coder/protocol/src/proto";
|
||||
import { Command, flags } from "@oclif/command";
|
||||
import { ServerMessage, SharedProcessActive } from "@coder/protocol/src/proto";
|
||||
import { ChildProcess, fork, ForkOptions, spawn } from "child_process";
|
||||
import { randomFillSync } from "crypto";
|
||||
import * as fs from "fs";
|
||||
@@ -9,249 +8,281 @@ import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as WebSocket from "ws";
|
||||
import { buildDir, cacheHome, dataHome, isCli, serveStatic } from "./constants";
|
||||
import { fillFs } from "./fill";
|
||||
import { setup as setupNativeModules } from "./modules";
|
||||
import { createApp } from "./server";
|
||||
import { forkModule, requireFork, requireModule } from "./vscode/bootstrapFork";
|
||||
import { SharedProcess, SharedProcessState } from "./vscode/sharedProcess";
|
||||
import opn = require("opn");
|
||||
|
||||
export class Entry extends Command {
|
||||
public static description = "Start your own self-hosted browser-accessible VS Code";
|
||||
public static flags = {
|
||||
cert: flags.string(),
|
||||
"cert-key": flags.string(),
|
||||
"data-dir": flags.string({ char: "d" }),
|
||||
help: flags.help(),
|
||||
host: flags.string({ char: "h", default: "0.0.0.0" }),
|
||||
open: flags.boolean({ char: "o", description: "Open in browser on startup" }),
|
||||
port: flags.integer({ char: "p", default: 8443, description: "Port to bind on" }),
|
||||
version: flags.version({ char: "v" }),
|
||||
"no-auth": flags.boolean({ default: false }),
|
||||
"allow-http": flags.boolean({ default: false }),
|
||||
password: flags.string(),
|
||||
import * as commander from "commander";
|
||||
|
||||
// Dev flags
|
||||
"bootstrap-fork": flags.string({ hidden: true }),
|
||||
"fork": flags.string({ hidden: true }),
|
||||
commander.version(process.env.VERSION || "development")
|
||||
.name("code-server")
|
||||
.description("Run VS Code on a remote server.")
|
||||
.option("--cert <value>")
|
||||
.option("--cert-key <value>")
|
||||
.option("-e, --extensions-dir <dir>", "Set the root path for extensions.")
|
||||
.option("-d --user-data-dir <dir>", " Specifies the directory that user data is kept in, useful when running as root.")
|
||||
.option("--data-dir <value>", "DEPRECATED: Use '--user-data-dir' instead. Customize where user-data is stored.")
|
||||
.option("-h, --host <value>", "Customize the hostname.", "0.0.0.0")
|
||||
.option("-o, --open", "Open in the browser on startup.", false)
|
||||
.option("-p, --port <number>", "Port to bind on.", 8443)
|
||||
.option("-N, --no-auth", "Start without requiring authentication.", undefined)
|
||||
.option("-H, --allow-http", "Allow http connections.", false)
|
||||
.option("-P, --password <value>", "Specify a password for authentication.")
|
||||
.option("--bootstrap-fork <name>", "Used for development. Never set.")
|
||||
.option("--fork <name>", "Used for development. Never set.")
|
||||
.option("--extra-args <args>", "Used for development. Never set.")
|
||||
.arguments("Specify working directory.")
|
||||
.parse(process.argv);
|
||||
|
||||
args: flags.string({ hidden: true }),
|
||||
Error.stackTraceLimit = Infinity;
|
||||
if (isCli) {
|
||||
require("nbin").shimNativeFs(buildDir);
|
||||
}
|
||||
// Makes strings or numbers bold in stdout
|
||||
const bold = (text: string | number): string | number => {
|
||||
return `\u001B[1m${text}\u001B[0m`;
|
||||
};
|
||||
|
||||
(async (): Promise<void> => {
|
||||
const args = commander.args;
|
||||
const options = commander.opts() as {
|
||||
noAuth: boolean;
|
||||
readonly allowHttp: boolean;
|
||||
readonly host: string;
|
||||
readonly port: number;
|
||||
|
||||
readonly userDataDir?: string;
|
||||
readonly extensionsDir?: string;
|
||||
|
||||
readonly dataDir?: string;
|
||||
readonly password?: string;
|
||||
readonly open?: boolean;
|
||||
readonly cert?: string;
|
||||
readonly certKey?: string;
|
||||
|
||||
readonly bootstrapFork?: string;
|
||||
readonly fork?: string;
|
||||
readonly extraArgs?: string;
|
||||
};
|
||||
public static args = [{
|
||||
name: "workdir",
|
||||
description: "Specify working dir",
|
||||
default: (): string => process.cwd(),
|
||||
}];
|
||||
|
||||
public async run(): Promise<void> {
|
||||
if (isCli) {
|
||||
fillFs();
|
||||
}
|
||||
// Commander has an exception for `--no` prefixes. Here we'll adjust that.
|
||||
// tslint:disable-next-line:no-any
|
||||
const noAuthValue = (commander as any).auth;
|
||||
options.noAuth = !noAuthValue;
|
||||
|
||||
const { args, flags } = this.parse(Entry);
|
||||
const dataDir = path.resolve(flags["data-dir"] || path.join(dataHome, "code-server"));
|
||||
const workingDir = path.resolve(args["workdir"]);
|
||||
const dataDir = path.resolve(options.userDataDir || options.dataDir || path.join(dataHome, "code-server"));
|
||||
const extensionsDir = options.extensionsDir ? path.resolve(options.extensionsDir) : path.resolve(dataDir, "extensions");
|
||||
const workingDir = path.resolve(args[0] || process.cwd());
|
||||
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
const oldDataDir = path.resolve(path.join(os.homedir(), ".code-server"));
|
||||
if (fs.existsSync(oldDataDir)) {
|
||||
await fse.move(oldDataDir, dataDir);
|
||||
logger.info(`Moved data directory from ${oldDataDir} to ${dataDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
fse.mkdirp(cacheHome),
|
||||
fse.mkdirp(dataDir),
|
||||
fse.mkdirp(workingDir),
|
||||
]);
|
||||
|
||||
setupNativeModules(dataDir);
|
||||
const builtInExtensionsDir = path.resolve(buildDir || path.join(__dirname, ".."), "build/extensions");
|
||||
if (flags["bootstrap-fork"]) {
|
||||
const modulePath = flags["bootstrap-fork"];
|
||||
if (!modulePath) {
|
||||
logger.error("No module path specified to fork!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
((flags.args ? JSON.parse(flags.args) : []) as string[]).forEach((arg, i) => {
|
||||
// [0] contains the binary running the script (`node` for example) and
|
||||
// [1] contains the script name, so the arguments come after that.
|
||||
process.argv[i + 2] = arg;
|
||||
});
|
||||
|
||||
return requireModule(modulePath, dataDir, builtInExtensionsDir);
|
||||
}
|
||||
|
||||
if (flags["fork"]) {
|
||||
const modulePath = flags["fork"];
|
||||
|
||||
return requireFork(modulePath, JSON.parse(flags.args!), builtInExtensionsDir);
|
||||
}
|
||||
|
||||
const logDir = path.join(cacheHome, "code-server/logs", new Date().toISOString().replace(/[-:.TZ]/g, ""));
|
||||
process.env.VSCODE_LOGS = logDir;
|
||||
|
||||
const certPath = flags.cert ? path.resolve(flags.cert) : undefined;
|
||||
const certKeyPath = flags["cert-key"] ? path.resolve(flags["cert-key"]) : undefined;
|
||||
|
||||
if (certPath && !certKeyPath) {
|
||||
logger.error("'--cert-key' flag is required when specifying a certificate!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!certPath && certKeyPath) {
|
||||
logger.error("'--cert' flag is required when specifying certificate key!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let certData: Buffer | undefined;
|
||||
let certKeyData: Buffer | undefined;
|
||||
|
||||
if (typeof certPath !== "undefined" && typeof certKeyPath !== "undefined") {
|
||||
try {
|
||||
certData = fs.readFileSync(certPath);
|
||||
} catch (ex) {
|
||||
logger.error(`Failed to read certificate: ${ex.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
certKeyData = fs.readFileSync(certKeyPath);
|
||||
} catch (ex) {
|
||||
logger.error(`Failed to read certificate key: ${ex.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`\u001B[1mcode-server ${process.env.VERSION ? `v${process.env.VERSION}` : "development"}`);
|
||||
// TODO: fill in appropriate doc url
|
||||
logger.info("Additional documentation: http://github.com/codercom/code-server");
|
||||
logger.info("Initializing", field("data-dir", dataDir), field("working-dir", workingDir), field("log-dir", logDir));
|
||||
const sharedProcess = new SharedProcess(dataDir, builtInExtensionsDir);
|
||||
const sendSharedProcessReady = (socket: WebSocket): void => {
|
||||
const active = new SharedProcessActiveMessage();
|
||||
active.setSocketPath(sharedProcess.socketPath);
|
||||
active.setLogPath(logDir);
|
||||
const serverMessage = new ServerMessage();
|
||||
serverMessage.setSharedProcessActive(active);
|
||||
socket.send(serverMessage.serializeBinary());
|
||||
};
|
||||
sharedProcess.onState((event) => {
|
||||
if (event.state === SharedProcessState.Ready) {
|
||||
app.wss.clients.forEach((c) => sendSharedProcessReady(c));
|
||||
}
|
||||
});
|
||||
|
||||
let password = flags.password;
|
||||
if (!password) {
|
||||
// Generate a random password with a length of 24.
|
||||
const buffer = Buffer.alloc(12);
|
||||
randomFillSync(buffer);
|
||||
password = buffer.toString("hex");
|
||||
}
|
||||
|
||||
const hasCustomHttps = certData && certKeyData;
|
||||
const app = await createApp({
|
||||
allowHttp: flags["allow-http"],
|
||||
bypassAuth: flags["no-auth"],
|
||||
registerMiddleware: (app): void => {
|
||||
app.use((req, res, next) => {
|
||||
res.on("finish", () => {
|
||||
logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.url}`, field("host", req.hostname), field("ip", req.ip));
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
// If we're not running from the binary and we aren't serving the static
|
||||
// pre-built version, use webpack to serve the web files.
|
||||
if (!isCli && !serveStatic) {
|
||||
const webpackConfig = require(path.resolve(__dirname, "..", "..", "web", "webpack.config.js"));
|
||||
const compiler = require("webpack")(webpackConfig);
|
||||
app.use(require("webpack-dev-middleware")(compiler, {
|
||||
logger,
|
||||
publicPath: webpackConfig.output.publicPath,
|
||||
stats: webpackConfig.stats,
|
||||
}));
|
||||
app.use(require("webpack-hot-middleware")(compiler));
|
||||
}
|
||||
},
|
||||
serverOptions: {
|
||||
builtInExtensionsDirectory: builtInExtensionsDir,
|
||||
dataDirectory: dataDir,
|
||||
workingDirectory: workingDir,
|
||||
cacheDirectory: cacheHome,
|
||||
fork: (modulePath: string, args: string[], options: ForkOptions): ChildProcess => {
|
||||
if (options && options.env && options.env.AMD_ENTRYPOINT) {
|
||||
return forkModule(options.env.AMD_ENTRYPOINT, args, options, dataDir);
|
||||
}
|
||||
|
||||
if (isCli) {
|
||||
return spawn(process.execPath, ["--fork", modulePath, "--args", JSON.stringify(args), "--data-dir", dataDir], {
|
||||
...options,
|
||||
stdio: [null, null, null, "ipc"],
|
||||
});
|
||||
} else {
|
||||
return fork(modulePath, args, options);
|
||||
}
|
||||
},
|
||||
},
|
||||
password,
|
||||
httpsOptions: hasCustomHttps ? {
|
||||
key: certKeyData,
|
||||
cert: certData,
|
||||
} : undefined,
|
||||
});
|
||||
|
||||
logger.info("Starting webserver...", field("host", flags.host), field("port", flags.port));
|
||||
app.server.listen(flags.port, flags.host);
|
||||
let clientId = 1;
|
||||
app.wss.on("connection", (ws, req) => {
|
||||
const id = clientId++;
|
||||
|
||||
if (sharedProcess.state === SharedProcessState.Ready) {
|
||||
sendSharedProcessReady(ws);
|
||||
}
|
||||
|
||||
logger.info(`WebSocket opened \u001B[0m${req.url}`, field("client", id), field("ip", req.socket.remoteAddress));
|
||||
|
||||
ws.on("close", (code) => {
|
||||
logger.info(`WebSocket closed \u001B[0m${req.url}`, field("client", id), field("code", code));
|
||||
});
|
||||
});
|
||||
|
||||
if (!flags["cert-key"] && !flags.cert) {
|
||||
logger.warn("No certificate specified. \u001B[1mThis could be insecure.");
|
||||
// TODO: fill in appropriate doc url
|
||||
logger.warn("Documentation on securing your setup: https://coder.com/docs");
|
||||
}
|
||||
|
||||
if (!flags["no-auth"]) {
|
||||
logger.info(" ");
|
||||
logger.info(`Password:\u001B[1m ${password}`);
|
||||
} else {
|
||||
logger.warn("Launched without authentication.");
|
||||
}
|
||||
|
||||
const url = `http://localhost:${flags.port}/`;
|
||||
logger.info(" ");
|
||||
logger.info("Started (click the link below to open):");
|
||||
logger.info(url);
|
||||
logger.info(" ");
|
||||
|
||||
if (flags.open) {
|
||||
try {
|
||||
await opn(url);
|
||||
} catch (e) {
|
||||
logger.warn("Url couldn't be opened automatically.", field("url", url), field("exception", e));
|
||||
}
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
const oldDataDir = path.resolve(path.join(os.homedir(), ".code-server"));
|
||||
if (fs.existsSync(oldDataDir)) {
|
||||
await fse.move(oldDataDir, dataDir);
|
||||
logger.info(`Moved data directory from ${oldDataDir} to ${dataDir}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Entry.run(undefined, {
|
||||
root: buildDir || __dirname,
|
||||
version: process.env.VERSION || "development",
|
||||
//@ts-ignore
|
||||
}).catch(require("@oclif/errors/handle"));
|
||||
await Promise.all([
|
||||
fse.mkdirp(cacheHome),
|
||||
fse.mkdirp(dataDir),
|
||||
fse.mkdirp(extensionsDir),
|
||||
fse.mkdirp(workingDir),
|
||||
]);
|
||||
|
||||
setupNativeModules(dataDir);
|
||||
const builtInExtensionsDir = path.resolve(buildDir || path.join(__dirname, ".."), "build/extensions");
|
||||
if (options.bootstrapFork) {
|
||||
const modulePath = options.bootstrapFork;
|
||||
if (!modulePath) {
|
||||
logger.error("No module path specified to fork!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
((options.extraArgs ? JSON.parse(options.extraArgs) : []) as string[]).forEach((arg, i) => {
|
||||
// [0] contains the binary running the script (`node` for example) and
|
||||
// [1] contains the script name, so the arguments come after that.
|
||||
process.argv[i + 2] = arg;
|
||||
});
|
||||
|
||||
return requireModule(modulePath, dataDir, builtInExtensionsDir);
|
||||
}
|
||||
|
||||
if (options.fork) {
|
||||
const modulePath = options.fork;
|
||||
|
||||
return requireFork(modulePath, JSON.parse(options.extraArgs!), builtInExtensionsDir);
|
||||
}
|
||||
|
||||
const logDir = path.join(cacheHome, "code-server/logs", new Date().toISOString().replace(/[-:.TZ]/g, ""));
|
||||
process.env.VSCODE_LOGS = logDir;
|
||||
|
||||
const certPath = options.cert ? path.resolve(options.cert) : undefined;
|
||||
const certKeyPath = options.certKey ? path.resolve(options.certKey) : undefined;
|
||||
|
||||
if (certPath && !certKeyPath) {
|
||||
logger.error("'--cert-key' flag is required when specifying a certificate!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!certPath && certKeyPath) {
|
||||
logger.error("'--cert' flag is required when specifying certificate key!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let certData: Buffer | undefined;
|
||||
let certKeyData: Buffer | undefined;
|
||||
|
||||
if (typeof certPath !== "undefined" && typeof certKeyPath !== "undefined") {
|
||||
try {
|
||||
certData = fs.readFileSync(certPath);
|
||||
} catch (ex) {
|
||||
logger.error(`Failed to read certificate: ${ex.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
certKeyData = fs.readFileSync(certKeyPath);
|
||||
} catch (ex) {
|
||||
logger.error(`Failed to read certificate key: ${ex.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`\u001B[1mcode-server ${process.env.VERSION ? `v${process.env.VERSION}` : "development"}`);
|
||||
|
||||
if (options.dataDir) {
|
||||
logger.warn('"--data-dir" is deprecated. Use "--user-data-dir" instead.');
|
||||
}
|
||||
|
||||
// TODO: fill in appropriate doc url
|
||||
logger.info("Additional documentation: http://github.com/codercom/code-server");
|
||||
logger.info("Initializing", field("data-dir", dataDir), field("extensions-dir", extensionsDir), field("working-dir", workingDir), field("log-dir", logDir));
|
||||
const sharedProcess = new SharedProcess(dataDir, extensionsDir, builtInExtensionsDir);
|
||||
const sendSharedProcessReady = (socket: WebSocket): void => {
|
||||
const active = new SharedProcessActive();
|
||||
active.setSocketPath(sharedProcess.socketPath);
|
||||
active.setLogPath(logDir);
|
||||
const serverMessage = new ServerMessage();
|
||||
serverMessage.setSharedProcessActive(active);
|
||||
socket.send(serverMessage.serializeBinary());
|
||||
};
|
||||
sharedProcess.onState((event) => {
|
||||
if (event.state === SharedProcessState.Ready) {
|
||||
app.wss.clients.forEach((c) => sendSharedProcessReady(c));
|
||||
}
|
||||
});
|
||||
|
||||
let password = options.password;
|
||||
if (!password) {
|
||||
// Generate a random password with a length of 24.
|
||||
const buffer = Buffer.alloc(12);
|
||||
randomFillSync(buffer);
|
||||
password = buffer.toString("hex");
|
||||
}
|
||||
|
||||
const hasCustomHttps = certData && certKeyData;
|
||||
const app = await createApp({
|
||||
allowHttp: options.allowHttp,
|
||||
bypassAuth: options.noAuth,
|
||||
registerMiddleware: (app): void => {
|
||||
app.use((req, res, next) => {
|
||||
res.on("finish", () => {
|
||||
logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.url}`, field("host", req.hostname), field("ip", req.ip));
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
// If we're not running from the binary and we aren't serving the static
|
||||
// pre-built version, use webpack to serve the web files.
|
||||
if (!isCli && !serveStatic) {
|
||||
const webpackConfig = require(path.resolve(__dirname, "..", "..", "web", "webpack.config.js"));
|
||||
const compiler = require("webpack")(webpackConfig);
|
||||
app.use(require("webpack-dev-middleware")(compiler, {
|
||||
logger,
|
||||
publicPath: webpackConfig.output.publicPath,
|
||||
stats: webpackConfig.stats,
|
||||
}));
|
||||
app.use(require("webpack-hot-middleware")(compiler));
|
||||
}
|
||||
},
|
||||
serverOptions: {
|
||||
extensionsDirectory: extensionsDir,
|
||||
builtInExtensionsDirectory: builtInExtensionsDir,
|
||||
dataDirectory: dataDir,
|
||||
workingDirectory: workingDir,
|
||||
cacheDirectory: cacheHome,
|
||||
fork: (modulePath: string, args?: string[], options?: ForkOptions): ChildProcess => {
|
||||
if (options && options.env && options.env.AMD_ENTRYPOINT) {
|
||||
return forkModule(options.env.AMD_ENTRYPOINT, args, options, dataDir);
|
||||
}
|
||||
|
||||
if (isCli) {
|
||||
return spawn(process.execPath, [path.join(buildDir, "out", "cli.js"), "--fork", modulePath, "--extra-args", JSON.stringify(args), "--data-dir", dataDir], {
|
||||
...options,
|
||||
stdio: [null, null, null, "ipc"],
|
||||
});
|
||||
} else {
|
||||
return fork(modulePath, args, options);
|
||||
}
|
||||
},
|
||||
},
|
||||
password,
|
||||
httpsOptions: hasCustomHttps ? {
|
||||
key: certKeyData,
|
||||
cert: certData,
|
||||
} : undefined,
|
||||
});
|
||||
|
||||
logger.info("Starting webserver...", field("host", options.host), field("port", options.port));
|
||||
app.server.listen(options.port, options.host);
|
||||
let clientId = 1;
|
||||
app.wss.on("connection", (ws, req) => {
|
||||
const id = clientId++;
|
||||
|
||||
if (sharedProcess.state === SharedProcessState.Ready) {
|
||||
sendSharedProcessReady(ws);
|
||||
}
|
||||
|
||||
logger.info(`WebSocket opened \u001B[0m${req.url}`, field("client", id), field("ip", req.socket.remoteAddress));
|
||||
|
||||
ws.on("close", (code) => {
|
||||
logger.info(`WebSocket closed \u001B[0m${req.url}`, field("client", id), field("code", code));
|
||||
});
|
||||
});
|
||||
app.wss.on("error", (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === "EADDRINUSE") {
|
||||
logger.error(`Port ${bold(options.port)} is in use. Please free up port ${options.port} or specify a different port with the -p flag`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
if (!options.certKey && !options.cert) {
|
||||
logger.warn("No certificate specified. \u001B[1mThis could be insecure.");
|
||||
// TODO: fill in appropriate doc url
|
||||
logger.warn("Documentation on securing your setup: https://github.com/codercom/code-server/blob/master/doc/security/ssl.md");
|
||||
}
|
||||
|
||||
if (!options.noAuth) {
|
||||
logger.info(" ");
|
||||
logger.info(`Password:\u001B[1m ${password}`);
|
||||
} else {
|
||||
logger.warn("Launched without authentication.");
|
||||
}
|
||||
|
||||
const url = `http://localhost:${options.port}/`;
|
||||
logger.info(" ");
|
||||
logger.info("Started (click the link below to open):");
|
||||
logger.info(url);
|
||||
logger.info(" ");
|
||||
|
||||
if (options.open) {
|
||||
try {
|
||||
await opn(url);
|
||||
} catch (e) {
|
||||
logger.warn("Url couldn't be opened automatically.", field("url", url), field("exception", e));
|
||||
}
|
||||
}
|
||||
})().catch((ex) => {
|
||||
logger.error(ex);
|
||||
});
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as util from "util";
|
||||
import { isCli, buildDir } from "./constants";
|
||||
|
||||
// tslint:disable:no-any
|
||||
const nativeFs = (<any>global).nativeFs as typeof fs || {};
|
||||
const oldAccess = fs.access;
|
||||
const existsWithinBinary = (path: fs.PathLike): Promise<boolean> => {
|
||||
return new Promise<boolean>((resolve): void => {
|
||||
if (typeof path === "number") {
|
||||
if (path < 0) {
|
||||
return resolve(true);
|
||||
}
|
||||
}
|
||||
oldAccess(path, fs.constants.F_OK, (err) => {
|
||||
const exists = !err;
|
||||
const es = fs.existsSync(path);
|
||||
const res = !exists && es;
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const fillFs = (): void => {
|
||||
/**
|
||||
* Refer to https://github.com/nexe/nexe/blob/master/src/fs/patch.ts
|
||||
* For impls
|
||||
*/
|
||||
|
||||
if (!isCli) {
|
||||
throw new Error("Should not fill FS when not in CLI");
|
||||
}
|
||||
|
||||
interface FD {
|
||||
readonly path: string;
|
||||
position: number;
|
||||
}
|
||||
|
||||
let lastFd = Number.MIN_SAFE_INTEGER;
|
||||
const fds = new Map<number, FD>();
|
||||
|
||||
const replaceNative = <T extends keyof typeof fs>(propertyName: T, func: (callOld: () => void, ...args: any[]) => any, customPromisify?: (...args: any[]) => Promise<any>): void => {
|
||||
const oldFunc = (<any>fs)[propertyName];
|
||||
fs[propertyName] = (...args: any[]): any => {
|
||||
try {
|
||||
return func(() => {
|
||||
return oldFunc(...args);
|
||||
}, ...args);
|
||||
} catch (ex) {
|
||||
return oldFunc(...args);
|
||||
}
|
||||
};
|
||||
if (customPromisify) {
|
||||
(<any>fs[propertyName])[util.promisify.custom] = (...args: any[]): any => {
|
||||
return customPromisify(...args).catch((ex) => {
|
||||
throw ex;
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
replaceNative("access", (callNative, path, mode, callback) => {
|
||||
existsWithinBinary(path).then((exists) => {
|
||||
if (!exists) {
|
||||
return callNative();
|
||||
}
|
||||
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
|
||||
replaceNative("exists", (callOld, path, callback) => {
|
||||
existsWithinBinary(path).then((exists) => {
|
||||
if (exists) {
|
||||
return callback(true);
|
||||
}
|
||||
|
||||
return callOld();
|
||||
});
|
||||
}, (path) => new Promise((res): void => fs.exists(path, res)));
|
||||
|
||||
replaceNative("open", (callOld, path: fs.PathLike, flags: string | Number, mode: any, callback: any) => {
|
||||
existsWithinBinary(path).then((exists) => {
|
||||
if (!exists) {
|
||||
return callOld();
|
||||
}
|
||||
|
||||
if (typeof mode === "function") {
|
||||
callback = mode;
|
||||
mode = undefined;
|
||||
}
|
||||
|
||||
if (path === process.execPath) {
|
||||
return callOld();
|
||||
}
|
||||
|
||||
const fd = lastFd++;
|
||||
fds.set(fd, {
|
||||
path: path.toString(),
|
||||
position: 0,
|
||||
});
|
||||
callback(undefined, fd);
|
||||
});
|
||||
});
|
||||
|
||||
replaceNative("close", (callOld, fd: number, callback) => {
|
||||
if (!fds.has(fd)) {
|
||||
return callOld();
|
||||
}
|
||||
|
||||
fds.delete(fd);
|
||||
callback();
|
||||
});
|
||||
|
||||
replaceNative("read", (callOld, fd: number, buffer: Buffer, offset: number, length: number, position: number | null, callback?: (err: NodeJS.ErrnoException, bytesRead: number, buffer: Buffer) => void) => {
|
||||
if (!fds.has(fd)) {
|
||||
return callOld();
|
||||
}
|
||||
|
||||
const fileDesc = fds.get(fd)!;
|
||||
|
||||
return fs.readFile(fileDesc.path, (err, rb) => {
|
||||
if (err) {
|
||||
return callOld();
|
||||
}
|
||||
|
||||
rb = rb.slice(position || fileDesc.position);
|
||||
const sliced = rb.slice(0, length);
|
||||
if (position === null) {
|
||||
fileDesc.position += sliced.byteLength;
|
||||
}
|
||||
buffer.set(sliced, offset);
|
||||
if (callback) {
|
||||
callback(undefined!, sliced.byteLength, buffer);
|
||||
}
|
||||
});
|
||||
}, (fd: number, buffer: Buffer, offset: number, length: number, position: number | null): Promise<{
|
||||
bytesRead: number;
|
||||
buffer: Buffer;
|
||||
}> => {
|
||||
return new Promise((res, rej): void => {
|
||||
fs.read(fd, buffer, offset, length, position, (err, bytesRead, buffer) => {
|
||||
if (err) {
|
||||
return rej(err);
|
||||
}
|
||||
|
||||
res({
|
||||
bytesRead,
|
||||
buffer,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
replaceNative("readdir", (callOld, directory: string, callback: (err: NodeJS.ErrnoException, paths: string[]) => void) => {
|
||||
const relative = path.relative(directory, buildDir!);
|
||||
if (relative.startsWith("..")) {
|
||||
return callOld();
|
||||
}
|
||||
|
||||
return nativeFs.readdir(directory, callback);
|
||||
});
|
||||
|
||||
const fillNativeFunc = <T extends keyof typeof fs>(propertyName: T): void => {
|
||||
replaceNative(propertyName, (callOld, newPath, ...args) => {
|
||||
if (typeof newPath !== "string") {
|
||||
return callOld();
|
||||
}
|
||||
|
||||
const rel = path.relative(newPath, buildDir!);
|
||||
if (rel.startsWith("..")) {
|
||||
return callOld();
|
||||
}
|
||||
|
||||
const func = nativeFs[propertyName] as any;
|
||||
|
||||
return func(newPath, ...args);
|
||||
});
|
||||
};
|
||||
|
||||
const properties: Array<keyof typeof fs> = [
|
||||
"existsSync",
|
||||
"readFile",
|
||||
"readFileSync",
|
||||
"createReadStream",
|
||||
"readdir",
|
||||
"readdirSync",
|
||||
"statSync",
|
||||
"stat",
|
||||
"realpath",
|
||||
"realpathSync",
|
||||
];
|
||||
properties.forEach((p) => fillNativeFunc(p));
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
//@ts-ignore
|
||||
import * as netstat from "node-netstat";
|
||||
import { Event, Emitter } from "@coder/events";
|
||||
import { logger } from "@coder/logger";
|
||||
|
||||
export interface PortScanner {
|
||||
readonly ports: ReadonlyArray<number>;
|
||||
@@ -16,7 +17,7 @@ export interface PortScanner {
|
||||
* Will scan local ports and emit events when ports are added or removed.
|
||||
* Currently only scans TCP ports.
|
||||
*/
|
||||
export const createPortScanner = (scanInterval: number = 250): PortScanner => {
|
||||
export const createPortScanner = (scanInterval: number = 5000): PortScanner => {
|
||||
const ports = new Map<number, number>();
|
||||
|
||||
const addEmitter = new Emitter<number[]>();
|
||||
@@ -75,11 +76,14 @@ export const createPortScanner = (scanInterval: number = 250): PortScanner => {
|
||||
let disposed: boolean = false;
|
||||
|
||||
const doInterval = (): void => {
|
||||
scan(() => {
|
||||
if (disposed) {
|
||||
return;
|
||||
logger.trace("scanning ports");
|
||||
scan((error) => {
|
||||
if (error) {
|
||||
logger.error(`Port scanning will not be available: ${error.message}.`);
|
||||
disposed = true;
|
||||
} else if (!disposed) {
|
||||
lastTimeout = setTimeout(doInterval, scanInterval);
|
||||
}
|
||||
lastTimeout = setTimeout(doInterval, scanInterval);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { mkdirp } from "fs-extra";
|
||||
import { logger, field } from "@coder/logger";
|
||||
import { field, logger } from "@coder/logger";
|
||||
import { ReadWriteConnection } from "@coder/protocol";
|
||||
import { Server, ServerOptions } from "@coder/protocol/src/node/server";
|
||||
import { TunnelCloseCode } from "@coder/tunnel/src/common";
|
||||
import { handle as handleTunnel } from "@coder/tunnel/src/server";
|
||||
import * as express from "express";
|
||||
//@ts-ignore
|
||||
import * as expressStaticGzip from "express-static-gzip";
|
||||
import * as fs from "fs";
|
||||
import { mkdirp } from "fs-extra";
|
||||
import * as http from "http";
|
||||
//@ts-ignore
|
||||
import * as httpolyglot from "httpolyglot";
|
||||
@@ -17,11 +19,9 @@ import * as path from "path";
|
||||
import * as pem from "pem";
|
||||
import * as util from "util";
|
||||
import * as ws from "ws";
|
||||
import safeCompare = require("safe-compare");
|
||||
import { TunnelCloseCode } from "@coder/tunnel/src/common";
|
||||
import { handle as handleTunnel } from "@coder/tunnel/src/server";
|
||||
import { createPortScanner } from "./portScanner";
|
||||
import { buildDir } from "./constants";
|
||||
import { createPortScanner } from "./portScanner";
|
||||
import safeCompare = require("safe-compare");
|
||||
|
||||
interface CreateAppOptions {
|
||||
registerMiddleware?: (app: express.Application) => void;
|
||||
@@ -180,10 +180,13 @@ export const createApp = async (options: CreateAppOptions): Promise<{
|
||||
logger.error(error.message);
|
||||
}
|
||||
},
|
||||
onUp: (): void => undefined, // This can't come back up.
|
||||
onDown: (cb): void => ws.addEventListener("close", () => cb()),
|
||||
onClose: (cb): void => ws.addEventListener("close", () => cb()),
|
||||
};
|
||||
|
||||
const server = new Server(connection, options.serverOptions);
|
||||
// tslint:disable-next-line no-unused-expression
|
||||
new Server(connection, options.serverOptions);
|
||||
});
|
||||
|
||||
const baseDir = buildDir || path.join(__dirname, "..");
|
||||
@@ -202,6 +205,10 @@ export const createApp = async (options: CreateAppOptions): Promise<{
|
||||
unauthStaticFunc(req, res, next);
|
||||
}
|
||||
});
|
||||
// @ts-ignore
|
||||
app.use((err, req, res, next) => {
|
||||
next();
|
||||
});
|
||||
app.get("/ping", (req, res) => {
|
||||
res.json({
|
||||
hostname: os.hostname(),
|
||||
@@ -235,9 +242,11 @@ export const createApp = async (options: CreateAppOptions): Promise<{
|
||||
}
|
||||
const content = await util.promisify(fs.readFile)(fullPath);
|
||||
|
||||
res.header("Content-Type", mimeType as string);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": mimeType,
|
||||
"Content-Length": content.byteLength,
|
||||
});
|
||||
res.write(content);
|
||||
res.status(200);
|
||||
res.end();
|
||||
} catch (ex) {
|
||||
res.write(ex.toString());
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as cp from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as zlib from "zlib";
|
||||
import * as vm from "vm";
|
||||
import { isCli } from "../constants";
|
||||
import { logger } from "@coder/logger";
|
||||
import { buildDir, isCli } from "../constants";
|
||||
|
||||
let ipcMsgBuffer: Buffer[] | undefined = [];
|
||||
let ipcMsgListener = process.send ? (d: Buffer): number => ipcMsgBuffer!.push(d) : undefined;
|
||||
@@ -11,6 +12,8 @@ if (ipcMsgListener) {
|
||||
process.on("message", ipcMsgListener);
|
||||
}
|
||||
|
||||
declare var __non_webpack_require__: typeof require;
|
||||
|
||||
/**
|
||||
* Requires a module from the filesystem.
|
||||
*
|
||||
@@ -29,7 +32,7 @@ const requireFilesystemModule = (id: string, builtInExtensionsDir: string): any
|
||||
const fileName = id.endsWith(".js") ? id : `${id}.js`;
|
||||
const req = vm.runInThisContext(mod.wrap(fs.readFileSync(fileName).toString()), {
|
||||
displayErrors: true,
|
||||
filename: id + fileName,
|
||||
filename: fileName,
|
||||
});
|
||||
req(customMod.exports, customMod.require.bind(customMod), customMod, fileName, path.dirname(id));
|
||||
|
||||
@@ -46,6 +49,14 @@ export const requireFork = (modulePath: string, args: string[], builtInExtension
|
||||
const Module = require("module") as typeof import("module");
|
||||
const oldRequire = Module.prototype.require;
|
||||
// tslint:disable-next-line:no-any
|
||||
const oldLoad = (Module as any)._findPath;
|
||||
// @ts-ignore
|
||||
(Module as any)._findPath = function (request, parent, isMain): any {
|
||||
const lookupPaths = oldLoad.call(this, request, parent, isMain);
|
||||
|
||||
return lookupPaths;
|
||||
};
|
||||
// tslint:disable-next-line:no-any
|
||||
Module.prototype.require = function (id: string): any {
|
||||
if (id === "typescript") {
|
||||
return require("typescript");
|
||||
@@ -96,23 +107,20 @@ export const requireModule = (modulePath: string, dataDir: string, builtInExtens
|
||||
*/
|
||||
// tslint:disable-next-line:no-any
|
||||
(<any>cp).fork = (modulePath: string, args: ReadonlyArray<string> = [], options?: cp.ForkOptions): cp.ChildProcess => {
|
||||
return cp.spawn(process.execPath, ["--fork", modulePath, "--args", JSON.stringify(args), "--data-dir", dataDir], {
|
||||
return cp.spawn(process.execPath, [path.join(buildDir, "out", "cli.js"), "--fork", modulePath, "--extra-args", JSON.stringify(args), "--data-dir", dataDir], {
|
||||
...options,
|
||||
stdio: [null, null, null, "ipc"],
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
let content: Buffer | undefined;
|
||||
const readFile = (name: string): Buffer => {
|
||||
return fs.readFileSync(path.join(process.env.BUILD_DIR as string || path.join(__dirname, "../.."), "./build", name));
|
||||
};
|
||||
const baseDir = path.join(buildDir, "build");
|
||||
if (isCli) {
|
||||
content = zlib.gunzipSync(readFile("bootstrap-fork.js.gz"));
|
||||
__non_webpack_require__(path.join(baseDir, "bootstrap-fork.js.gz"));
|
||||
} else {
|
||||
content = readFile("../../vscode/out/bootstrap-fork.js");
|
||||
// We need to check `isCli` here to confuse webpack.
|
||||
require(path.join(__dirname, isCli ? "" : "../../../vscode/out/bootstrap-fork.js"));
|
||||
}
|
||||
eval(content.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -123,28 +131,35 @@ export const requireModule = (modulePath: string, dataDir: string, builtInExtens
|
||||
* cp.stderr.on("data", (data) => console.log(data.toString("utf8")));
|
||||
* @param modulePath Path of the VS Code module to load.
|
||||
*/
|
||||
export const forkModule = (modulePath: string, args: string[], options: cp.ForkOptions, dataDir?: string): cp.ChildProcess => {
|
||||
export const forkModule = (modulePath: string, args?: string[], options?: cp.ForkOptions, dataDir?: string): cp.ChildProcess => {
|
||||
let proc: cp.ChildProcess;
|
||||
const forkOptions: cp.ForkOptions = {
|
||||
stdio: [null, null, null, "ipc"],
|
||||
};
|
||||
if (options.env) {
|
||||
if (options && options.env) {
|
||||
// This prevents vscode from trying to load original-fs from electron.
|
||||
delete options.env.ELECTRON_RUN_AS_NODE;
|
||||
forkOptions.env = options.env;
|
||||
}
|
||||
const forkArgs = ["--bootstrap-fork", modulePath];
|
||||
if (args) {
|
||||
forkArgs.push("--args", JSON.stringify(args));
|
||||
forkArgs.push("--extra-args", JSON.stringify(args));
|
||||
}
|
||||
if (dataDir) {
|
||||
forkArgs.push("--data-dir", dataDir);
|
||||
}
|
||||
if (isCli) {
|
||||
proc = cp.spawn(process.execPath, forkArgs, forkOptions);
|
||||
proc = cp.spawn(process.execPath, [path.join(buildDir, "out", "cli.js"), ...forkArgs], forkOptions);
|
||||
} else {
|
||||
proc = cp.spawn(process.execPath, ["--require", "ts-node/register", "--require", "tsconfig-paths/register", process.argv[1], ...forkArgs], forkOptions);
|
||||
}
|
||||
if (args && args[0] === "--type=watcherService" && os.platform() === "linux") {
|
||||
cp.exec(`renice -n 19 -p ${proc.pid}`, (error) => {
|
||||
if (error) {
|
||||
logger.warn(error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return proc;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChildProcess } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as fse from "fs-extra";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { forkModule } from "./bootstrapFork";
|
||||
@@ -7,7 +8,7 @@ import { StdioIpcHandler } from "../ipc";
|
||||
import { ParsedArgs } from "vs/platform/environment/common/environment";
|
||||
import { Emitter } from "@coder/events/src";
|
||||
import { retry } from "@coder/ide/src/retry";
|
||||
import { logger, field, Level } from "@coder/logger";
|
||||
import { logger, Level } from "@coder/logger";
|
||||
|
||||
export enum SharedProcessState {
|
||||
Stopped,
|
||||
@@ -23,123 +24,144 @@ export type SharedProcessEvent = {
|
||||
};
|
||||
|
||||
export class SharedProcess {
|
||||
public readonly socketPath: string = os.platform() === "win32" ? path.join("\\\\?\\pipe", os.tmpdir(), `.code-server${Math.random().toString()}`) : path.join(os.tmpdir(), `.code-server${Math.random().toString()}`);
|
||||
public readonly socketPath: string = os.platform() === "win32"
|
||||
? path.join("\\\\?\\pipe", os.tmpdir(), `.code-server${Math.random().toString()}`)
|
||||
: path.join(os.tmpdir(), `.code-server${Math.random().toString()}`);
|
||||
private _state: SharedProcessState = SharedProcessState.Stopped;
|
||||
private activeProcess: ChildProcess | undefined;
|
||||
private ipcHandler: StdioIpcHandler | undefined;
|
||||
private readonly onStateEmitter = new Emitter<SharedProcessEvent>();
|
||||
public readonly onState = this.onStateEmitter.event;
|
||||
private readonly retryName = "Shared process";
|
||||
private readonly logger = logger.named("shared");
|
||||
private readonly retry = retry.register("Shared process", () => this.connect());
|
||||
private disposed: boolean = false;
|
||||
|
||||
public constructor(
|
||||
private readonly userDataDir: string,
|
||||
private readonly extensionsDir: string,
|
||||
private readonly builtInExtensionsDir: string,
|
||||
) {
|
||||
retry.register(this.retryName, () => this.restart());
|
||||
retry.run(this.retryName);
|
||||
this.retry.run();
|
||||
}
|
||||
|
||||
public get state(): SharedProcessState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public restart(): void {
|
||||
if (this.activeProcess && !this.activeProcess.killed) {
|
||||
this.activeProcess.kill();
|
||||
}
|
||||
|
||||
const extensionsDir = path.join(this.userDataDir, "extensions");
|
||||
const mkdir = (dir: string): void => {
|
||||
try {
|
||||
fs.mkdirSync(dir);
|
||||
} catch (ex) {
|
||||
if (ex.code !== "EEXIST" && ex.code !== "EISDIR") {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
};
|
||||
mkdir(this.userDataDir);
|
||||
mkdir(extensionsDir);
|
||||
|
||||
this.setState({
|
||||
state: SharedProcessState.Starting,
|
||||
});
|
||||
let resolved: boolean = false;
|
||||
const maybeStop = (error: string): void => {
|
||||
if (resolved) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
error,
|
||||
state: SharedProcessState.Stopped,
|
||||
});
|
||||
if (!this.activeProcess) {
|
||||
return;
|
||||
}
|
||||
this.activeProcess.kill();
|
||||
};
|
||||
this.activeProcess = forkModule("vs/code/electron-browser/sharedProcess/sharedProcessMain", [], {
|
||||
env: {
|
||||
VSCODE_ALLOW_IO: "true",
|
||||
VSCODE_LOGS: process.env.VSCODE_LOGS,
|
||||
},
|
||||
}, this.userDataDir);
|
||||
if (this.logger.level <= Level.Trace) {
|
||||
this.activeProcess.stdout.on("data", (data) => {
|
||||
this.logger.trace(() => ["stdout", field("data", data.toString())]);
|
||||
});
|
||||
}
|
||||
this.activeProcess.on("error", (error) => {
|
||||
this.logger.error("error", field("error", error));
|
||||
maybeStop(error.message);
|
||||
});
|
||||
this.activeProcess.on("exit", (err) => {
|
||||
if (this._state !== SharedProcessState.Stopped) {
|
||||
this.setState({
|
||||
error: `Exited with ${err}`,
|
||||
state: SharedProcessState.Stopped,
|
||||
});
|
||||
}
|
||||
retry.run(this.retryName, new Error(`Exited with ${err}`));
|
||||
});
|
||||
this.ipcHandler = new StdioIpcHandler(this.activeProcess);
|
||||
this.ipcHandler.once("handshake:hello", () => {
|
||||
const data: {
|
||||
sharedIPCHandle: string;
|
||||
args: Partial<ParsedArgs>;
|
||||
logLevel: Level;
|
||||
} = {
|
||||
args: {
|
||||
"builtin-extensions-dir": this.builtInExtensionsDir,
|
||||
"user-data-dir": this.userDataDir,
|
||||
"extensions-dir": extensionsDir,
|
||||
},
|
||||
logLevel: this.logger.level,
|
||||
sharedIPCHandle: this.socketPath,
|
||||
};
|
||||
this.ipcHandler!.send("handshake:hey there", "", data);
|
||||
});
|
||||
this.ipcHandler.once("handshake:im ready", () => {
|
||||
resolved = true;
|
||||
retry.recover(this.retryName);
|
||||
this.setState({
|
||||
state: SharedProcessState.Ready,
|
||||
});
|
||||
});
|
||||
this.activeProcess.stderr.on("data", (data) => {
|
||||
this.logger.error("stderr", field("data", data.toString()));
|
||||
maybeStop(data.toString());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal the shared process to terminate.
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.disposed = true;
|
||||
if (this.ipcHandler) {
|
||||
this.ipcHandler.send("handshake:goodbye");
|
||||
}
|
||||
this.ipcHandler = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start and connect to the shared process.
|
||||
*/
|
||||
private async connect(): Promise<void> {
|
||||
this.setState({ state: SharedProcessState.Starting });
|
||||
const activeProcess = await this.restart();
|
||||
|
||||
activeProcess.stderr.on("data", (data) => {
|
||||
// Warn instead of error to prevent panic. It's unlikely stderr here is
|
||||
// about anything critical to the functioning of the editor.
|
||||
logger.warn(data.toString());
|
||||
});
|
||||
|
||||
activeProcess.on("exit", (exitCode) => {
|
||||
const error = new Error(`Exited with ${exitCode}`);
|
||||
this.setState({
|
||||
error: error.message,
|
||||
state: SharedProcessState.Stopped,
|
||||
});
|
||||
if (!this.disposed) {
|
||||
this.retry.run(error);
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({ state: SharedProcessState.Ready });
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the shared process. Kill existing process if running. Resolve when
|
||||
* the shared process is ready and reject when it errors or dies before being
|
||||
* ready.
|
||||
*/
|
||||
private async restart(): Promise<ChildProcess> {
|
||||
if (this.activeProcess && !this.activeProcess.killed) {
|
||||
this.activeProcess.kill();
|
||||
}
|
||||
|
||||
const backupsDir = path.join(this.userDataDir, "Backups");
|
||||
await Promise.all([
|
||||
fse.mkdirp(backupsDir),
|
||||
]);
|
||||
|
||||
const workspacesFile = path.join(backupsDir, "workspaces.json");
|
||||
if (!fs.existsSync(workspacesFile)) {
|
||||
fs.appendFileSync(workspacesFile, "");
|
||||
}
|
||||
|
||||
const activeProcess = forkModule("vs/code/electron-browser/sharedProcess/sharedProcessMain", [], {
|
||||
env: {
|
||||
VSCODE_ALLOW_IO: "true",
|
||||
VSCODE_LOGS: process.env.VSCODE_LOGS,
|
||||
},
|
||||
}, this.userDataDir);
|
||||
this.activeProcess = activeProcess;
|
||||
|
||||
await new Promise((resolve, reject): void => {
|
||||
const doReject = (error: Error | number | null): void => {
|
||||
if (error === null) {
|
||||
error = new Error("Exited unexpectedly");
|
||||
} else if (typeof error === "number") {
|
||||
error = new Error(`Exited with ${error}`);
|
||||
}
|
||||
activeProcess.removeAllListeners();
|
||||
this.setState({
|
||||
error: error.message,
|
||||
state: SharedProcessState.Stopped,
|
||||
});
|
||||
reject(error);
|
||||
};
|
||||
|
||||
activeProcess.on("error", doReject);
|
||||
activeProcess.on("exit", doReject);
|
||||
|
||||
this.ipcHandler = new StdioIpcHandler(activeProcess);
|
||||
this.ipcHandler.once("handshake:hello", () => {
|
||||
const data: {
|
||||
sharedIPCHandle: string;
|
||||
args: Partial<ParsedArgs>;
|
||||
logLevel: Level;
|
||||
} = {
|
||||
args: {
|
||||
"builtin-extensions-dir": this.builtInExtensionsDir,
|
||||
"user-data-dir": this.userDataDir,
|
||||
"extensions-dir": this.extensionsDir,
|
||||
},
|
||||
logLevel: this.logger.level,
|
||||
sharedIPCHandle: this.socketPath,
|
||||
};
|
||||
this.ipcHandler!.send("handshake:hey there", "", data);
|
||||
});
|
||||
this.ipcHandler.once("handshake:im ready", () => {
|
||||
activeProcess.removeListener("error", doReject);
|
||||
activeProcess.removeListener("exit", doReject);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return activeProcess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the internal shared process state and emit the state event.
|
||||
*/
|
||||
private setState(event: SharedProcessEvent): void {
|
||||
this._state = event.state;
|
||||
this.onStateEmitter.emit(event);
|
||||
|
||||
@@ -13,6 +13,7 @@ module.exports = merge(
|
||||
path: path.join(__dirname, "out"),
|
||||
libraryTarget: "commonjs",
|
||||
},
|
||||
mode: "production",
|
||||
node: {
|
||||
console: false,
|
||||
global: false,
|
||||
@@ -27,7 +28,10 @@ module.exports = merge(
|
||||
"node-pty": "node-pty-prebuilt",
|
||||
},
|
||||
},
|
||||
externals: ["tslib"],
|
||||
externals: {
|
||||
"nbin": "commonjs nbin",
|
||||
"fsevents": "fsevents",
|
||||
},
|
||||
entry: "./packages/server/src/cli.ts",
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
3
packages/vscode/.gitignore
vendored
3
packages/vscode/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
bin
|
||||
bin
|
||||
test/.test*
|
||||
@@ -8,7 +8,6 @@
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.4.24",
|
||||
"onigasm": "^2.2.1",
|
||||
"spdlog": "^0.7.2",
|
||||
"string-replace-loader": "^2.1.1",
|
||||
"tar-stream": "^2.0.1"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as util from "util";
|
||||
import { Emitter, Event } from "@coder/events";
|
||||
import { client as ideClient } from "@coder/ide/src/fill/client";
|
||||
import { $, addClass, append } from "vs/base/browser/dom";
|
||||
import { HighlightedLabel } from "vs/base/browser/ui/highlightedlabel/highlightedLabel";
|
||||
import { ObjectTree } from "vs/base/browser/ui/tree/objectTree";
|
||||
@@ -16,8 +16,6 @@ import { IThemeService } from "vs/platform/theme/common/themeService";
|
||||
import { workbench } from "./workbench";
|
||||
import "./dialog.scss";
|
||||
|
||||
declare var __non_webpack_require__: typeof require;
|
||||
|
||||
export enum DialogType {
|
||||
NewFolder,
|
||||
Save,
|
||||
@@ -183,15 +181,15 @@ class Dialog {
|
||||
this.filesNode = document.createElement("div");
|
||||
this.filesNode.className = "files-list";
|
||||
this.entryList = new ObjectTree<DialogEntry, string>(this.filesNode, {
|
||||
getHeight: (entry: DialogEntry): number => {
|
||||
getHeight: (_entry: DialogEntry): number => {
|
||||
return 20;
|
||||
},
|
||||
getTemplateId: (entry: DialogEntry): string => {
|
||||
getTemplateId: (_entry: DialogEntry): string => {
|
||||
return "dialog-entry";
|
||||
},
|
||||
}, [new DialogEntryRenderer()], {
|
||||
openController: {
|
||||
shouldOpen: (event): boolean => {
|
||||
shouldOpen: (_event): boolean => {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
@@ -341,7 +339,6 @@ class Dialog {
|
||||
}
|
||||
|
||||
private set path(directory: string) {
|
||||
const ts = Date.now();
|
||||
this.list(directory).then((value) => {
|
||||
this._path = directory;
|
||||
this.buildPath();
|
||||
@@ -380,32 +377,16 @@ class Dialog {
|
||||
}
|
||||
|
||||
private async list(directory: string): Promise<ReadonlyArray<DialogEntry>> {
|
||||
return ideClient.evaluate((_helper, directory) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
const path = __non_webpack_require__("path") as typeof import("path");
|
||||
const paths = (await util.promisify(fs.readdir)(directory)).sort();
|
||||
const stats = await Promise.all(paths.map(p => util.promisify(fs.stat)(path.join(directory, p))));
|
||||
|
||||
return util.promisify(fs.readdir)(directory).then((paths) => {
|
||||
paths = paths.sort();
|
||||
|
||||
return Promise.all(paths.map(p => util.promisify(fs.stat)(path.join(directory, p)))).then((stats) => {
|
||||
return {
|
||||
paths,
|
||||
stats,
|
||||
};
|
||||
});
|
||||
}).then(({ paths, stats }) => {
|
||||
return stats.map((stat, index): DialogEntry => {
|
||||
return {
|
||||
fullPath: path.join(directory, paths[index]),
|
||||
name: paths[index],
|
||||
isDirectory: stat.isDirectory(),
|
||||
lastModified: stat.mtime.toDateString(),
|
||||
size: stat.size,
|
||||
};
|
||||
});
|
||||
});
|
||||
}, directory);
|
||||
return stats.map((stat, index): DialogEntry => ({
|
||||
fullPath: path.join(directory, paths[index]),
|
||||
name: paths[index],
|
||||
isDirectory: stat.isDirectory(),
|
||||
lastModified: stat.mtime.toDateString(),
|
||||
size: stat.size,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,7 +422,7 @@ class DialogEntryRenderer implements ITreeRenderer<DialogEntry, string, DialogEn
|
||||
};
|
||||
}
|
||||
|
||||
public renderElement(node: ITreeNode<DialogEntry, string>, index: number, templateData: DialogEntryData): void {
|
||||
public renderElement(node: ITreeNode<DialogEntry, string>, _index: number, templateData: DialogEntryData): void {
|
||||
templateData.icon.className = "dialog-entry-icon monaco-icon-label";
|
||||
const classes = getIconClasses(
|
||||
workbench.serviceCollection.get<IModelService>(IModelService) as IModelService,
|
||||
@@ -465,7 +446,7 @@ class DialogEntryRenderer implements ITreeRenderer<DialogEntry, string, DialogEn
|
||||
templateData.lastModified.innerText = node.element.lastModified;
|
||||
}
|
||||
|
||||
public disposeTemplate(templateData: DialogEntryData): void {
|
||||
public disposeTemplate(_templateData: DialogEntryData): void {
|
||||
// throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export class EnvironmentService extends environment.EnvironmentService {
|
||||
}
|
||||
|
||||
public get extensionsPath(): string {
|
||||
return path.join(paths.getAppDataPath(), "extensions");
|
||||
return paths.getExtensionsDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +1,4 @@
|
||||
import { Module } from "@coder/protocol";
|
||||
import { client } from "@coder/ide/src/fill/client";
|
||||
import { EventEmitter } from "events";
|
||||
import * as nodePty from "node-pty";
|
||||
import { ActiveEvalHelper } from "@coder/protocol";
|
||||
import { logger } from "@coder/logger";
|
||||
|
||||
/**
|
||||
* Implementation of nodePty for the browser.
|
||||
*/
|
||||
class Pty implements nodePty.IPty {
|
||||
private readonly emitter = new EventEmitter();
|
||||
private readonly ae: ActiveEvalHelper;
|
||||
private _pid = -1;
|
||||
private _process = "";
|
||||
|
||||
public constructor(file: string, args: string[] | string, options: nodePty.IPtyForkOptions) {
|
||||
this.ae = client.run((ae, file, args, options) => {
|
||||
ae.preserveEnv(options);
|
||||
|
||||
const ptyProc = ae.modules.pty.spawn(file, args, options);
|
||||
|
||||
let process = ptyProc.process;
|
||||
ae.emit("process", process);
|
||||
ae.emit("pid", ptyProc.pid);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (ptyProc.process !== process) {
|
||||
process = ptyProc.process;
|
||||
ae.emit("process", process);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
ptyProc.on("exit", (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
ae.emit("exit", code, signal);
|
||||
});
|
||||
|
||||
ptyProc.on("data", (data) => ae.emit("data", data));
|
||||
|
||||
ae.on("resize", (cols: number, rows: number) => ptyProc.resize(cols, rows));
|
||||
ae.on("write", (data: string) => ptyProc.write(data));
|
||||
ae.on("kill", (signal: string) => ptyProc.kill(signal));
|
||||
|
||||
return {
|
||||
onDidDispose: (cb): void => ptyProc.on("exit", cb),
|
||||
dispose: (): void => {
|
||||
ptyProc.kill();
|
||||
setTimeout(() => ptyProc.kill("SIGKILL"), 5000); // Double tap.
|
||||
},
|
||||
};
|
||||
}, file, args, options);
|
||||
|
||||
this.ae.on("error", (error) => logger.error(error.message));
|
||||
|
||||
this.ae.on("pid", (pid) => this._pid = pid);
|
||||
this.ae.on("process", (process) => this._process = process);
|
||||
|
||||
this.ae.on("exit", (code, signal) => this.emitter.emit("exit", code, signal));
|
||||
this.ae.on("data", (data) => this.emitter.emit("data", data));
|
||||
}
|
||||
|
||||
public get pid(): number {
|
||||
return this._pid;
|
||||
}
|
||||
|
||||
public get process(): string {
|
||||
return this._process;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public on(event: string, listener: (...args: any[]) => void): void {
|
||||
this.emitter.on(event, listener);
|
||||
}
|
||||
|
||||
public resize(columns: number, rows: number): void {
|
||||
this.ae.emit("resize", columns, rows);
|
||||
}
|
||||
|
||||
public write(data: string): void {
|
||||
this.ae.emit("write", data);
|
||||
}
|
||||
|
||||
public kill(signal?: string): void {
|
||||
this.ae.emit("kill", signal);
|
||||
}
|
||||
}
|
||||
|
||||
const ptyType: typeof nodePty = {
|
||||
spawn: (file: string, args: string[] | string, options: nodePty.IPtyForkOptions): nodePty.IPty => {
|
||||
return new Pty(file, args, options);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = ptyType;
|
||||
export = client.modules[Module.NodePty];
|
||||
|
||||
@@ -4,6 +4,7 @@ class Paths {
|
||||
private _appData: string | undefined;
|
||||
private _defaultUserData: string | undefined;
|
||||
private _socketPath: string | undefined;
|
||||
private _extensionsDirectory: string | undefined;
|
||||
private _builtInExtensionsDirectory: string | undefined;
|
||||
private _workingDirectory: string | undefined;
|
||||
|
||||
@@ -31,6 +32,14 @@ class Paths {
|
||||
return this._socketPath;
|
||||
}
|
||||
|
||||
public get extensionsDirectory(): string {
|
||||
if (!this._extensionsDirectory) {
|
||||
throw new Error("trying to access extensions directory before it has been set");
|
||||
}
|
||||
|
||||
return this._extensionsDirectory;
|
||||
}
|
||||
|
||||
public get builtInExtensionsDirectory(): string {
|
||||
if (!this._builtInExtensionsDirectory) {
|
||||
throw new Error("trying to access builtin extensions directory before it has been set");
|
||||
@@ -52,6 +61,7 @@ class Paths {
|
||||
this._appData = data.dataDirectory;
|
||||
this._defaultUserData = data.dataDirectory;
|
||||
this._socketPath = sharedData.socketPath;
|
||||
this._extensionsDirectory = data.extensionsDirectory;
|
||||
this._builtInExtensionsDirectory = data.builtInExtensionsDirectory;
|
||||
this._workingDirectory = data.workingDirectory;
|
||||
}
|
||||
@@ -61,5 +71,6 @@ export const _paths = new Paths();
|
||||
export const getAppDataPath = (): string => _paths.appData;
|
||||
export const getDefaultUserDataPath = (): string => _paths.defaultUserData;
|
||||
export const getWorkingDirectory = (): string => _paths.workingDirectory;
|
||||
export const getExtensionsDirectory = (): string => _paths.extensionsDirectory;
|
||||
export const getBuiltInExtensionsDirectory = (): string => _paths.builtInExtensionsDirectory;
|
||||
export const getSocketPath = (): string => _paths.socketPath;
|
||||
|
||||
@@ -1,63 +1,4 @@
|
||||
import { RotatingLogger as NodeRotatingLogger } from "spdlog";
|
||||
import { logger } from "@coder/logger";
|
||||
import { Module } from "@coder/protocol";
|
||||
import { client } from "@coder/ide/src/fill/client";
|
||||
|
||||
const ae = client.run((ae) => {
|
||||
const loggers = new Map<number, NodeRotatingLogger>();
|
||||
|
||||
ae.on("new", (id: number, name: string, filePath: string, fileSize: number, fileCount: number) => {
|
||||
const logger = new ae.modules.spdlog.RotatingLogger(name, filePath, fileSize, fileCount);
|
||||
loggers.set(id, logger);
|
||||
});
|
||||
|
||||
ae.on("clearFormatters", (id: number) => loggers.get(id)!.clearFormatters());
|
||||
ae.on("critical", (id: number, message: string) => loggers.get(id)!.critical(message));
|
||||
ae.on("debug", (id: number, message: string) => loggers.get(id)!.debug(message));
|
||||
ae.on("drop", (id: number) => loggers.get(id)!.drop());
|
||||
ae.on("errorLog", (id: number, message: string) => loggers.get(id)!.error(message));
|
||||
ae.on("flush", (id: number) => loggers.get(id)!.flush());
|
||||
ae.on("info", (id: number, message: string) => loggers.get(id)!.info(message));
|
||||
ae.on("setAsyncMode", (bufferSize: number, flushInterval: number) => ae.modules.spdlog.setAsyncMode(bufferSize, flushInterval));
|
||||
ae.on("setLevel", (id: number, level: number) => loggers.get(id)!.setLevel(level));
|
||||
ae.on("trace", (id: number, message: string) => loggers.get(id)!.trace(message));
|
||||
ae.on("warn", (id: number, message: string) => loggers.get(id)!.warn(message));
|
||||
|
||||
const disposeCallbacks = <Array<() => void>>[];
|
||||
|
||||
return {
|
||||
onDidDispose: (cb): number => disposeCallbacks.push(cb),
|
||||
dispose: (): void => {
|
||||
loggers.forEach((logger) => logger.flush());
|
||||
loggers.clear();
|
||||
disposeCallbacks.forEach((cb) => cb());
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const spdLogger = logger.named("spdlog");
|
||||
ae.on("close", () => spdLogger.error("session closed prematurely"));
|
||||
ae.on("error", (error: Error) => spdLogger.error(error.message));
|
||||
|
||||
let id = 0;
|
||||
export class RotatingLogger implements NodeRotatingLogger {
|
||||
private readonly id = id++;
|
||||
|
||||
public constructor(name: string, filePath: string, fileSize: number, fileCount: number) {
|
||||
ae.emit("new", this.id, name, filePath, fileSize, fileCount);
|
||||
}
|
||||
|
||||
public trace(message: string): void { ae.emit("trace", this.id, message); }
|
||||
public debug(message: string): void { ae.emit("debug", this.id, message); }
|
||||
public info(message: string): void { ae.emit("info", this.id, message); }
|
||||
public warn(message: string): void { ae.emit("warn", this.id, message); }
|
||||
public error(message: string): void { ae.emit("errorLog", this.id, message); }
|
||||
public critical(message: string): void { ae.emit("critical", this.id, message); }
|
||||
public setLevel(level: number): void { ae.emit("setLevel", this.id, level); }
|
||||
public clearFormatters(): void { ae.emit("clearFormatters", this.id); }
|
||||
public flush(): void { ae.emit("flush", this.id); }
|
||||
public drop(): void { ae.emit("drop", this.id); }
|
||||
}
|
||||
|
||||
export const setAsyncMode = (bufferSize: number, flushInterval: number): void => {
|
||||
ae.emit("setAsyncMode", bufferSize, flushInterval);
|
||||
};
|
||||
export = client.modules[Module.Spdlog];
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from "vs/nls";
|
||||
import * as vszip from "vszip";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as tarStream from "tar-stream";
|
||||
import { promisify } from "util";
|
||||
import { ILogService } from "vs/platform/log/common/log";
|
||||
import { CancellationToken } from "vs/base/common/cancellation";
|
||||
import { mkdirp } from "vs/base/node/pfs";
|
||||
|
||||
@@ -16,8 +16,8 @@ export interface IExtractOptions {
|
||||
overwrite?: boolean;
|
||||
|
||||
/**
|
||||
* Source path within the ZIP archive. Only the files contained in this
|
||||
* path will be extracted.
|
||||
* Source path within the TAR/ZIP archive. Only the files
|
||||
* contained in this path will be extracted.
|
||||
*/
|
||||
sourcePath?: string;
|
||||
}
|
||||
@@ -28,11 +28,15 @@ export interface IFile {
|
||||
localPath?: string;
|
||||
}
|
||||
|
||||
export function zip(tarPath: string, files: IFile[]): Promise<string> {
|
||||
return new Promise<string>((c, e) => {
|
||||
/**
|
||||
* Override the standard VS Code behavior for zipping
|
||||
* extensions to use the TAR format instead of ZIP.
|
||||
*/
|
||||
export const zip = (tarPath: string, files: IFile[]): Promise<string> => {
|
||||
return new Promise<string>((c, e): void => {
|
||||
const pack = tarStream.pack();
|
||||
const chunks: Buffer[] = [];
|
||||
const ended = new Promise<Buffer>((res, rej) => {
|
||||
const ended = new Promise<Buffer>((res): void => {
|
||||
pack.on("end", () => {
|
||||
res(Buffer.concat(chunks));
|
||||
});
|
||||
@@ -56,132 +60,160 @@ export function zip(tarPath: string, files: IFile[]): Promise<string> {
|
||||
e(ex);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export async function extract(tarPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> {
|
||||
const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : '');
|
||||
/**
|
||||
* Override the standard VS Code behavior for extracting
|
||||
* archives, to first attempt to process the archive as a TAR
|
||||
* and then fallback on the original implementation, for processing
|
||||
* ZIPs.
|
||||
*/
|
||||
export const extract = (archivePath: string, extractPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> => {
|
||||
return new Promise<void>((c, e): void => {
|
||||
extractTar(archivePath, extractPath, options, token).then(c).catch((ex) => {
|
||||
if (!ex.toString().includes("Invalid tar header")) {
|
||||
e(ex);
|
||||
|
||||
return new Promise<void>(async (c, e) => {
|
||||
const buffer = await promisify(fs.readFile)(tarPath);
|
||||
const extractor = tarStream.extract();
|
||||
extractor.once('error', e);
|
||||
extractor.on('entry', (header, stream, next) => {
|
||||
const rawName = header.name;
|
||||
|
||||
const nextEntry = (): void => {
|
||||
stream.resume();
|
||||
next();
|
||||
};
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
return nextEntry();
|
||||
}
|
||||
|
||||
if (!sourcePathRegex.test(rawName)) {
|
||||
return nextEntry();
|
||||
}
|
||||
|
||||
const fileName = rawName.replace(sourcePathRegex, '');
|
||||
|
||||
const targetFileName = path.join(targetPath, fileName);
|
||||
if (/\/$/.test(fileName)) {
|
||||
stream.resume();
|
||||
mkdirp(targetFileName).then(() => {
|
||||
next();
|
||||
}, e);
|
||||
return;
|
||||
}
|
||||
|
||||
const dirName = path.dirname(fileName);
|
||||
const targetDirName = path.join(targetPath, dirName);
|
||||
if (targetDirName.indexOf(targetPath) !== 0) {
|
||||
e(nls.localize('invalid file', "Error extracting {0}. Invalid file.", fileName));
|
||||
return nextEntry();
|
||||
}
|
||||
|
||||
mkdirp(targetDirName, void 0, token).then(() => {
|
||||
const fstream = fs.createWriteStream(targetFileName, { mode: header.mode });
|
||||
fstream.once('close', () => {
|
||||
next();
|
||||
});
|
||||
fstream.once('error', (err) => {
|
||||
e(err);
|
||||
});
|
||||
stream.pipe(fstream);
|
||||
stream.resume();
|
||||
});
|
||||
vszip.extract(archivePath, extractPath, options, token).then(c).catch(e);
|
||||
});
|
||||
extractor.once('finish', () => {
|
||||
c();
|
||||
});
|
||||
extractor.write(buffer);
|
||||
extractor.end();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function buffer(tarPath: string, filePath: string): Promise<Buffer> {
|
||||
return new Promise<Buffer>(async (c, e) => {
|
||||
/**
|
||||
* Override the standard VS Code behavior for buffering
|
||||
* archives, to first process the Buffer as a TAR and then
|
||||
* fallback on the original implementation, for processing ZIPs.
|
||||
*/
|
||||
export const buffer = (targetPath: string, filePath: string): Promise<Buffer> => {
|
||||
return new Promise<Buffer>((c, e): void => {
|
||||
let done: boolean = false;
|
||||
extractAssets(tarPath, new RegExp(filePath), (path: string, data: Buffer) => {
|
||||
if (path === filePath) {
|
||||
extractAssets(targetPath, new RegExp(filePath), (assetPath: string, data: Buffer) => {
|
||||
if (path.normalize(assetPath) === path.normalize(filePath)) {
|
||||
done = true;
|
||||
c(data);
|
||||
}
|
||||
}).then(() => {
|
||||
if (!done) {
|
||||
e("couldnt find asset " + filePath);
|
||||
e("couldn't find asset " + filePath);
|
||||
}
|
||||
}).catch((ex) => {
|
||||
e(ex);
|
||||
if (!ex.toString().includes("Invalid tar header")) {
|
||||
e(ex);
|
||||
|
||||
return;
|
||||
}
|
||||
vszip.buffer(targetPath, filePath).then(c).catch(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async function extractAssets(tarPath: string, match: RegExp, callback: (path: string, data: Buffer) => void): Promise<void> {
|
||||
const buffer = await promisify(fs.readFile)(tarPath);
|
||||
const extractor = tarStream.extract();
|
||||
let callbackResolve: () => void;
|
||||
let callbackReject: (ex?) => void;
|
||||
const complete = new Promise<void>((r, rej) => {
|
||||
callbackResolve = r;
|
||||
callbackReject = rej;
|
||||
});
|
||||
extractor.once("error", (err) => {
|
||||
callbackReject(err);
|
||||
});
|
||||
extractor.on("entry", (header, stream, next) => {
|
||||
const name = header.name;
|
||||
if (match.test(name)) {
|
||||
extractData(stream).then((data) => {
|
||||
callback(name, data);
|
||||
next();
|
||||
/**
|
||||
* Override the standard VS Code behavior for extracting assets
|
||||
* from archive Buffers to use the TAR format instead of ZIP.
|
||||
*/
|
||||
export const extractAssets = (tarPath: string, match: RegExp, callback: (path: string, data: Buffer) => void): Promise<void> => {
|
||||
return new Promise<void>(async (c, e): Promise<void> => {
|
||||
try {
|
||||
const buffer = await promisify(fs.readFile)(tarPath);
|
||||
const extractor = tarStream.extract();
|
||||
extractor.once("error", e);
|
||||
extractor.on("entry", (header, stream, next) => {
|
||||
const name = header.name;
|
||||
if (match.test(name)) {
|
||||
extractData(stream).then((data) => {
|
||||
callback(name, data);
|
||||
next();
|
||||
}).catch(e);
|
||||
stream.resume();
|
||||
} else {
|
||||
stream.on("end", () => {
|
||||
next();
|
||||
});
|
||||
stream.resume();
|
||||
}
|
||||
});
|
||||
stream.resume();
|
||||
} else {
|
||||
stream.on("end", () => {
|
||||
next();
|
||||
extractor.on("finish", () => {
|
||||
c();
|
||||
});
|
||||
stream.resume();
|
||||
extractor.write(buffer);
|
||||
extractor.end();
|
||||
} catch (ex) {
|
||||
e(ex);
|
||||
}
|
||||
});
|
||||
extractor.on("finish", () => {
|
||||
callbackResolve();
|
||||
});
|
||||
extractor.write(buffer);
|
||||
extractor.end();
|
||||
return complete;
|
||||
}
|
||||
};
|
||||
|
||||
async function extractData(stream: NodeJS.ReadableStream): Promise<Buffer> {
|
||||
return new Promise<Buffer>((res, rej) => {
|
||||
const extractData = (stream: NodeJS.ReadableStream): Promise<Buffer> => {
|
||||
return new Promise<Buffer>((c, e): void => {
|
||||
const fileData: Buffer[] = [];
|
||||
stream.on('data', (data) => fileData.push(data));
|
||||
stream.on('end', () => {
|
||||
stream.on("data", (data) => fileData.push(data));
|
||||
stream.on("end", () => {
|
||||
const fd = Buffer.concat(fileData);
|
||||
res(fd);
|
||||
});
|
||||
stream.on('error', (err) => {
|
||||
rej(err);
|
||||
c(fd);
|
||||
});
|
||||
stream.on("error", e);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const extractTar = (tarPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> => {
|
||||
return new Promise<void>(async (c, e): Promise<void> => {
|
||||
try {
|
||||
const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : "");
|
||||
const buffer = await promisify(fs.readFile)(tarPath);
|
||||
const extractor = tarStream.extract();
|
||||
extractor.once("error", e);
|
||||
extractor.on("entry", (header, stream, next) => {
|
||||
const rawName = path.normalize(header.name);
|
||||
|
||||
const nextEntry = (): void => {
|
||||
stream.resume();
|
||||
next();
|
||||
};
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
return nextEntry();
|
||||
}
|
||||
|
||||
if (!sourcePathRegex.test(rawName)) {
|
||||
return nextEntry();
|
||||
}
|
||||
|
||||
const fileName = rawName.replace(sourcePathRegex, "");
|
||||
const targetFileName = path.join(targetPath, fileName);
|
||||
if (/\/$/.test(fileName)) {
|
||||
stream.resume();
|
||||
mkdirp(targetFileName).then(() => {
|
||||
next();
|
||||
}, e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const dirName = path.dirname(fileName);
|
||||
const targetDirName = path.join(targetPath, dirName);
|
||||
if (targetDirName.indexOf(targetPath) !== 0) {
|
||||
e(nls.localize("invalid file", "Error extracting {0}. Invalid file.", fileName));
|
||||
|
||||
return nextEntry();
|
||||
}
|
||||
|
||||
return mkdirp(targetDirName, undefined, token).then(() => {
|
||||
const fstream = fs.createWriteStream(targetFileName, { mode: header.mode });
|
||||
fstream.once("close", () => {
|
||||
next();
|
||||
});
|
||||
fstream.once("error", e);
|
||||
stream.pipe(fstream);
|
||||
stream.resume();
|
||||
});
|
||||
});
|
||||
extractor.once("finish", c);
|
||||
extractor.write(buffer);
|
||||
extractor.end();
|
||||
} catch (ex) {
|
||||
e(ex);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as os from "os";
|
||||
import { IProgress, INotificationHandle } from "@coder/ide";
|
||||
import { logger } from "@coder/logger";
|
||||
import { client } from "./client";
|
||||
|
||||
import "./fill/platform";
|
||||
@@ -28,12 +29,23 @@ import { LogLevel } from "vs/platform/log/common/log";
|
||||
import { RawContextKey, IContextKeyService } from "vs/platform/contextkey/common/contextkey";
|
||||
import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection";
|
||||
import { URI } from "vs/base/common/uri";
|
||||
import { BackupMainService } from "vs/platform/backup/electron-main/backupMainService";
|
||||
import { IInstantiationService } from "vs/platform/instantiation/common/instantiation";
|
||||
|
||||
/**
|
||||
* Initializes VS Code and provides a way to call into general client
|
||||
* functionality.
|
||||
*/
|
||||
export class Workbench {
|
||||
public readonly retry = client.retry;
|
||||
|
||||
private readonly windowId = parseInt(new Date().toISOString().replace(/[-:.TZ]/g, ""), 10);
|
||||
private _serviceCollection: ServiceCollection | undefined;
|
||||
private _clipboardContextKey: RawContextKey<boolean> | undefined;
|
||||
|
||||
/**
|
||||
* Handle a drop event on the file explorer.
|
||||
*/
|
||||
public async handleExternalDrop(target: ExplorerItem | ExplorerModel, originalEvent: DragEvent): Promise<void> {
|
||||
await client.upload.uploadDropped(
|
||||
originalEvent,
|
||||
@@ -41,11 +53,14 @@ export class Workbench {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a drop event on the editor.
|
||||
*/
|
||||
public handleDrop(event: DragEvent, resolveTargetGroup: () => IEditorGroup, afterDrop: (targetGroup: IEditorGroup) => void, targetIndex?: number): void {
|
||||
client.upload.uploadDropped(event, URI.file(paths.getWorkingDirectory())).then((paths) => {
|
||||
client.upload.uploadDropped(event, URI.file(paths.getWorkingDirectory())).then(async (paths) => {
|
||||
const uris = paths.map((p) => URI.file(p));
|
||||
if (uris.length) {
|
||||
(this.serviceCollection.get(IWindowsService) as IWindowsService).addRecentlyOpened(uris);
|
||||
await (this.serviceCollection.get(IWindowsService) as IWindowsService).addRecentlyOpened(uris);
|
||||
}
|
||||
|
||||
const editors: IResourceEditor[] = uris.map(uri => ({
|
||||
@@ -57,10 +72,10 @@ export class Workbench {
|
||||
}));
|
||||
|
||||
const targetGroup = resolveTargetGroup();
|
||||
|
||||
(this.serviceCollection.get(IEditorService) as IEditorService).openEditors(editors, targetGroup).then(() => {
|
||||
afterDrop(targetGroup);
|
||||
});
|
||||
await (this.serviceCollection.get(IEditorService) as IEditorService).openEditors(editors, targetGroup);
|
||||
afterDrop(targetGroup);
|
||||
}).catch((error) => {
|
||||
logger.error(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,6 +130,15 @@ export class Workbench {
|
||||
|
||||
public set serviceCollection(collection: ServiceCollection) {
|
||||
this._serviceCollection = collection;
|
||||
|
||||
// TODO: If possible it might be better to start the app from vs/code/electron-main/app.
|
||||
// For now, manually initialize services from there as needed.
|
||||
const inst = this._serviceCollection.get(IInstantiationService) as IInstantiationService;
|
||||
const backupMainService = inst.createInstance(BackupMainService) as BackupMainService;
|
||||
backupMainService.initialize().catch((error) => {
|
||||
logger.error(error.message);
|
||||
});
|
||||
|
||||
client.progressService = {
|
||||
start: <T>(title: string, task: (progress: IProgress) => Promise<T>, onCancel: () => void): Promise<T> => {
|
||||
let lastProgress = 0;
|
||||
@@ -164,6 +188,9 @@ export class Workbench {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Start VS Code.
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
this._clipboardContextKey = new RawContextKey("nativeClipboard", client.clipboard.isEnabled);
|
||||
|
||||
@@ -185,11 +212,31 @@ export class Workbench {
|
||||
_: [],
|
||||
};
|
||||
if ((workspace as IWorkspaceIdentifier).configPath) {
|
||||
config.workspace = workspace as IWorkspaceIdentifier;
|
||||
// tslint:disable-next-line:no-any
|
||||
let wid: IWorkspaceIdentifier = (<any>Object).assign({}, workspace);
|
||||
if (!URI.isUri(wid.configPath)) {
|
||||
// Ensure that the configPath is a valid URI.
|
||||
wid.configPath = URI.file(wid.configPath);
|
||||
}
|
||||
config.workspace = wid;
|
||||
} else {
|
||||
config.folderUri = workspace as URI;
|
||||
}
|
||||
await main(config);
|
||||
try {
|
||||
await main(config);
|
||||
} catch (ex) {
|
||||
if (ex.toString().indexOf("UriError") !== -1 || ex.toString().indexOf("backupPath") !== -1) {
|
||||
/**
|
||||
* Resolves the error of the workspace identifier being invalid.
|
||||
*/
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(ex);
|
||||
this.workspace = undefined;
|
||||
location.reload();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
const contextKeys = this.serviceCollection.get(IContextKeyService) as IContextKeyService;
|
||||
const bounded = this.clipboardContextKey.bindTo(contextKeys);
|
||||
client.clipboard.onPermissionChange((enabled) => {
|
||||
|
||||
BIN
packages/vscode/test/test-extension.tar
Normal file
BIN
packages/vscode/test/test-extension.tar
Normal file
Binary file not shown.
BIN
packages/vscode/test/test-extension.vsix
Normal file
BIN
packages/vscode/test/test-extension.vsix
Normal file
Binary file not shown.
59
packages/vscode/test/zip.test.ts
Normal file
59
packages/vscode/test/zip.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as zip from "../src/fill/zip";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import * as cp from "child_process";
|
||||
import { CancellationToken } from "vs/base/common/cancellation";
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
jest.mock("vs/nls", () => ({ "localize": (...args: any): string => `${JSON.stringify(args)}` }));
|
||||
|
||||
describe("zip", () => {
|
||||
const tarPath = path.resolve(__dirname, "./test-extension.tar");
|
||||
const vsixPath = path.resolve(__dirname, "./test-extension.vsix");
|
||||
const extractPath = path.resolve(__dirname, "./.test-extension");
|
||||
|
||||
beforeEach(() => {
|
||||
if (!fs.existsSync(extractPath) || path.dirname(extractPath) !== __dirname) {
|
||||
return;
|
||||
}
|
||||
cp.execSync(`rm -rf '${extractPath}'`);
|
||||
});
|
||||
|
||||
const resolveExtract = async (archivePath: string): Promise<void> => {
|
||||
expect(fs.existsSync(archivePath)).toEqual(true);
|
||||
await expect(zip.extract(
|
||||
archivePath,
|
||||
extractPath,
|
||||
{ sourcePath: "extension", overwrite: true },
|
||||
CancellationToken.None,
|
||||
)).resolves.toBe(undefined);
|
||||
expect(fs.existsSync(extractPath)).toEqual(true);
|
||||
};
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
const extract = (archivePath: string): () => any => {
|
||||
// tslint:disable-next-line:no-any
|
||||
return async (): Promise<any> => {
|
||||
await resolveExtract(archivePath);
|
||||
expect(fs.existsSync(path.resolve(extractPath, ".vsixmanifest"))).toEqual(true);
|
||||
expect(fs.existsSync(path.resolve(extractPath, "package.json"))).toEqual(true);
|
||||
};
|
||||
};
|
||||
it("should extract from tarred VSIX", extract(tarPath), 2000);
|
||||
it("should extract from zipped VSIX", extract(vsixPath), 2000);
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
const buffer = (archivePath: string): () => any => {
|
||||
// tslint:disable-next-line:no-any
|
||||
return async (): Promise<any> => {
|
||||
await resolveExtract(archivePath);
|
||||
const manifestPath = path.resolve(extractPath, ".vsixmanifest");
|
||||
expect(fs.existsSync(manifestPath)).toEqual(true);
|
||||
const manifestBuf = fs.readFileSync(manifestPath);
|
||||
expect(manifestBuf.length).toBeGreaterThan(0);
|
||||
await expect(zip.buffer(archivePath, "extension.vsixmanifest")).resolves.toEqual(manifestBuf);
|
||||
};
|
||||
};
|
||||
it("should buffer tarred VSIX", buffer(tarPath), 2000);
|
||||
it("should buffer zipped VSIX", buffer(vsixPath), 2000);
|
||||
});
|
||||
@@ -62,6 +62,7 @@ module.exports = merge(
|
||||
"vs/platform/product/node/package": path.resolve(vsFills, "package.ts"),
|
||||
"vs/platform/product/node/product": path.resolve(vsFills, "product.ts"),
|
||||
"vs/base/node/zip": path.resolve(vsFills, "zip.ts"),
|
||||
"vszip": path.resolve(root, "lib/vscode/src/vs/base/node/zip.ts"),
|
||||
"vs": path.resolve(root, "lib/vscode/src/vs"),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -34,11 +34,6 @@ big.js@^5.2.2:
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
|
||||
|
||||
bindings@^1.3.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.1.tgz#21fc7c6d67c18516ec5aaa2815b145ff77b26ea5"
|
||||
integrity sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==
|
||||
|
||||
bl@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88"
|
||||
@@ -114,24 +109,12 @@ lru-cache@^4.1.1:
|
||||
pseudomap "^1.0.2"
|
||||
yallist "^2.1.2"
|
||||
|
||||
minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
|
||||
|
||||
minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
|
||||
|
||||
mkdirp@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
|
||||
dependencies:
|
||||
minimist "0.0.8"
|
||||
|
||||
nan@^2.10.0, nan@^2.8.0:
|
||||
nan@^2.10.0:
|
||||
version "2.12.1"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
|
||||
integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
|
||||
@@ -194,15 +177,6 @@ schema-utils@^0.4.5:
|
||||
ajv "^6.1.0"
|
||||
ajv-keywords "^3.1.0"
|
||||
|
||||
spdlog@^0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.7.2.tgz#9298753d7694b9ee9bbfd7e01ea1e4c6ace1e64d"
|
||||
integrity sha512-rHfWCaWMD4NindDnql6rc6kn7Bs8JR92jhiUpCl3D6v+jYcQ6GozMLig0RliOOR8st5mU+IHLZnr15fBys5x/Q==
|
||||
dependencies:
|
||||
bindings "^1.3.0"
|
||||
mkdirp "^0.5.1"
|
||||
nan "^2.8.0"
|
||||
|
||||
string-replace-loader@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string-replace-loader/-/string-replace-loader-2.1.1.tgz#b72e7b57b6ef04efe615aff0ad989b5c14ca63d1"
|
||||
|
||||
@@ -12,7 +12,6 @@ module.exports = merge(
|
||||
typescriptCompilerOptions: {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "esnext"],
|
||||
"importHelpers": true,
|
||||
},
|
||||
},
|
||||
), {
|
||||
@@ -58,6 +57,7 @@ module.exports = merge(
|
||||
"fs": path.join(fills, "fs.ts"),
|
||||
"net": path.join(fills, "net.ts"),
|
||||
"util": path.join(fills, "util.ts"),
|
||||
"trash": path.join(fills, "trash.ts"),
|
||||
"electron": path.join(fills, "electron.ts"),
|
||||
|
||||
"native-keymap": path.join(vsFills, "native-keymap.ts"),
|
||||
@@ -75,6 +75,7 @@ module.exports = merge(
|
||||
"vs/platform/product/node/package": path.resolve(vsFills, "package.ts"),
|
||||
"vs/platform/product/node/product": path.resolve(vsFills, "product.ts"),
|
||||
"vs/base/node/zip": path.resolve(vsFills, "zip.ts"),
|
||||
"vszip": path.resolve(root, "lib/vscode/src/vs/base/node/zip.ts"),
|
||||
"vs": path.join(root, "lib", "vscode", "src", "vs"),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
const fs = require("fs");
|
||||
const util = require("util");
|
||||
|
||||
// This isn't properly promisified in Jest.
|
||||
Object.defineProperty(fs.read, util.promisify.custom, {
|
||||
configurable: true,
|
||||
value: (...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
args.push((error, bytesRead, buffer) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ bytesRead, buffer });
|
||||
}
|
||||
});
|
||||
fs.read(...args);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
global.requestAnimationFrame = (cb) => {
|
||||
setTimeout(cb, 0);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user