Compare commits

...

53 Commits

Author SHA1 Message Date
Kyle Carberry
2bd7281fa0 Update @coder/nbin 2019-04-04 09:59:50 -04:00
Asher
e12fcd3a0d Fix error when shared process exits with null 2019-04-03 17:32:20 -05:00
Kyle Carberry
4af84fcaf6 Add flags for customizing user data dir and extensions dir (#420)
* Add flags for customizing extensions directory

* Update @coder/nbin
2019-04-03 17:07:47 -05:00
Asher
c607015a26 Initialize backup service (#419)
- Fixes #399
- Fixes #332
2019-04-03 16:08:15 -05:00
John McCambridge
217515344e Add port in use message (#418)
* Add clear error message if port is in use

* Add bold function for text/numbers

* remove unused dependency:

* remove unused line break

* Change logger message

* Use NodeJS.ErrnoException type

* Add back check for error code
2019-04-03 15:50:52 -05:00
Kyle Carberry
dcf409aecb Improve CI caching (#416)
* Adjust linux distro to ubuntu 14.04

* Cache lib directory for speedy builds

* Fix path linking for default extensions

* Update reset

* Reset to head

* Improve caching

* Still run yarn in CI

* Update yarn before install

* Increase cache timeout

* Install vscode from vstar

* Undo data-dir changes to CLI, add back clean, remove unused CI func

* Remove additional flags added

* Remove unused dependency

* Reset vscode install dir so patching always works
2019-04-03 14:24:00 -05:00
Kyle Carberry
2683b7c734 Update notes title 2019-04-03 11:23:32 -04:00
Asher
3a672d725a Convert fully to protobuf (was partially JSON) (#402)
* Convert fully to protobuf (was partially JSON)

* Handle all floating promises

* Remove stringified proto from trace logging

It wasn't proving to be very useful.
2019-04-02 17:44:28 -05:00
Kyle Carberry
f484781693 Minor update in notes 2019-04-02 11:41:44 -04:00
Anmol Sethi
97f5b07003 Fix icons on safari when using cookie authentication (#398)
Cookie's are not sent with url's in -webkit-mask so we
embed the svg's directly in the css.
2019-04-01 15:20:39 -05:00
MOZGIII
7481395353 Changed "lib" to "/lib" at .gitignore (#395) 2019-04-01 14:15:43 -05:00
Asher
033ef151ca Improve retry
Registering returns an instance that lets you retry and recover without
needing to keep passing the name everywhere.

Also refactored the shared process a little to make better use of the
retry and downgraded stderr messages to warnings because they aren't
critical.
2019-04-01 13:31:34 -05:00
Jeff Delaney
3fec7f432c doc: fixed name of binary to match latest release (#386) 2019-03-31 13:15:33 -05:00
Asher
4887078423 Fix typescript tslint plugin
tslint-language-service is the deprecated version which we don't
actually even have listed in the package.json. typescript-tslint-plugin
is the new version.
2019-03-29 18:44:04 -05:00
Asher
91deaece47 Reduce frequency of port scanning 2019-03-29 16:14:28 -05:00
Asher
03ad2a17b2 Handle disconnects (#363)
* Make proxies decide how to handle disconnects

* Connect to a new terminal instance on disconnect

* Use our retry for the watcher

* Specify method when proxy doesn't exist

* Don't error when closing/killing disconnected proxy

* Specify proxy ID when a method doesn't exist

* Use our retry for the searcher

Also dispose some things for the watcher because it doesn't seem that
was done properly.

The searcher also now starts immediately so there won't be lag when you
perform your first search.

* Use our retry for the extension host

* Emit error in parent proxy class

Reduces duplicate code. Not all items are "supposed" to have an error
event according to the original implementation we are filling, but there
is no reason why we can't emit our own events (and are already doing so
for the "disconnected" event anyway).

* Reconnect spdlog

* Add error message when shared process disconnects

* Pass method resolve to parse

* Don't pass method to getProxy

It doesn't tell you anything that trace logging wouldn't and has
no relation to what the function actually does.

* Fix infinite recursion when disposing protocol client in tests
2019-03-28 17:59:49 -05:00
John McCambridge
a4cca6b759 Add information about release notifications/gif (#355)
* Add information about release notifications/gif

* Remove unecessary space

* Add smaller gif

* Add even smaller gif

* Trim time in new gif

* Cropped a tad more

* Fix weird aligning
2019-03-27 17:05:44 -05:00
NGTmeaty
6105bba0a4 Add higher quality Discord badge and add a link to the license badge. (#364)
* Add higher quality Discord badge and add link 

to license.

* Use @grant's much better version :)
2019-03-27 17:05:23 -05:00
Asher
259095eae2 Watcher and initial load performance improvements (#357)
* Set low CPU priority on watcher

Fixes #247.

* Batch stat and readdir calls

* Fix fs.exists

callbackify seems to always adds an error as the first argument. Opted
to just use the promise for this one.

* Batch lstat

* Add maximum time for flushing batches
2019-03-27 17:04:19 -05:00
Carlos Azaustre
38a0706b18 Add example with letsencrypt certificates
* updated the download link

* added example with letsencrypt certificates
2019-03-27 10:36:03 -05:00
Ryo Ochiai
c7ae12c2ed Add .node-version file (#272) 2019-03-27 10:35:00 -05:00
James Peters
3331f9b28d Update Dockerfile (#327) 2019-03-27 10:34:34 -05:00
Reda Aissaoui
def4104c53 Changed executable name (#353)
code-server-luni should be only code-server
2019-03-27 10:23:23 -05:00
Kyle Carberry
4eb5331ddc Fixes #121 2019-03-27 10:36:32 -04:00
Kyle Carberry
3bb5c0bbe5 Fixes #351 2019-03-27 09:56:05 -04:00
Kyle Carberry
83aa952de2 Update nbin version. Fixes extensions 2019-03-26 22:07:36 -04:00
Kyle Carberry
e0d33f2399 Update nbin to 1.0.3 2019-03-26 17:57:35 -04:00
Kyle Carberry
194cbca0f2 Update nbin 2019-03-26 17:53:36 -04:00
Kyle Carberry
1697cc32a3 Use commander instead of oclif 2019-03-26 16:21:03 -04:00
Kyle Carberry
f058f90340 Place all envs in one line 2019-03-26 14:52:33 -04:00
Kyle Carberry
ca4b0346cb Update versioning format 2019-03-26 14:38:38 -04:00
Kyle Carberry
8d692ded4a Remove tslib external 2019-03-26 14:25:12 -04:00
Asher
dc2253e718 Refactor evaluations (#285)
* Replace evaluations with proxies and messages

* Return proxies synchronously

Otherwise events can be lost.

* Ensure events cannot be missed

* Refactor remaining fills

* Use more up-to-date version of util

For callbackify.

* Wait for dispose to come back before removing

This prevents issues with the "done" event not always being the last
event fired. For example a socket might close and then end, but only
if the caller called end.

* Remove old node-pty tests

* Fix emitting events twice on duplex streams

* Preserve environment when spawning processes

* Throw a better error if the proxy doesn't exist

* Remove rimraf dependency from ide

* Update net.Server.listening

* Use exit event instead of killed

Doesn't look like killed is even a thing.

* Add response timeout to server

* Fix trash

* Require node-pty & spdlog after they get unpackaged

This fixes an error when running in the binary.

* Fix errors in down emitter preventing reconnecting

* Fix disposing proxies when nothing listens to "error" event

* Refactor event tests to use jest.fn()

* Reject proxy call when disconnected

Otherwise it'll wait for the timeout which is a waste of time since we
already know the connection is dead.

* Use nbin for binary packaging

* Remove additional module requires

* Attempt to remove require for local bootstrap-fork

* Externalize fsevents
2019-03-26 13:01:25 -05:00
Asher
d16c6aeb30 Fix port scanner when netstat isn't available
- It logs the error now.
- For some reason when there is an error node-netstat runs the callback
  twice. That resulted in us scheduling an exponentially growing number
	of calls which ate up all the CPU (and probably memory eventually).
  For now, opted to dispose when there is an error.
2019-03-25 18:31:43 -05:00
Forest Hoffman
cdc40d36ff Ensure workspace configPath is a valid URI object (#317) 2019-03-22 16:58:37 -05:00
Hayden Young
80c19878e0 Add note about extensions needing to be OSS (#113)
* Add note about extensions needing to be OSS

* Update README.md

* Update README.md
2019-03-22 14:58:13 -05:00
Forest Hoffman
18f395b853 Fix install from VSIX for TAR and ZIP formats (#245)
* Fix install from VSIX for TAR and ZIP formats

* Parse TAR before ZIP, when installing from VSIX
2019-03-21 14:04:09 -05:00
Dominik Schenner
75435be949 Update index.md (#297)
Added Apache reverse proxy example configuration
2019-03-21 13:49:27 -05:00
Mike Hatch
ce73bc58e5 Capitalized first letters to be consistent (#304)
In addition to capitalizing, I also reworded a sentence for improved grammar.
2019-03-21 09:39:34 -05:00
Hikari Kibo
70219d1071 doc: add CrOS install guide (#225)
* doc: add CrOS install guide

Signed-off-by: Hikari <enra@sayonika.moe>

* doc: change occurences of index to install guide

Signed-off-by: Hikari <enra@sayonika.moe>

* doc: add penguin.linux.test as alternative endpoint

Signed-off-by: Hikari <enra@sayonika.moe>

* doc: link Crostini and crouton info pages and describe install guide

Signed-off-by: Hikari <enra@sayonika.moe>

* doc: remove citations for dev mode requirement

Signed-off-by: Hikari <enra@sayonika.moe>

* doc: clarify more wording

Signed-off-by: Hikari <enra@sayonika.moe>

* doc: fix typo in Crostini section

Signed-off-by: Hikari <enra@sayonika.moe>
2019-03-20 09:24:07 -05:00
Kyle Carberry
e9e0bf7d84 Fixes #275 2019-03-20 09:58:56 -04:00
Kyle Carberry
3da1dccf73 Merge branch 'master' of github.com:codercom/code-server 2019-03-19 14:00:27 -04:00
Kyle Carberry
e02101c676 Update codeowners 2019-03-19 14:00:25 -04:00
Easy
ffc47054dd update nginx config (#288)
should add one line `proxy_set_header Accept-Encoding gzip; `  or you may get a  `Cannot GET /` error.
2019-03-19 12:59:50 -05:00
Kyle Carberry
2169045377 Fix debugging 2019-03-19 12:53:05 -04:00
Kyle Carberry
277c6cb690 Fix failure to resolve arrays error 2019-03-18 21:05:21 -04:00
Moien Tajik
91a98b8082 Fixed documentation download links based on latest version (#130)
* Fixed documentation based on latest release

* Fixed google-cloud documentation steps order

* Edited docs based on the latest release versions

* Make docs more dynamic based on Releases

* Changed ordered list to unordered list
2019-03-18 10:45:20 -05:00
Steve Sloka
6028a8b1a8 Add support for Kubernetes by deploying code-server. Also includes AWS (#146)
example which persists data to a Persistent Volume / Claim.

Signed-off-by: Steve Sloka <slokas@vmware.com>
2019-03-18 10:44:08 -05:00
Jim Tittsler
6749f25bbf Doc: fix typo (#277) 2019-03-17 22:35:29 -05:00
Sean Smith
f6b96e3778 Fix security links in cloud setup guides (#260) 2019-03-15 15:32:24 -04:00
Kyle Carberry
3b8cd0a3cd Fix docker cmd in readme 2019-03-15 01:24:44 -04:00
Kyle Carberry
2f27b5df8c Fix #251 2019-03-15 00:51:05 -04:00
Kyle Carberry
400fba7f6f Fix type import not resolving properly 2019-03-15 00:48:39 -04:00
109 changed files with 10829 additions and 7797 deletions

2
.github/CODEOWNERS vendored
View File

@@ -1,2 +1,2 @@
* @coderasher @kylecarbs
* @code-asher @kylecarbs
Dockerfile @nhooyr

3
.gitignore vendored
View File

@@ -1,6 +1,7 @@
lib
/lib
node_modules
dist
out
.DS_Store
release
.cache

1
.node-version Normal file
View File

@@ -0,0 +1 @@
8.15.0

View File

@@ -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

View File

@@ -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"]

View File

@@ -2,14 +2,14 @@
[!["Open Issues"](https://img.shields.io/github/issues-raw/codercom/code-server.svg)](https://github.com/codercom/code-server/issues)
[!["Latest Release"](https://img.shields.io/github/release/codercom/code-server.svg)](https://github.com/codercom/code-server/releases/latest)
[![MIT license](https://img.shields.io/badge/license-MIT-green.svg)](#)
[![Discord](https://discordapp.com/api/guilds/463752820026376202/widget.png)](https://discord.gg/zxSwN8Z)
[![MIT license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/codercom/code-server/blob/master/LICENSE)
[![Discord](https://img.shields.io/discord/463752820026376202.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/zxSwN8Z)
`code-server` is [VS Code](https://github.com/Microsoft/vscode) running on a remote server, accessible through the browser.
Try it out:
```bash
docker run -t -p 127.0.0.1:8443:8443 -v "${PWD}:/root/project" codercom/code-server --allow-http --no-auth
docker run -t -p 127.0.0.1:8443:8443 -v "${PWD}:/root/project" codercom/code-server code-server --allow-http --no-auth
```
- Code on your Chromebook, tablet, and laptop with a consistent dev environment.
@@ -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.
![Screenshot](/doc/assets/release.gif)
- 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.

View File

@@ -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);

View File

@@ -0,0 +1,74 @@
apiVersion: v1
kind: Namespace
metadata:
name: code-server
---
apiVersion: v1
kind: Service
metadata:
name: code-server
namespace: code-server
spec:
ports:
- port: 8443
name: https
protocol: TCP
selector:
app: code-server
type: ClusterIP
---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: gp2
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp2
fsType: ext4
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: code-store
namespace: code-server
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 60Gi
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: code-server
name: code-server
namespace: code-server
spec:
selector:
matchLabels:
app: code-server
replicas: 1
template:
metadata:
labels:
app: code-server
spec:
containers:
- image: codercom/code-server
imagePullPolicy: Always
name: code-servery
ports:
- containerPort: 8443
name: https
volumeMounts:
- name: code-server-storage
mountPath: /go/src
volumes:
- name: code-server-storage
persistentVolumeClaim:
claimName: code-store

View File

@@ -0,0 +1,43 @@
apiVersion: v1
kind: Namespace
metadata:
name: code-server
---
apiVersion: v1
kind: Service
metadata:
name: code-server
namespace: code-server
spec:
ports:
- port: 8443
name: https
protocol: TCP
selector:
app: code-server
type: ClusterIP
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
app: code-server
name: code-server
namespace: code-server
spec:
selector:
matchLabels:
app: code-server
replicas: 1
template:
metadata:
labels:
app: code-server
spec:
containers:
- image: codercom/code-server
imagePullPolicy: Always
name: code-server
ports:
- containerPort: 8443
name: https

View File

@@ -32,19 +32,31 @@ If you're just starting out, we recommend [installing code-server locally](../..
```
>example: `ssh -i "/Users/John/Downloads/TestInstance.pem" ubuntu@ec2-3-45-678-910.compute-1.amazonaws.co`
- You should see a prompt for your EC2 instance like so<img src="../../assets/aws_ubuntu.png">
- At this point it is time to download the `code-server` binary. We will of course want the linux version. Make sure you copy the link for the latest linux version on our [releases page](https://github.com/codercom/code-server/releases)
- With the URL in the clipboard, run:
- At this point it is time to download the `code-server` binary. We will of course want the linux version.
- Find the latest Linux release from this URL:
```
wget https://github.com/codercom/code-server/releases/download/0.1.4/code-server-linux
https://github.com/codercom/code-server/releases/latest
```
- Replace {version} in the following command with the version found on the releases page and run it (or just copy the download URL from the releases page):
```
wget https://github.com/codercom/code-server/releases/download/{version}/code-server-{version}-linux-x64.tar.gz
```
- Extract the downloaded tar.gz file with this command, for example:
```
tar -xvzf code-server-{version}-linux-x64.tar.gz
```
- Navigate to extracted directory with this command:
```
cd code-server-{version}-linux-x64
```
- If you run into any permission errors, make the binary executable by running:
```
chmod +x code-server-linux
chmod +x code-server
```
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md)
> 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">

View File

@@ -16,15 +16,27 @@ If you're just starting out, we recommend [installing code-server locally](../..
- Open a terminal on your computer and SSH into your instance
> example: ssh root@203.0.113.0
- Once in the SSH session, visit code-server [releases page](https://github.com/codercom/code-server/releases/) and copy the link to the download for the latest linux release
- In the shell run the below command with the URL from your clipboard
- Find the latest Linux release from this URL:
```
wget https://github.com/codercom/code-server/releases/download/0.1.4/code-server-linux
https://github.com/codercom/code-server/releases/latest
```
- Replace {version} in the following command with the version found on the releases page and run it (or just copy the download URL from the releases page):
```
wget https://github.com/codercom/code-server/releases/download/{version}/code-server-{version}-linux-x64.tar.gz
```
- Extract the downloaded tar.gz file with this command, for example:
```
tar -xvzf code-server-{version}-linux-x64.tar.gz
```
- Navigate to extracted directory with this command:
```
cd code-server-{version}-linux-x64
```
- If you run into any permission errors when attempting to run the binary:
```
chmod +x code-server-linux
chmod +x code-server
```
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md)
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../../security/ssl.md)
- Finally start the code-server
```
sudo ./code-server-linux -p 80

View File

@@ -13,34 +13,59 @@ If you're just starting out, we recommend [installing code-server locally](../..
- Choose an appropriate machine type (we recommend 2 vCPU and 7.5 GB RAM, more depending on team size and number of repositories/languages enabled)
- Choose Ubuntu 16.04 LTS as your boot disk
- Check the boxes for **Allow HTTP traffic** and **Allow HTTPS traffic** in the **Firewall** section
- Create your VM, and **take note** of it's public IP address.
- Create your VM, and **take note** of its public IP address.
- Copy the link to download the latest Linux binary from our [releases page](https://github.com/codercom/code-server/releases)
---
## Final Steps
1. SSH into your Google Cloud VM
- SSH into your Google Cloud VM
```
gcloud compute ssh --zone [region] [instance name]
```
2. Download the binary using the link we copied to clipboard
- Find the latest Linux release from this URL:
```
wget https://github.com/codercom/code-server/releases/download/0.1.4/code-server-linux
https://github.com/codercom/code-server/releases/latest
```
3. Make the binary executable if you run into any errors regarding permission:
```
chmod +x code-server-linux
```
- Replace {version} in the following command with the version found on the releases page and run it (or just copy the download URL from the releases page):
```
wget https://github.com/codercom/code-server/releases/download/{version}/code-server-{version}-linux-x64.tar.gz
```
- Extract the downloaded tar.gz file with this command, for example:
```
tar -xvzf code-server-{version}-linux-x64.tar.gz
```
- Navigate to extracted directory with this command:
```
cd code-server-{version}-linux-x64
```
- Make the binary executable if you run into any errors regarding permission:
```
chmod +x code-server
```
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md)
4. Start the 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
5. Access code-server from the public IP of your Google Cloud instance we noted earlier in your browser.
- Access code-server from the public IP of your Google Cloud instance we noted earlier in your browser.
> example: 32.32.32.234
6. 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">
7. Then click **"proceed anyway"**<img src="../../assets/chrome_confirm.png">
- 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">
---
> 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).
> 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).

BIN
doc/assets/cros.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
doc/assets/release.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View 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!
![code-server on Chromebook](../assets/cros.png)
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.

View File

@@ -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.

View File

@@ -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",

View File

@@ -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;
}
}

View 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();
});
});

View File

@@ -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;
}
}

View File

@@ -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"
}

View File

@@ -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];

View File

@@ -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();
});
});

View File

@@ -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[] => {
@@ -46,7 +44,9 @@ const newCreateElement = <K extends keyof HTMLElementTagNameMap>(tagName: K): HT
return oldSrc!.get!.call(img);
},
set: (value: string): void => {
value = value.replace(/file:\/\//g, "/resource");
if (value) {
value = value.replace(/file:\/\//g, "/resource");
}
oldSrc!.set!.call(img, value);
},
});
@@ -65,7 +65,9 @@ const newCreateElement = <K extends keyof HTMLElementTagNameMap>(tagName: K): HT
return oldInnerHtml!.get!.call(style);
},
set: (value: string): void => {
value = value.replace(/file:\/\//g, "/resource");
if (value) {
value = value.replace(/file:\/\//g, "/resource");
}
oldInnerHtml!.set!.call(style, value);
},
});
@@ -180,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);
}
}

View File

@@ -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];

View File

@@ -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];

View File

@@ -0,0 +1,4 @@
import { Module } from "@coder/protocol";
import { client } from "./client";
export = client.modules[Module.Trash].trash;

View File

@@ -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) => {

View File

@@ -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=

View File

@@ -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"

View File

@@ -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"
}

View File

@@ -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)!;
}
}

View 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));
}
}

View 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));
}
}

View 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";

View 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");
}
}

View 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);
}
}

View 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);
}
}

View 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");
}
}

View 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);
}
}

View File

@@ -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;
}

View File

@@ -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));
}
}

View 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)));
}
}

View File

@@ -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 };
}
};

View File

@@ -1,4 +1,4 @@
export * from "./browser/client";
export * from "./common/connection";
export * from "./common/helpers";
export * from "./common/proxy";
export * from "./common/util";

View File

@@ -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;
};

View 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),
};
}
}

View 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,
};
}
}

View 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";

View 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));
}
}

View 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));
}
}

View 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);
}
}

View 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"));
}
}

View 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);
}
}

View File

@@ -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)!;
}
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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 {}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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);
};

View File

@@ -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"));
});
});

View File

@@ -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());
});
});

View File

@@ -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();
});
});

View File

@@ -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),
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View 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);
});
});

View 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);
});
});

View 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);
});
});

View File

@@ -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"

View File

@@ -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",

View 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);
});

View File

@@ -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/**/*"),
],
});

View File

@@ -1,250 +1,288 @@
import * as fse from "fs-extra";
import { field, logger } from "@coder/logger";
import { ServerMessage, SharedProcessActiveMessage } from "@coder/protocol/src/proto";
import { Command, flags } from "@oclif/command";
import { fork, ForkOptions, ChildProcess } from "child_process";
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";
import * as fse from "fs-extra";
import * as os from "os";
import * as path from "path";
import * as WebSocket from "ws";
import { createApp } from "./server";
import { requireModule, requireFork, forkModule } from "./vscode/bootstrapFork";
import { SharedProcess, SharedProcessState } from "./vscode/sharedProcess";
import { buildDir, cacheHome, dataHome, isCli, serveStatic } from "./constants";
import { setup as setupNativeModules } from "./modules";
import { fillFs } from "./fill";
import { isCli, serveStatic, buildDir, dataHome, cacheHome } from "./constants";
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);
}
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);
});

View File

@@ -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));
};

View File

@@ -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);
});
};

View File

@@ -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());

View File

@@ -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,12 +49,21 @@ 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
Module.prototype.require = (id: string): 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");
}
return oldRequire(id);
// tslint:disable-next-line:no-any
return oldRequire.call(this, id as any);
};
if (!process.send) {
@@ -59,7 +71,6 @@ export const requireFork = (modulePath: string, args: string[], builtInExtension
}
process.argv = ["", "", ...args];
requireFilesystemModule(modulePath, builtInExtensionsDir);
if (ipcMsgBuffer && ipcMsgListener) {
@@ -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;
};

View File

@@ -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);

View File

@@ -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

View File

@@ -1 +1,2 @@
bin
bin
test/.test*

View File

@@ -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"
},

View File

@@ -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.");
}
}

View File

@@ -8,7 +8,7 @@ export class EnvironmentService extends environment.EnvironmentService {
}
public get extensionsPath(): string {
return path.join(paths.getAppDataPath(), "extensions");
return paths.getExtensionsDirectory();
}
}

View File

@@ -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];

View File

@@ -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;

View File

@@ -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];

View File

@@ -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);
}
});
};

View File

@@ -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) => {

Binary file not shown.

Binary file not shown.

View 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);
});

View File

@@ -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"),
},
},

Some files were not shown because too many files have changed in this diff Show More