Compare commits

...

41 Commits

Author SHA1 Message Date
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
Kyle Carberry
bfaadd4e51 Merge branch 'master' of github.com:codercom/code-server 2019-03-14 22:58:21 +00:00
Kyle Carberry
d16b35ed0b Remove sourcemaps for production 2019-03-14 22:58:12 +00:00
Dafydd
633f8dcd72 Fixing broken syntax in the run command (#238) 2019-03-14 18:32:40 -04:00
Mike Hatch
98cad8ae69 Fixed a couple of typos (#243) 2019-03-14 18:32:18 -04:00
Kyle Carberry
2e53bb6690 Automatically target production for client-side builds 2019-03-14 22:29:19 +00:00
Anmol Sethi
e3d9716607 Merge pull request #231 from nhooyr/docker
Dockerfile: include git
2019-03-13 15:38:55 -04:00
Anmol Sethi
862c94401a Dockerfile: include git
Closes #221
Closes #230
Closes #203
2019-03-13 15:19:38 -04:00
Sandro Jäckel
3a6e27bc87 Combine apt-get in final docker image and remove cache 2019-03-13 15:13:24 -04:00
pokemonlover1234
ec2d01ab40 Add git to docker container
Install git in the docker container, proposed by #203. While it is possible that issue requires further discussion, this is just a proposal pull request. If that proposal is accepted, here's the pull request.
2019-03-13 15:12:18 -04:00
Asher
e4ff723895 Fix race with watcher & stat
There is a window between when the stat is made and the result makes it
back to the client where a file is created or deleted and it won't be a
part of that stat.

To fix it, I added a new property signaling that we got some changes
since starting the stat and should run the stat again because we have no
way of knowing whether the currently running stat will include the new
changes or not.
2019-03-13 14:04:40 -05:00
Sandro Jäckel
f9448c6cd4 [ImgBot] Optimize images (#222)
*Total -- 1,393.88kb -> 908.68kb (34.81%)

/doc/assets/chrome_confirm.png -- 34.10kb -> 18.95kb (44.43%)
/doc/assets/server-password-modal.png -- 96.67kb -> 58.14kb (39.86%)
/doc/assets/ide.png -- 984.53kb -> 603.38kb (38.71%)
/doc/assets/chrome_warning.png -- 44.25kb -> 30.03kb (32.14%)
/doc/assets/logo-horizontal.png -- 22.00kb -> 17.48kb (20.55%)
/doc/assets/aws_ubuntu.png -- 51.62kb -> 43.93kb (14.91%)
/doc/assets/cli.png -- 160.70kb -> 136.78kb (14.89%)
2019-03-13 14:45:52 -04:00
Michael Desantis
0efae1fcb6 Add error message for incorrect password. Fix issue #55 (#201) 2019-03-12 19:19:32 -04:00
Kyle Carberry
7cc7aa51aa Add additional ide-api events 2019-03-12 17:43:53 -04:00
107 changed files with 6881 additions and 6818 deletions

2
.github/CODEOWNERS vendored
View File

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

View File

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

View File

@@ -20,11 +20,13 @@ FROM ubuntu:18.10
WORKDIR /root/project
COPY --from=0 /src/packages/server/cli-linux-x64 /usr/local/bin/code-server
EXPOSE 8443
RUN apt-get update && apt-get install -y \
openssl \
net-tools
RUN apt-get install -y locales && \
locale-gen en_US.UTF-8
net-tools \
git \
locales
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

View File

@@ -9,7 +9,7 @@
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.
@@ -33,7 +33,7 @@ See docker oneliner mentioned above. Dockerfile is at [/Dockerfile](/Dockerfile)
### Binaries
1. [Download a binary](https://github.com/codercom/code-server/releases) (Linux and OSX supported. Windows coming soon)
1. [Download a binary](https://github.com/codercom/code-server/releases) (Linux and OS X supported. Windows coming soon)
2. Start the binary with the project directory as the first argument
```
@@ -59,9 +59,13 @@ How to [secure your setup](/doc/security/ssl.md).
### Future
- Windows support.
- Electron and ChromeOS applications to bridge the gap between local<->remote.
- 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.
### Notes
- 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

@@ -10,7 +10,7 @@ const libPath = path.join(__dirname, "../lib");
const vscodePath = path.join(libPath, "vscode");
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 buildServerBinary = register("build:server:binary", async (runner) => {
await ensureInstalled();
@@ -33,50 +33,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");
@@ -274,7 +236,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,16 +32,28 @@ 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

View File

@@ -16,22 +16,34 @@ 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 -p80
sudo ./code-server-linux -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
- When you visit the public IP for your Digital Ocean 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">
---
> 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).

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
```
> 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).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 137 KiB

BIN
doc/assets/cros.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 984 KiB

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 58 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,7 +2,7 @@
[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).
@@ -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,7 +54,7 @@ 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
@@ -75,7 +75,7 @@ OPTIONS
> 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 +85,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

@@ -37,10 +37,12 @@
"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",
"util": "^0.11.1",
"webpack": "^4.28.4",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.2.1",

View File

@@ -8,21 +8,20 @@
<body>
<div class="login">
<div class="back">
<- Back </div>
<h4 class="title">code-server</h4>
<h2 class="subtitle">
Enter server password
</h2>
<div class="mdc-text-field">
<input type="password" id="password" class="mdc-text-field__input" required>
<label class="mdc-floating-label" for="password">Password</label>
<div class="mdc-line-ripple"></div>
</div>
<button id="submit" class="mdc-button mdc-button--unelevated">
<span class="mdc-button__label">Enter IDE</span>
</button>
<div class="back"> <- Back </div>
<h4 class="title">code-server</h4>
<h2 class="subtitle">
Enter server password
</h2>
<div class="mdc-text-field">
<input type="password" id="password" class="mdc-text-field__input" required>
<label class="mdc-floating-label" for="password">Password</label>
<div class="mdc-line-ripple"></div>
</div>
<button id="submit" class="mdc-button mdc-button--unelevated">
<span class="mdc-button__label">Enter IDE</span>
</button>
<div id="error-display"></div>
</div>
</body>

View File

@@ -106,3 +106,16 @@ body {
// transition: 500ms opacity ease;
}
#error-display {
box-sizing: border-box;
color: #bb2d0f;
font-size: 14px;
font-weight: 400;
letter-spacing: 0.3px;
line-height: 12px;
padding: 8px;
padding-bottom: 0;
padding-top: 20px;
text-align: center;
}

View File

@@ -28,3 +28,14 @@ submit.addEventListener("click", () => {
document.cookie = `password=${password.value}`;
location.reload();
});
/**
* Notify user on load of page if previous password was unsuccessful
*/
const reg = new RegExp(`password=(\\w+);?`);
const matches = document.cookie.match(reg);
const errorDisplay = document.getElementById("error-display") as HTMLDivElement;
if (document.referrer === document.location.href && matches) {
errorDisplay.innerText = "Password is incorrect!";
}

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,37 +137,34 @@ 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;
readonly notificationService: INotificationService;
readonly menuRegistry: IMenuRegistry;
readonly commandRegistry: ICommandRegistry;
onFileCreate(cb: (path: string) => void): void;
onFileMove(cb: (path: string, target: string) => void): void;
onFileDelete(cb: (path: string) => void): void;
onFileSaved(cb: (path: string) => void): void;
onFileCopy(cb: (path: string, target: string) => void): void;
onModelAdded(cb: (path: string, languageId: string) => void): void;
onModelRemoved(cb: (path: string, languageId: string) => void): void;
onModelLanguageChange(cb: (path: string, languageId: string, oldLanguageId: string) => void): void;
onTerminalAdded(cb: () => void): void;
onTerminalRemoved(cb: () => void): void;
};
export enum Severity {
Ignore = 0,
Info = 1,
Warning = 2,
Error = 3
}
Error = 3,
}
export enum StatusbarAlignment {
LEFT = 0,
@@ -216,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,6 +1,6 @@
{
"name": "@coder/ide-api",
"version": "1.0.2",
"version": "1.0.3",
"typings": "api.d.ts",
"author": "Coder",
"license": "MIT",

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

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

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

@@ -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 { stringify, parse, moduleToProto, protoToModule, protoToOperatingSystem } from "../common/util";
import { Ping, ServerMessage, ClientMessage, MethodMessage, NamedProxyMessage, NumberedProxyMessage, SuccessMessage, FailMessage, EventMessage, CallbackMessage } 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<SuccessMessage>();
private readonly failEmitter = new Emitter<FailMessage>();
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 FailMessage();
const error = new Error("disconnected");
message.setResponse(stringify(error));
this.failEmitter.emit(message);
this.eventEmitter.emit({ event: "exit", args: [1] });
this.eventEmitter.emit({ event: "close", args: [] });
try {
this.eventEmitter.emit({ event: "error", args: [error] });
} catch (error) {
// If nothing is listening, EventEmitter will throw an error.
}
this.eventEmitter.emit({ event: "done", args: [true] });
};
connection.onDown(() => handleDisconnect());
connection.onClose(() => {
clearTimeout(this.pingTimeout as any);
this.pingTimeout = undefined;
handleDisconnect();
});
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,158 +170,109 @@ 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);
private remoteCall(proxyId: number | Module, method: string, args: any[]): Promise<any> {
if (this.disconnected) {
return Promise.reject(new Error("disconnected"));
}
// 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));
}
});
const message = new MethodMessage();
const id = this.messageId++;
let proxyMessage: NamedProxyMessage | NumberedProxyMessage;
if (typeof proxyId === "string") {
proxyMessage = new NamedProxyMessage();
proxyMessage.setModule(moduleToProto(proxyId));
message.setNamedProxy(proxyMessage);
} else {
proxyMessage = new NumberedProxyMessage();
proxyMessage.setProxyId(proxyId);
message.setNumberedProxy(proxyMessage);
}
proxyMessage.setId(id);
proxyMessage.setMethod(method);
doEval.completed.then(() => {
d1.dispose();
}).catch((ex) => {
d1.dispose();
// This error event is only received by the client.
eventEmitter.emit("error", ex);
});
const storeCallback = (cb: (...args: any[]) => void): number => {
const callbackId = this.callbackId++;
logger.trace(() => [
"storing callback",
field("proxyId", proxyId),
field("callbackId", callbackId),
]);
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
});
}
this.getProxy(proxyId).callbacks.set(callbackId, cb);
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;
}
return 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());
const stringifiedArgs = args.map((a) => stringify(a, storeCallback));
logger.trace(() => [
"sending",
field("id", id),
field("proxyId", proxyId),
field("method", method),
field("args", stringifiedArgs),
]);
const clientMsg = new ClientMessage();
clientMsg.setNewEval(newEval);
this.connection.send(clientMsg.serializeBinary());
proxyMessage.setArgsList(stringifiedArgs);
const completed = new Promise<R>((resolve, reject): void => {
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.parse(message.getResponse()));
});
const d2 = this.evalFailedEmitter.event((failedMsg) => {
if (failedMsg.getId() === id) {
dispose();
reject(parse(failedMsg.getResponse()));
}
const d2 = this.failEmitter.event(id, (message) => {
dispose();
reject(parse(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 {
private async handleMessage(message: ServerMessage): Promise<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,
os: protoToOperatingSystem(init.getOperatingSystem()),
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.hasSuccess()) {
this.emitSuccess(message.getSuccess()!);
} else if (message.hasFail()) {
this.emitFail(message.getFail()!);
} else if (message.hasEvent()) {
await this.emitEvent(message.getEvent()!);
} else if (message.hasCallback()) {
await this.runCallback(message.getCallback()!);
} else if (message.hasSharedProcessActive()) {
const sharedProcessActiveMessage = message.getSharedProcessActive()!;
this.sharedProcessActiveEmitter.emit({
@@ -227,13 +280,85 @@ export class Client {
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.
// 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.
} else {
throw new Error("unknown message type");
}
}
private emitSuccess(message: SuccessMessage): void {
logger.trace(() => [
"received resolve",
field("id", message.getId()),
]);
this.successEmitter.emit(message.getId(), message);
}
private emitFail(message: FailMessage): 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: EventMessage): 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),
field("args", eventMessage.getArgsList()),
]);
const args = eventMessage.getArgsList().map((a) => this.parse(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: CallbackMessage): 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),
field("args", callbackMessage.getArgsList()),
]);
const args = callbackMessage.getArgsList().map((a) => this.parse(a));
this.getProxy(proxyId).callbacks.get(callbackId)!(...args);
}
/**
* Start the ping loop. Does nothing if already pinging.
*/
private startPinging = (): void => {
if (typeof this.pingTimeout !== "undefined") {
return;
@@ -250,4 +375,136 @@ 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((disconnected: boolean) => {
const log = (): void => {
logger.trace(() => [
typeof proxyId === "number" ? "disposed proxy" : "disposed proxy callbacks",
field("proxyId", proxyId),
field("disconnected", 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 (!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;
}
private parse(value?: string, promise?: Promise<any>): any {
return parse(value, undefined, (id) => this.createProxy(id, promise));
}
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,126 @@
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";
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.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.proxy.kill();
}
public disconnect(): void {
this.proxy.disconnect();
}
public ref(): void {
this.proxy.ref();
}
public unref(): void {
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.
}
}
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,316 @@
import * as fs from "fs";
import { callbackify } from "util";
import { ClientProxy } 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
class Watcher extends ClientProxy<WatcherProxy> implements fs.FSWatcher {
public close(): void {
this.proxy.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.proxy.close();
}
}
export class FsModule {
public constructor(private readonly proxy: FsModuleProxy) {}
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 => {
callbackify(this.proxy.exists)(path, (exists) => {
callback!(exists as any);
});
}
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.proxy.lstat)(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.proxy.readdir)(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.proxy.stat)(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,280 @@
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";
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);
}
this.proxy.connect(options, host);
return this;
}
// 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.proxy.unref();
}
public ref(): void {
this.proxy.ref();
}
}
export class Server extends ClientProxy<NetServerProxy> implements net.Server {
private readonly sockets = new Map<number, net.Socket>();
private _listening: boolean = false;
public constructor(proxyPromise: Promise<NetServerProxy> | NetServerProxy) {
super(proxyPromise);
this.proxy.onConnection((socketProxy) => {
this.emit("connection", new Socket(socketProxy));
});
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);
}
this.proxy.listen(handle, hostname, backlog);
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 close(callback?: () => void): this {
this._listening = false;
if (callback) {
this.on("close", callback);
}
this.proxy.close();
return this;
}
public ref(): this {
this.proxy.ref();
return this;
}
public unref(): this {
this.proxy.unref();
return this;
}
public getConnections(cb: (error: Error | null, count: number) => void): void {
cb(null, this.sockets.size);
}
}
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,45 @@
import * as pty from "node-pty";
import { ClientProxy } from "../../common/proxy";
import { NodePtyModuleProxy, NodePtyProcessProxy } from "../../node/modules/node-pty";
export class NodePtyProcess extends ClientProxy<NodePtyProcessProxy> implements pty.IPty {
private _pid = -1;
private _process = "";
public constructor(proxyPromise: Promise<NodePtyProcessProxy>) {
super(proxyPromise);
this.proxy.getPid().then((pid) => this._pid = pid);
this.proxy.getProcess().then((process) => this._process = process);
this.on("process", (process) => this._process = process);
}
public get pid(): number {
return this._pid;
}
public get process(): string {
return this._process;
}
public resize(columns: number, rows: number): void {
this.proxy.resize(columns, rows);
}
public write(data: string): void {
this.proxy.write(data);
}
public kill(signal?: string): void {
this.proxy.kill(signal);
}
}
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.spawn(file, args, options));
}
}

View File

@@ -0,0 +1,32 @@
import * as spdlog from "spdlog";
import { ClientProxy } from "../../common/proxy";
import { RotatingLoggerProxy, SpdlogModuleProxy } from "../../node/modules/spdlog";
class RotatingLogger extends ClientProxy<RotatingLoggerProxy> implements spdlog.RotatingLogger {
public async trace (message: string): Promise<void> { this.proxy.trace(message); }
public async debug (message: string): Promise<void> { this.proxy.debug(message); }
public async info (message: string): Promise<void> { this.proxy.info(message); }
public async warn (message: string): Promise<void> { this.proxy.warn(message); }
public async error (message: string): Promise<void> { this.proxy.error(message); }
public async critical (message: string): Promise<void> { this.proxy.critical(message); }
public async setLevel (level: number): Promise<void> { this.proxy.setLevel(level); }
public async clearFormatters (): Promise<void> { this.proxy.clearFormatters(); }
public async flush (): Promise<void> { this.proxy.flush(); }
public async drop (): Promise<void> { this.proxy.drop(); }
}
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.createLogger(name, filename, filesize, filecount));
}
};
}
public setAsyncMode = (bufferSize: number, flushInterval: number): void => {
this.proxy.setAsyncMode(bufferSize, flushInterval);
}
}

View File

@@ -0,0 +1,233 @@
import * as stream from "stream";
import { callbackify } from "util";
import { ClientProxy } from "../../common/proxy";
import { DuplexProxy, IReadableProxy, WritableProxy } from "../../node/modules/stream";
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.proxy.destroy();
}
public setDefaultEncoding(encoding: string): this {
this.proxy.setDefaultEncoding(encoding);
return this;
}
// 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();
}
});
}
}
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.proxy.destroy();
}
public setEncoding(encoding: string): this {
this.proxy.setEncoding(encoding);
return this;
}
}
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 {
this.proxy.setEncoding(encoding);
return this;
}
}

View File

@@ -0,0 +1,10 @@
import * as trash from "trash";
import { TrashModuleProxy } from "../../node/modules/trash";
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;
}

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,83 @@
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 {
protected readonly proxy: T;
/**
* You can specify not to bind events in order to avoid emitting twice for
* duplex streams.
*/
public constructor(proxyPromise: Promise<T> | T, bindEvents: boolean = true) {
super();
this.proxy = isPromise(proxyPromise) ? unpromisify(proxyPromise) : proxyPromise;
if (bindEvents) {
this.proxy.onEvent((event, ...args): void => {
this.emit(event, ...args);
});
}
}
}
/**
* 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(): 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>;
}
export enum Module {
Fs = "fs",
ChildProcess = "child_process",
Net = "net",
Spdlog = "spdlog",
NodePty = "node-pty",
Trash = "trash",
}

View File

@@ -1,3 +1,9 @@
import { Module as ProtoModule, WorkingInitMessage } 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,294 @@ 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.
*/
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),
});
}
interface StringifiedError {
type: "error";
data: {
message: string;
stack?: string;
code?: string;
};
}
return JSON.stringify(arg);
interface StringifiedBuffer {
type: "buffer";
data: number[];
}
interface StringifiedObject {
type: "object";
data: { [key: string]: StringifiedValue };
}
interface StringifiedArray {
type: "array";
data: StringifiedValue[];
}
interface StringifiedProxy {
type: "proxy";
data: {
id: number;
};
}
interface StringifiedFunction {
type: "function";
data: {
id: number;
};
}
interface StringifiedUndefined {
type: "undefined";
}
type StringifiedValue = StringifiedFunction | StringifiedProxy
| StringifiedUndefined | StringifiedObject | StringifiedArray
| StringifiedBuffer | StringifiedError | number | string | boolean | null;
const isPrimitive = (value: any): value is number | string | boolean | null => {
return typeof value === "number"
|| typeof value === "string"
|| typeof value === "boolean"
|| value === null;
};
/**
* 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);
}
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;
}
break;
/**
* Stringify an argument or a return value.
* If sending a function is possible, provide `storeFunction`.
* If sending a proxy is possible, provide `storeProxy`.
*/
export const stringify = (
value: any,
storeFunction?: (fn: () => void) => number,
storeProxy?: (proxy: ServerProxy) => number,
): string => {
const convert = (currentValue: any): StringifiedValue => {
// Errors don't stringify at all. They just become "{}".
// For some reason when running in Jest errors aren't instances of Error,
// so also check against the values.
if (currentValue instanceof Error
|| (currentValue && typeof currentValue.message !== "undefined"
&& typeof currentValue.stack !== "undefined")) {
return {
type: "error",
data: {
message: currentValue.message,
stack: currentValue.stack,
code: (currentValue as NodeJS.ErrnoException).code,
},
};
}
// With stringify, Uint8Array gets 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. Also handle Buffer so it doesn't get caught by the
// object check and to get the same type.
if (currentValue instanceof Uint8Array || currentValue instanceof Buffer) {
return {
type: "buffer",
data: Array.from(currentValue),
};
}
if (Array.isArray(currentValue)) {
return {
type: "array",
data: currentValue.map((a) => convert(a)),
};
}
if (isProxy(currentValue)) {
if (!storeProxy) {
throw new Error("no way to serialize proxy");
}
return {
type: "proxy",
data: {
id: storeProxy(currentValue),
},
};
}
if (currentValue !== null && typeof currentValue === "object") {
const converted: { [key: string]: StringifiedValue } = {};
Object.keys(currentValue).forEach((key) => {
converted[key] = convert(currentValue[key]);
});
return {
type: "object",
data: converted,
};
}
// `undefined` can't be stringified.
if (typeof currentValue === "undefined") {
return {
type: "undefined",
};
}
if (typeof currentValue === "function") {
if (!storeFunction) {
throw new Error("no way to serialize function");
}
return {
type: "function",
data: {
id: storeFunction(currentValue),
},
};
}
if (!isPrimitive(currentValue)) {
throw new Error(`cannot stringify ${typeof currentValue}`);
}
return currentValue;
};
return JSON.stringify(convert(value));
};
/**
* Parse an argument.
* If running a remote callback is supported, provide `runCallback`.
* If using a remote proxy is supported, provide `createProxy`.
*/
export const parse = (
value?: string,
runCallback?: (id: number, args: any[]) => void,
createProxy?: (id: number) => ServerProxy,
): any => {
const convert = (currentValue: StringifiedValue): any => {
if (currentValue && !isPrimitive(currentValue)) {
// Would prefer a switch but the types don't seem to work.
if (currentValue.type === "buffer") {
return Buffer.from(currentValue.data);
}
if (currentValue.type === "error") {
const error = new Error(currentValue.data.message);
if (typeof currentValue.data.code !== "undefined") {
(error as NodeJS.ErrnoException).code = currentValue.data.code;
}
(error as any).originalStack = currentValue.data.stack;
return error;
}
if (currentValue.type === "object") {
const converted: { [key: string]: any } = {};
Object.keys(currentValue.data).forEach((key) => {
converted[key] = convert(currentValue.data[key]);
});
return converted;
}
if (currentValue.type === "array") {
return currentValue.data.map(convert);
}
if (currentValue.type === "undefined") {
return undefined;
}
if (currentValue.type === "function") {
if (!runCallback) {
throw new Error("no way to run remote callback");
}
return (...args: any[]): void => {
return runCallback(currentValue.data.id, args);
};
}
if (currentValue.type === "proxy") {
if (!createProxy) {
throw new Error("no way to create proxy");
}
return createProxy(currentValue.data.id);
}
}
if (value && typeof value === "object") {
Object.keys(value).forEach((key) => {
value[key] = convert(value[key]);
});
}
return value;
return currentValue;
};
return arg ? convert(JSON.parse(arg)) : arg;
return value && convert(JSON.parse(value));
};
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: WorkingInitMessage.OperatingSystem): OperatingSystem => {
switch (protoOp) {
case WorkingInitMessage.OperatingSystem.WINDOWS: return OperatingSystem.Windows;
case WorkingInitMessage.OperatingSystem.LINUX: return OperatingSystem.Linux;
case WorkingInitMessage.OperatingSystem.MAC: return OperatingSystem.Mac;
default: throw new Error(`unsupported operating system ${protoOp}`);
}
};
export const platformToProto = (platform: NodeJS.Platform): WorkingInitMessage.OperatingSystem => {
switch (platform) {
case "win32": return WorkingInitMessage.OperatingSystem.WINDOWS;
case "linux": return WorkingInitMessage.OperatingSystem.LINUX;
case "darwin": return WorkingInitMessage.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,103 @@
import * as cp from "child_process";
import { ServerProxy } from "../../common/proxy";
import { preserveEnv } from "../../common/util";
import { WritableProxy, ReadableProxy } from "./stream";
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) => {
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.kill();
setTimeout(() => this.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;
stdout?: ReadableProxy;
stderr?: ReadableProxy;
}
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,250 @@
import * as fs from "fs";
import { promisify } from "util";
import { ServerProxy } from "../../common/proxy";
import { IEncodingOptions } from "../../common/util";
import { WritableProxy } from "./stream";
/**
* 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> {
super.dispose();
this.stream.close();
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
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);
}
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 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 = new Buffer(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 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 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,90 @@
import * as net from "net";
import { ServerProxy } from "../../common/proxy";
import { DuplexProxy } from "./stream";
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> {
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,75 @@
/// <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";
/**
* 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.onDone(() => 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.kill();
setTimeout(() => this.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,46 @@
/// <reference path="../../../../../lib/vscode/src/typings/spdlog.d.ts" />
import { EventEmitter } from "events";
import * as spdlog from "spdlog";
import { ServerProxy } from "../../common/proxy";
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> {
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,107 @@
import * as stream from "stream";
import { ServerProxy } from "../../common/proxy";
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> {
super.onEvent(cb);
this.stream.on("data", (chunk) => cb("data", chunk));
this.stream.on("end", () => cb("end"));
}
}

View File

@@ -0,0 +1,7 @@
import * as trash from "trash";
export class TrashModuleProxy {
public async trash(path: string, options?: trash.Options): Promise<void> {
return trash(path, options);
}
}

View File

@@ -1,10 +1,13 @@
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, parse, platformToProto, protoToModule, stringify } from "../common/util";
import { CallbackMessage, ClientMessage, EventMessage, FailMessage, MethodMessage, NamedCallbackMessage, NamedEventMessage, NumberedCallbackMessage, NumberedEventMessage, Pong, ServerMessage, SuccessMessage, WorkingInitMessage } from "../proto";
import { ChildProcessModuleProxy, ForkProvider, FsModuleProxy, NetModuleProxy, NodePtyModuleProxy, SpdlogModuleProxy, TrashModuleProxy } from "./modules";
// tslint:disable no-any
export interface ServerOptions {
readonly workingDirectory: string;
@@ -14,27 +17,59 @@ export interface ServerOptions {
readonly fork?: ForkProvider;
}
interface ProxyData {
disposeTimeout?: number | NodeJS.Timer;
instance: any;
}
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 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();
}
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.");
@@ -55,53 +90,19 @@ export class Server {
initMsg.setBuiltinExtensionsDir(this.options.builtInExtensionsDirectory);
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.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);
/**
* Handle all messages from the client.
*/
private async handleMessage(message: ClientMessage): Promise<void> {
if (message.hasMethod()) {
await this.runMethod(message.getMethod()!);
} else if (message.hasPing()) {
logger.trace("ping");
const srvMsg = new ServerMessage();
@@ -111,4 +112,230 @@ export class Server {
throw new Error("unknown message type");
}
}
/**
* Run a method on a proxy.
*/
private async runMethod(message: MethodMessage): 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) => parse(
a,
(id, args) => this.sendCallback(proxyId, id, args),
));
logger.trace(() => [
"received",
field("id", id),
field("proxyId", proxyId),
field("method", method),
field("args", proxyMessage.getArgsList()),
]);
let response: any;
try {
const proxy = this.getProxy(proxyId);
if (typeof proxy.instance[method] !== "function") {
throw new Error(`"${method}" is not a function`);
}
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 {
const stringifiedArgs = args.map((a) => this.stringify(a));
logger.trace(() => [
"sending callback",
field("proxyId", proxyId),
field("callbackId", callbackId),
field("args", stringifiedArgs),
]);
const message = new CallbackMessage();
let callbackMessage: NamedCallbackMessage | NumberedCallbackMessage;
if (typeof proxyId === "string") {
callbackMessage = new NamedCallbackMessage();
callbackMessage.setModule(moduleToProto(proxyId));
message.setNamedCallback(callbackMessage);
} else {
callbackMessage = new NumberedCallbackMessage();
callbackMessage.setProxyId(proxyId);
message.setNumberedCallback(callbackMessage);
}
callbackMessage.setCallbackId(callbackId);
callbackMessage.setArgsList(stringifiedArgs);
const serverMessage = new ServerMessage();
serverMessage.setCallback(message);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Store a proxy and bind events to send them back to the client.
*/
private storeProxy(instance: ServerProxy): number;
private storeProxy(instance: any, moduleProxyId: Module): Module;
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();
}
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));
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();
this.removeProxy(proxyId);
}, this.responseTimeout);
}
});
}
return proxyId;
}
/**
* Send an event to the client.
*/
private sendEvent(proxyId: number | Module, event: string, ...args: any[]): void {
const stringifiedArgs = args.map((a) => this.stringify(a));
logger.trace(() => [
"sending event",
field("proxyId", proxyId),
field("event", event),
field("args", stringifiedArgs),
]);
const message = new EventMessage();
let eventMessage: NamedEventMessage | NumberedEventMessage;
if (typeof proxyId === "string") {
eventMessage = new NamedEventMessage();
eventMessage.setModule(moduleToProto(proxyId));
message.setNamedEvent(eventMessage);
} else {
eventMessage = new NumberedEventMessage();
eventMessage.setProxyId(proxyId);
message.setNumberedEvent(eventMessage);
}
eventMessage.setEvent(event);
eventMessage.setArgsList(stringifiedArgs);
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 {
const stringifiedResponse = this.stringify(response);
logger.trace(() => [
"sending resolve",
field("id", id),
field("response", stringifiedResponse),
]);
const successMessage = new SuccessMessage();
successMessage.setId(id);
successMessage.setResponse(stringifiedResponse);
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 {
const stringifiedError = stringify(error);
logger.trace(() => [
"sending reject",
field("id", id) ,
field("response", stringifiedError),
]);
const failedMessage = new FailMessage();
failedMessage.setId(id);
failedMessage.setResponse(stringifiedError);
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),
]);
}
private stringify(value: any): string {
return stringify(value, undefined, (p) => this.storeProxy(p));
}
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,29 +2,29 @@ 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;
MethodMessage 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;
FailMessage fail = 13;
SuccessMessage success = 14;
EventMessage event = 19;
CallbackMessage callback = 22;
Pong pong = 18;
WorkingInitMessage init = 16;
// vscode.proto
SharedProcessActiveMessage shared_process_active = 17;
Pong pong = 18;
}
}

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.MethodMessage | undefined;
setMethod(value?: node_pb.MethodMessage): void;
hasPing(): boolean;
clearPing(): void;
@@ -34,34 +29,42 @@ 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.MethodMessage.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.FailMessage | undefined;
setFail(value?: node_pb.FailMessage): void;
hasEvalDone(): boolean;
clearEvalDone(): void;
getEvalDone(): node_pb.EvalDoneMessage | undefined;
setEvalDone(value?: node_pb.EvalDoneMessage): void;
hasSuccess(): boolean;
clearSuccess(): void;
getSuccess(): node_pb.SuccessMessage | undefined;
setSuccess(value?: node_pb.SuccessMessage): void;
hasEvalEvent(): boolean;
clearEvalEvent(): void;
getEvalEvent(): node_pb.EvalEventMessage | undefined;
setEvalEvent(value?: node_pb.EvalEventMessage): void;
hasEvent(): boolean;
clearEvent(): void;
getEvent(): node_pb.EventMessage | undefined;
setEvent(value?: node_pb.EventMessage): void;
hasCallback(): boolean;
clearCallback(): void;
getCallback(): node_pb.CallbackMessage | undefined;
setCallback(value?: node_pb.CallbackMessage): void;
hasPong(): boolean;
clearPong(): void;
getPong(): node_pb.Pong | undefined;
setPong(value?: node_pb.Pong): void;
hasInit(): boolean;
clearInit(): void;
@@ -73,11 +76,6 @@ export class ServerMessage extends jspb.Message {
getSharedProcessActive(): vscode_pb.SharedProcessActiveMessage | undefined;
setSharedProcessActive(value?: vscode_pb.SharedProcessActiveMessage): void;
hasPong(): boolean;
clearPong(): void;
getPong(): node_pb.Pong | undefined;
setPong(value?: node_pb.Pong): void;
getMsgCase(): ServerMessage.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ServerMessage.AsObject;
@@ -91,22 +89,24 @@ 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,
fail?: node_pb.FailMessage.AsObject,
success?: node_pb.SuccessMessage.AsObject,
event?: node_pb.EventMessage.AsObject,
callback?: node_pb.CallbackMessage.AsObject,
pong?: node_pb.Pong.AsObject,
init?: WorkingInitMessage.AsObject,
sharedProcessActive?: vscode_pb.SharedProcessActiveMessage.AsObject,
pong?: node_pb.Pong.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,
}
}

View File

@@ -43,16 +43,15 @@ if (goog.DEBUG && !COMPILED) {
* @private {!Array<!Array<number>>}
* @const
*/
proto.ClientMessage.oneofGroups_ = [[11,12,13]];
proto.ClientMessage.oneofGroups_ = [[20,21]];
/**
* @enum {number}
*/
proto.ClientMessage.MsgCase = {
MSG_NOT_SET: 0,
NEW_EVAL: 11,
EVAL_EVENT: 12,
PING: 13
METHOD: 20,
PING: 21
};
/**
@@ -91,8 +90,7 @@ proto.ClientMessage.prototype.toObject = function(opt_includeInstance) {
*/
proto.ClientMessage.toObject = function(includeInstance, msg) {
var f, obj = {
newEval: (f = msg.getNewEval()) && node_pb.NewEvalMessage.toObject(includeInstance, f),
evalEvent: (f = msg.getEvalEvent()) && node_pb.EvalEventMessage.toObject(includeInstance, f),
method: (f = msg.getMethod()) && node_pb.MethodMessage.toObject(includeInstance, f),
ping: (f = msg.getPing()) && node_pb.Ping.toObject(includeInstance, f)
};
@@ -130,17 +128,12 @@ proto.ClientMessage.deserializeBinaryFromReader = function(msg, reader) {
}
var field = reader.getFieldNumber();
switch (field) {
case 11:
var value = new node_pb.NewEvalMessage;
reader.readMessage(value,node_pb.NewEvalMessage.deserializeBinaryFromReader);
msg.setNewEval(value);
case 20:
var value = new node_pb.MethodMessage;
reader.readMessage(value,node_pb.MethodMessage.deserializeBinaryFromReader);
msg.setMethod(value);
break;
case 12:
var value = new node_pb.EvalEventMessage;
reader.readMessage(value,node_pb.EvalEventMessage.deserializeBinaryFromReader);
msg.setEvalEvent(value);
break;
case 13:
case 21:
var value = new node_pb.Ping;
reader.readMessage(value,node_pb.Ping.deserializeBinaryFromReader);
msg.setPing(value);
@@ -174,26 +167,18 @@ proto.ClientMessage.prototype.serializeBinary = function() {
*/
proto.ClientMessage.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getNewEval();
f = message.getMethod();
if (f != null) {
writer.writeMessage(
11,
20,
f,
node_pb.NewEvalMessage.serializeBinaryToWriter
);
}
f = message.getEvalEvent();
if (f != null) {
writer.writeMessage(
12,
f,
node_pb.EvalEventMessage.serializeBinaryToWriter
node_pb.MethodMessage.serializeBinaryToWriter
);
}
f = message.getPing();
if (f != null) {
writer.writeMessage(
13,
21,
f,
node_pb.Ping.serializeBinaryToWriter
);
@@ -202,23 +187,23 @@ proto.ClientMessage.serializeBinaryToWriter = function(message, writer) {
/**
* optional NewEvalMessage new_eval = 11;
* @return {?proto.NewEvalMessage}
* optional MethodMessage method = 20;
* @return {?proto.MethodMessage}
*/
proto.ClientMessage.prototype.getNewEval = function() {
return /** @type{?proto.NewEvalMessage} */ (
jspb.Message.getWrapperField(this, node_pb.NewEvalMessage, 11));
proto.ClientMessage.prototype.getMethod = function() {
return /** @type{?proto.MethodMessage} */ (
jspb.Message.getWrapperField(this, node_pb.MethodMessage, 20));
};
/** @param {?proto.NewEvalMessage|undefined} value */
proto.ClientMessage.prototype.setNewEval = function(value) {
jspb.Message.setOneofWrapperField(this, 11, proto.ClientMessage.oneofGroups_[0], value);
/** @param {?proto.MethodMessage|undefined} value */
proto.ClientMessage.prototype.setMethod = function(value) {
jspb.Message.setOneofWrapperField(this, 20, proto.ClientMessage.oneofGroups_[0], value);
};
proto.ClientMessage.prototype.clearNewEval = function() {
this.setNewEval(undefined);
proto.ClientMessage.prototype.clearMethod = function() {
this.setMethod(undefined);
};
@@ -226,54 +211,24 @@ proto.ClientMessage.prototype.clearNewEval = function() {
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ClientMessage.prototype.hasNewEval = function() {
return jspb.Message.getField(this, 11) != null;
proto.ClientMessage.prototype.hasMethod = function() {
return jspb.Message.getField(this, 20) != null;
};
/**
* optional EvalEventMessage eval_event = 12;
* @return {?proto.EvalEventMessage}
*/
proto.ClientMessage.prototype.getEvalEvent = function() {
return /** @type{?proto.EvalEventMessage} */ (
jspb.Message.getWrapperField(this, node_pb.EvalEventMessage, 12));
};
/** @param {?proto.EvalEventMessage|undefined} value */
proto.ClientMessage.prototype.setEvalEvent = function(value) {
jspb.Message.setOneofWrapperField(this, 12, proto.ClientMessage.oneofGroups_[0], value);
};
proto.ClientMessage.prototype.clearEvalEvent = function() {
this.setEvalEvent(undefined);
};
/**
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ClientMessage.prototype.hasEvalEvent = function() {
return jspb.Message.getField(this, 12) != null;
};
/**
* optional Ping ping = 13;
* optional Ping ping = 21;
* @return {?proto.Ping}
*/
proto.ClientMessage.prototype.getPing = function() {
return /** @type{?proto.Ping} */ (
jspb.Message.getWrapperField(this, node_pb.Ping, 13));
jspb.Message.getWrapperField(this, node_pb.Ping, 21));
};
/** @param {?proto.Ping|undefined} value */
proto.ClientMessage.prototype.setPing = function(value) {
jspb.Message.setOneofWrapperField(this, 13, proto.ClientMessage.oneofGroups_[0], value);
jspb.Message.setOneofWrapperField(this, 21, proto.ClientMessage.oneofGroups_[0], value);
};
@@ -287,7 +242,7 @@ proto.ClientMessage.prototype.clearPing = function() {
* @return {!boolean}
*/
proto.ClientMessage.prototype.hasPing = function() {
return jspb.Message.getField(this, 13) != null;
return jspb.Message.getField(this, 21) != null;
};
@@ -317,19 +272,20 @@ if (goog.DEBUG && !COMPILED) {
* @private {!Array<!Array<number>>}
* @const
*/
proto.ServerMessage.oneofGroups_ = [[13,14,15,16,17,18]];
proto.ServerMessage.oneofGroups_ = [[13,14,19,22,18,16,17]];
/**
* @enum {number}
*/
proto.ServerMessage.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
SHARED_PROCESS_ACTIVE: 17
};
/**
@@ -368,12 +324,13 @@ proto.ServerMessage.prototype.toObject = function(opt_includeInstance) {
*/
proto.ServerMessage.toObject = function(includeInstance, msg) {
var f, obj = {
evalFailed: (f = msg.getEvalFailed()) && node_pb.EvalFailedMessage.toObject(includeInstance, f),
evalDone: (f = msg.getEvalDone()) && node_pb.EvalDoneMessage.toObject(includeInstance, f),
evalEvent: (f = msg.getEvalEvent()) && node_pb.EvalEventMessage.toObject(includeInstance, f),
fail: (f = msg.getFail()) && node_pb.FailMessage.toObject(includeInstance, f),
success: (f = msg.getSuccess()) && node_pb.SuccessMessage.toObject(includeInstance, f),
event: (f = msg.getEvent()) && node_pb.EventMessage.toObject(includeInstance, f),
callback: (f = msg.getCallback()) && node_pb.CallbackMessage.toObject(includeInstance, f),
pong: (f = msg.getPong()) && node_pb.Pong.toObject(includeInstance, f),
init: (f = msg.getInit()) && proto.WorkingInitMessage.toObject(includeInstance, f),
sharedProcessActive: (f = msg.getSharedProcessActive()) && vscode_pb.SharedProcessActiveMessage.toObject(includeInstance, f),
pong: (f = msg.getPong()) && node_pb.Pong.toObject(includeInstance, f)
sharedProcessActive: (f = msg.getSharedProcessActive()) && vscode_pb.SharedProcessActiveMessage.toObject(includeInstance, f)
};
if (includeInstance) {
@@ -411,19 +368,29 @@ proto.ServerMessage.deserializeBinaryFromReader = function(msg, reader) {
var field = reader.getFieldNumber();
switch (field) {
case 13:
var value = new node_pb.EvalFailedMessage;
reader.readMessage(value,node_pb.EvalFailedMessage.deserializeBinaryFromReader);
msg.setEvalFailed(value);
var value = new node_pb.FailMessage;
reader.readMessage(value,node_pb.FailMessage.deserializeBinaryFromReader);
msg.setFail(value);
break;
case 14:
var value = new node_pb.EvalDoneMessage;
reader.readMessage(value,node_pb.EvalDoneMessage.deserializeBinaryFromReader);
msg.setEvalDone(value);
var value = new node_pb.SuccessMessage;
reader.readMessage(value,node_pb.SuccessMessage.deserializeBinaryFromReader);
msg.setSuccess(value);
break;
case 15:
var value = new node_pb.EvalEventMessage;
reader.readMessage(value,node_pb.EvalEventMessage.deserializeBinaryFromReader);
msg.setEvalEvent(value);
case 19:
var value = new node_pb.EventMessage;
reader.readMessage(value,node_pb.EventMessage.deserializeBinaryFromReader);
msg.setEvent(value);
break;
case 22:
var value = new node_pb.CallbackMessage;
reader.readMessage(value,node_pb.CallbackMessage.deserializeBinaryFromReader);
msg.setCallback(value);
break;
case 18:
var value = new node_pb.Pong;
reader.readMessage(value,node_pb.Pong.deserializeBinaryFromReader);
msg.setPong(value);
break;
case 16:
var value = new proto.WorkingInitMessage;
@@ -435,11 +402,6 @@ proto.ServerMessage.deserializeBinaryFromReader = function(msg, reader) {
reader.readMessage(value,vscode_pb.SharedProcessActiveMessage.deserializeBinaryFromReader);
msg.setSharedProcessActive(value);
break;
case 18:
var value = new node_pb.Pong;
reader.readMessage(value,node_pb.Pong.deserializeBinaryFromReader);
msg.setPong(value);
break;
default:
reader.skipField();
break;
@@ -469,28 +431,44 @@ proto.ServerMessage.prototype.serializeBinary = function() {
*/
proto.ServerMessage.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getEvalFailed();
f = message.getFail();
if (f != null) {
writer.writeMessage(
13,
f,
node_pb.EvalFailedMessage.serializeBinaryToWriter
node_pb.FailMessage.serializeBinaryToWriter
);
}
f = message.getEvalDone();
f = message.getSuccess();
if (f != null) {
writer.writeMessage(
14,
f,
node_pb.EvalDoneMessage.serializeBinaryToWriter
node_pb.SuccessMessage.serializeBinaryToWriter
);
}
f = message.getEvalEvent();
f = message.getEvent();
if (f != null) {
writer.writeMessage(
15,
19,
f,
node_pb.EvalEventMessage.serializeBinaryToWriter
node_pb.EventMessage.serializeBinaryToWriter
);
}
f = message.getCallback();
if (f != null) {
writer.writeMessage(
22,
f,
node_pb.CallbackMessage.serializeBinaryToWriter
);
}
f = message.getPong();
if (f != null) {
writer.writeMessage(
18,
f,
node_pb.Pong.serializeBinaryToWriter
);
}
f = message.getInit();
@@ -509,35 +487,27 @@ proto.ServerMessage.serializeBinaryToWriter = function(message, writer) {
vscode_pb.SharedProcessActiveMessage.serializeBinaryToWriter
);
}
f = message.getPong();
if (f != null) {
writer.writeMessage(
18,
f,
node_pb.Pong.serializeBinaryToWriter
);
}
};
/**
* optional EvalFailedMessage eval_failed = 13;
* @return {?proto.EvalFailedMessage}
* optional FailMessage fail = 13;
* @return {?proto.FailMessage}
*/
proto.ServerMessage.prototype.getEvalFailed = function() {
return /** @type{?proto.EvalFailedMessage} */ (
jspb.Message.getWrapperField(this, node_pb.EvalFailedMessage, 13));
proto.ServerMessage.prototype.getFail = function() {
return /** @type{?proto.FailMessage} */ (
jspb.Message.getWrapperField(this, node_pb.FailMessage, 13));
};
/** @param {?proto.EvalFailedMessage|undefined} value */
proto.ServerMessage.prototype.setEvalFailed = function(value) {
/** @param {?proto.FailMessage|undefined} value */
proto.ServerMessage.prototype.setFail = function(value) {
jspb.Message.setOneofWrapperField(this, 13, proto.ServerMessage.oneofGroups_[0], value);
};
proto.ServerMessage.prototype.clearEvalFailed = function() {
this.setEvalFailed(undefined);
proto.ServerMessage.prototype.clearFail = function() {
this.setFail(undefined);
};
@@ -545,29 +515,29 @@ proto.ServerMessage.prototype.clearEvalFailed = function() {
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ServerMessage.prototype.hasEvalFailed = function() {
proto.ServerMessage.prototype.hasFail = function() {
return jspb.Message.getField(this, 13) != null;
};
/**
* optional EvalDoneMessage eval_done = 14;
* @return {?proto.EvalDoneMessage}
* optional SuccessMessage success = 14;
* @return {?proto.SuccessMessage}
*/
proto.ServerMessage.prototype.getEvalDone = function() {
return /** @type{?proto.EvalDoneMessage} */ (
jspb.Message.getWrapperField(this, node_pb.EvalDoneMessage, 14));
proto.ServerMessage.prototype.getSuccess = function() {
return /** @type{?proto.SuccessMessage} */ (
jspb.Message.getWrapperField(this, node_pb.SuccessMessage, 14));
};
/** @param {?proto.EvalDoneMessage|undefined} value */
proto.ServerMessage.prototype.setEvalDone = function(value) {
/** @param {?proto.SuccessMessage|undefined} value */
proto.ServerMessage.prototype.setSuccess = function(value) {
jspb.Message.setOneofWrapperField(this, 14, proto.ServerMessage.oneofGroups_[0], value);
};
proto.ServerMessage.prototype.clearEvalDone = function() {
this.setEvalDone(undefined);
proto.ServerMessage.prototype.clearSuccess = function() {
this.setSuccess(undefined);
};
@@ -575,29 +545,29 @@ proto.ServerMessage.prototype.clearEvalDone = function() {
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ServerMessage.prototype.hasEvalDone = function() {
proto.ServerMessage.prototype.hasSuccess = function() {
return jspb.Message.getField(this, 14) != null;
};
/**
* optional EvalEventMessage eval_event = 15;
* @return {?proto.EvalEventMessage}
* optional EventMessage event = 19;
* @return {?proto.EventMessage}
*/
proto.ServerMessage.prototype.getEvalEvent = function() {
return /** @type{?proto.EvalEventMessage} */ (
jspb.Message.getWrapperField(this, node_pb.EvalEventMessage, 15));
proto.ServerMessage.prototype.getEvent = function() {
return /** @type{?proto.EventMessage} */ (
jspb.Message.getWrapperField(this, node_pb.EventMessage, 19));
};
/** @param {?proto.EvalEventMessage|undefined} value */
proto.ServerMessage.prototype.setEvalEvent = function(value) {
jspb.Message.setOneofWrapperField(this, 15, proto.ServerMessage.oneofGroups_[0], value);
/** @param {?proto.EventMessage|undefined} value */
proto.ServerMessage.prototype.setEvent = function(value) {
jspb.Message.setOneofWrapperField(this, 19, proto.ServerMessage.oneofGroups_[0], value);
};
proto.ServerMessage.prototype.clearEvalEvent = function() {
this.setEvalEvent(undefined);
proto.ServerMessage.prototype.clearEvent = function() {
this.setEvent(undefined);
};
@@ -605,8 +575,68 @@ proto.ServerMessage.prototype.clearEvalEvent = function() {
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ServerMessage.prototype.hasEvalEvent = function() {
return jspb.Message.getField(this, 15) != null;
proto.ServerMessage.prototype.hasEvent = function() {
return jspb.Message.getField(this, 19) != null;
};
/**
* optional CallbackMessage callback = 22;
* @return {?proto.CallbackMessage}
*/
proto.ServerMessage.prototype.getCallback = function() {
return /** @type{?proto.CallbackMessage} */ (
jspb.Message.getWrapperField(this, node_pb.CallbackMessage, 22));
};
/** @param {?proto.CallbackMessage|undefined} value */
proto.ServerMessage.prototype.setCallback = function(value) {
jspb.Message.setOneofWrapperField(this, 22, proto.ServerMessage.oneofGroups_[0], value);
};
proto.ServerMessage.prototype.clearCallback = function() {
this.setCallback(undefined);
};
/**
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ServerMessage.prototype.hasCallback = function() {
return jspb.Message.getField(this, 22) != null;
};
/**
* optional Pong pong = 18;
* @return {?proto.Pong}
*/
proto.ServerMessage.prototype.getPong = function() {
return /** @type{?proto.Pong} */ (
jspb.Message.getWrapperField(this, node_pb.Pong, 18));
};
/** @param {?proto.Pong|undefined} value */
proto.ServerMessage.prototype.setPong = function(value) {
jspb.Message.setOneofWrapperField(this, 18, proto.ServerMessage.oneofGroups_[0], value);
};
proto.ServerMessage.prototype.clearPong = function() {
this.setPong(undefined);
};
/**
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ServerMessage.prototype.hasPong = function() {
return jspb.Message.getField(this, 18) != null;
};
@@ -670,36 +700,6 @@ proto.ServerMessage.prototype.hasSharedProcessActive = function() {
};
/**
* optional Pong pong = 18;
* @return {?proto.Pong}
*/
proto.ServerMessage.prototype.getPong = function() {
return /** @type{?proto.Pong} */ (
jspb.Message.getWrapperField(this, node_pb.Pong, 18));
};
/** @param {?proto.Pong|undefined} value */
proto.ServerMessage.prototype.setPong = function(value) {
jspb.Message.setOneofWrapperField(this, 18, proto.ServerMessage.oneofGroups_[0], value);
};
proto.ServerMessage.prototype.clearPong = function() {
this.setPong(undefined);
};
/**
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ServerMessage.prototype.hasPong = function() {
return jspb.Message.getField(this, 18) != null;
};
/**
* Generated by JsPbCodeGenerator.

View File

@@ -1,28 +1,90 @@
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 {
// A proxy identified by a unique name like "fs".
message NamedProxyMessage {
uint64 id = 1;
Module module = 2;
string method = 3;
repeated string args = 4;
}
// A general proxy identified by an ID like WriteStream.
message NumberedProxyMessage {
uint64 id = 1;
uint64 proxy_id = 2;
string method = 3;
repeated string args = 4;
}
// Call a remote method.
message MethodMessage {
oneof msg {
NamedProxyMessage named_proxy = 1;
NumberedProxyMessage numbered_proxy = 2;
}
}
// Call a remote callback.
message CallbackMessage {
oneof msg {
NamedCallbackMessage named_callback = 1;
NumberedCallbackMessage numbered_callback = 2;
}
}
// A remote callback for uniquely named proxy.
message NamedCallbackMessage {
Module module = 1;
uint64 callback_id = 2;
repeated string args = 3;
}
// A remote callback for a numbered proxy.
message NumberedCallbackMessage {
uint64 proxy_id = 1;
uint64 callback_id = 2;
repeated string args = 3;
}
// Emit an event.
message EventMessage {
oneof msg {
NamedEventMessage named_event = 1;
NumberedEventMessage numbered_event = 2;
}
}
// Emit an event on a uniquely named proxy.
message NamedEventMessage {
Module module = 1;
string event = 2;
repeated string args = 3;
}
message EvalFailedMessage {
// Emit an event on a numbered proxy.
message NumberedEventMessage {
uint64 proxy_id = 1;
string event = 2;
repeated string args = 3;
}
// Remote method failed.
message FailMessage {
uint64 id = 1;
string response = 2;
}
message EvalDoneMessage {
// Remote method succeeded.
message SuccessMessage {
uint64 id = 1;
string response = 2;
}

View File

@@ -3,48 +3,243 @@
import * as jspb from "google-protobuf";
export class NewEvalMessage extends jspb.Message {
export class NamedProxyMessage extends jspb.Message {
getId(): number;
setId(value: number): void;
getFunction(): string;
setFunction(value: string): void;
getModule(): Module;
setModule(value: Module): void;
getMethod(): string;
setMethod(value: string): void;
clearArgsList(): void;
getArgsList(): Array<string>;
setArgsList(value: Array<string>): void;
addArgs(value: string, index?: number): string;
getTimeout(): number;
setTimeout(value: number): void;
getActive(): boolean;
setActive(value: boolean): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NewEvalMessage.AsObject;
static toObject(includeInstance: boolean, msg: NewEvalMessage): NewEvalMessage.AsObject;
toObject(includeInstance?: boolean): NamedProxyMessage.AsObject;
static toObject(includeInstance: boolean, msg: NamedProxyMessage): NamedProxyMessage.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: NamedProxyMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NamedProxyMessage;
static deserializeBinaryFromReader(message: NamedProxyMessage, reader: jspb.BinaryReader): NamedProxyMessage;
}
export namespace NewEvalMessage {
export namespace NamedProxyMessage {
export type AsObject = {
id: number,
pb_function: string,
module: Module,
method: string,
argsList: Array<string>,
timeout: number,
active: boolean,
}
}
export class EvalEventMessage extends jspb.Message {
export class NumberedProxyMessage 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<string>;
setArgsList(value: Array<string>): void;
addArgs(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NumberedProxyMessage.AsObject;
static toObject(includeInstance: boolean, msg: NumberedProxyMessage): NumberedProxyMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: NumberedProxyMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NumberedProxyMessage;
static deserializeBinaryFromReader(message: NumberedProxyMessage, reader: jspb.BinaryReader): NumberedProxyMessage;
}
export namespace NumberedProxyMessage {
export type AsObject = {
id: number,
proxyId: number,
method: string,
argsList: Array<string>,
}
}
export class MethodMessage extends jspb.Message {
hasNamedProxy(): boolean;
clearNamedProxy(): void;
getNamedProxy(): NamedProxyMessage | undefined;
setNamedProxy(value?: NamedProxyMessage): void;
hasNumberedProxy(): boolean;
clearNumberedProxy(): void;
getNumberedProxy(): NumberedProxyMessage | undefined;
setNumberedProxy(value?: NumberedProxyMessage): void;
getMsgCase(): MethodMessage.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): MethodMessage.AsObject;
static toObject(includeInstance: boolean, msg: MethodMessage): MethodMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: MethodMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): MethodMessage;
static deserializeBinaryFromReader(message: MethodMessage, reader: jspb.BinaryReader): MethodMessage;
}
export namespace MethodMessage {
export type AsObject = {
namedProxy?: NamedProxyMessage.AsObject,
numberedProxy?: NumberedProxyMessage.AsObject,
}
export enum MsgCase {
MSG_NOT_SET = 0,
NAMED_PROXY = 1,
NUMBERED_PROXY = 2,
}
}
export class CallbackMessage extends jspb.Message {
hasNamedCallback(): boolean;
clearNamedCallback(): void;
getNamedCallback(): NamedCallbackMessage | undefined;
setNamedCallback(value?: NamedCallbackMessage): void;
hasNumberedCallback(): boolean;
clearNumberedCallback(): void;
getNumberedCallback(): NumberedCallbackMessage | undefined;
setNumberedCallback(value?: NumberedCallbackMessage): void;
getMsgCase(): CallbackMessage.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): CallbackMessage.AsObject;
static toObject(includeInstance: boolean, msg: CallbackMessage): CallbackMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: CallbackMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): CallbackMessage;
static deserializeBinaryFromReader(message: CallbackMessage, reader: jspb.BinaryReader): CallbackMessage;
}
export namespace CallbackMessage {
export type AsObject = {
namedCallback?: NamedCallbackMessage.AsObject,
numberedCallback?: NumberedCallbackMessage.AsObject,
}
export enum MsgCase {
MSG_NOT_SET = 0,
NAMED_CALLBACK = 1,
NUMBERED_CALLBACK = 2,
}
}
export class NamedCallbackMessage extends jspb.Message {
getModule(): Module;
setModule(value: Module): void;
getCallbackId(): number;
setCallbackId(value: number): void;
clearArgsList(): void;
getArgsList(): Array<string>;
setArgsList(value: Array<string>): void;
addArgs(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NamedCallbackMessage.AsObject;
static toObject(includeInstance: boolean, msg: NamedCallbackMessage): NamedCallbackMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: NamedCallbackMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NamedCallbackMessage;
static deserializeBinaryFromReader(message: NamedCallbackMessage, reader: jspb.BinaryReader): NamedCallbackMessage;
}
export namespace NamedCallbackMessage {
export type AsObject = {
module: Module,
callbackId: number,
argsList: Array<string>,
}
}
export class NumberedCallbackMessage extends jspb.Message {
getProxyId(): number;
setProxyId(value: number): void;
getCallbackId(): number;
setCallbackId(value: number): void;
clearArgsList(): void;
getArgsList(): Array<string>;
setArgsList(value: Array<string>): void;
addArgs(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NumberedCallbackMessage.AsObject;
static toObject(includeInstance: boolean, msg: NumberedCallbackMessage): NumberedCallbackMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: NumberedCallbackMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NumberedCallbackMessage;
static deserializeBinaryFromReader(message: NumberedCallbackMessage, reader: jspb.BinaryReader): NumberedCallbackMessage;
}
export namespace NumberedCallbackMessage {
export type AsObject = {
proxyId: number,
callbackId: number,
argsList: Array<string>,
}
}
export class EventMessage extends jspb.Message {
hasNamedEvent(): boolean;
clearNamedEvent(): void;
getNamedEvent(): NamedEventMessage | undefined;
setNamedEvent(value?: NamedEventMessage): void;
hasNumberedEvent(): boolean;
clearNumberedEvent(): void;
getNumberedEvent(): NumberedEventMessage | undefined;
setNumberedEvent(value?: NumberedEventMessage): void;
getMsgCase(): EventMessage.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): EventMessage.AsObject;
static toObject(includeInstance: boolean, msg: EventMessage): EventMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: EventMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): EventMessage;
static deserializeBinaryFromReader(message: EventMessage, reader: jspb.BinaryReader): EventMessage;
}
export namespace EventMessage {
export type AsObject = {
namedEvent?: NamedEventMessage.AsObject,
numberedEvent?: NumberedEventMessage.AsObject,
}
export enum MsgCase {
MSG_NOT_SET = 0,
NAMED_EVENT = 1,
NUMBERED_EVENT = 2,
}
}
export class NamedEventMessage extends jspb.Message {
getModule(): Module;
setModule(value: Module): void;
getEvent(): string;
setEvent(value: string): void;
@@ -54,24 +249,54 @@ export class EvalEventMessage extends jspb.Message {
addArgs(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): EvalEventMessage.AsObject;
static toObject(includeInstance: boolean, msg: EvalEventMessage): EvalEventMessage.AsObject;
toObject(includeInstance?: boolean): NamedEventMessage.AsObject;
static toObject(includeInstance: boolean, msg: NamedEventMessage): NamedEventMessage.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: NamedEventMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NamedEventMessage;
static deserializeBinaryFromReader(message: NamedEventMessage, reader: jspb.BinaryReader): NamedEventMessage;
}
export namespace EvalEventMessage {
export namespace NamedEventMessage {
export type AsObject = {
id: number,
module: Module,
event: string,
argsList: Array<string>,
}
}
export class EvalFailedMessage extends jspb.Message {
export class NumberedEventMessage extends jspb.Message {
getProxyId(): number;
setProxyId(value: number): void;
getEvent(): string;
setEvent(value: string): void;
clearArgsList(): void;
getArgsList(): Array<string>;
setArgsList(value: Array<string>): void;
addArgs(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NumberedEventMessage.AsObject;
static toObject(includeInstance: boolean, msg: NumberedEventMessage): NumberedEventMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: NumberedEventMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NumberedEventMessage;
static deserializeBinaryFromReader(message: NumberedEventMessage, reader: jspb.BinaryReader): NumberedEventMessage;
}
export namespace NumberedEventMessage {
export type AsObject = {
proxyId: number,
event: string,
argsList: Array<string>,
}
}
export class FailMessage extends jspb.Message {
getId(): number;
setId(value: number): void;
@@ -79,23 +304,23 @@ export class EvalFailedMessage extends jspb.Message {
setResponse(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): EvalFailedMessage.AsObject;
static toObject(includeInstance: boolean, msg: EvalFailedMessage): EvalFailedMessage.AsObject;
toObject(includeInstance?: boolean): FailMessage.AsObject;
static toObject(includeInstance: boolean, msg: FailMessage): FailMessage.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: FailMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): FailMessage;
static deserializeBinaryFromReader(message: FailMessage, reader: jspb.BinaryReader): FailMessage;
}
export namespace EvalFailedMessage {
export namespace FailMessage {
export type AsObject = {
id: number,
response: string,
}
}
export class EvalDoneMessage extends jspb.Message {
export class SuccessMessage extends jspb.Message {
getId(): number;
setId(value: number): void;
@@ -103,16 +328,16 @@ export class EvalDoneMessage extends jspb.Message {
setResponse(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): EvalDoneMessage.AsObject;
static toObject(includeInstance: boolean, msg: EvalDoneMessage): EvalDoneMessage.AsObject;
toObject(includeInstance?: boolean): SuccessMessage.AsObject;
static toObject(includeInstance: boolean, msg: SuccessMessage): SuccessMessage.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: SuccessMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): SuccessMessage;
static deserializeBinaryFromReader(message: SuccessMessage, reader: jspb.BinaryReader): SuccessMessage;
}
export namespace EvalDoneMessage {
export namespace SuccessMessage {
export type AsObject = {
id: number,
response: string,
@@ -151,3 +376,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

@@ -3,12 +3,12 @@ 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))
.then((s) => s.toString());
@@ -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

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

@@ -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.4",
"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,266 @@
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 { 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")
.option("--cert-key")
.option("-d, --data-dir <value>", "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 }),
};
public static args = [{
name: "workdir",
description: "Specify working dir",
default: (): string => process.cwd(),
}];
public async run(): Promise<void> {
if (isCli) {
fillFs();
}
const { args, flags } = this.parse(Entry);
const dataDir = path.resolve(flags["data-dir"] || path.join(dataHome, "code-server"));
const workingDir = path.resolve(args["workdir"]);
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));
}
}
}
Error.stackTraceLimit = Infinity;
if (isCli) {
require("nbin").shimNativeFs(buildDir);
}
Entry.run(undefined, {
root: buildDir || __dirname,
version: process.env.VERSION || "development",
//@ts-ignore
}).catch(require("@oclif/errors/handle"));
(async (): Promise<void> => {
const args = commander.args;
const options = commander.opts() as {
noAuth: boolean;
readonly allowHttp: boolean;
readonly host: string;
readonly port: number;
readonly dataDir?: string;
readonly password?: string;
readonly open?: boolean;
readonly cert?: string;
readonly certKey?: string;
readonly bootstrapFork?: string;
readonly fork?: string;
readonly extraArgs?: string;
};
// 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 dataDir = path.resolve(options.dataDir || path.join(dataHome, "code-server"));
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 (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"}`);
// 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 = 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: {
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));
});
});
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://coder.com/docs");
}
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>;
@@ -75,11 +76,13 @@ export const createPortScanner = (scanInterval: number = 250): PortScanner => {
let disposed: boolean = false;
const doInterval = (): void => {
scan(() => {
if (disposed) {
return;
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,8 @@
import * as cp from "child_process";
import * as fs from "fs";
import * as path from "path";
import * as zlib from "zlib";
import * as vm from "vm";
import { isCli } from "../constants";
import { buildDir, isCli } from "../constants";
let ipcMsgBuffer: Buffer[] | undefined = [];
let ipcMsgListener = process.send ? (d: Buffer): number => ipcMsgBuffer!.push(d) : undefined;
@@ -11,6 +10,8 @@ if (ipcMsgListener) {
process.on("message", ipcMsgListener);
}
declare var __non_webpack_require__: typeof require;
/**
* Requires a module from the filesystem.
*
@@ -29,7 +30,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 +47,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 +69,6 @@ export const requireFork = (modulePath: string, args: string[], builtInExtension
}
process.argv = ["", "", ...args];
requireFilesystemModule(modulePath, builtInExtensionsDir);
if (ipcMsgBuffer && ipcMsgListener) {
@@ -96,23 +105,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,25 +129,25 @@ 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);
}

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

@@ -8,6 +8,10 @@ import product from "./fill/product";
import "./vscode.scss";
import { MenuId, MenuRegistry } from "vs/platform/actions/common/actions";
import { CommandsRegistry } from "vs/platform/commands/common/commands";
import { IFileService, FileOperation } from "vs/platform/files/common/files";
import { ITextFileService } from "vs/workbench/services/textfile/common/textfiles";
import { IModelService } from "vs/editor/common/services/modelService";
import { ITerminalService } from "vs/workbench/contrib/terminal/common/terminal";
// NOTE: shouldn't import anything from VS Code here or anything that will
// depend on a synchronous fill like `os`.
@@ -34,6 +38,63 @@ class VSClient extends IdeClient {
// tslint:disable-next-line:no-any
statusbarService: getService<IStatusbarService>(IStatusbarService) as any,
notificationService: getService<INotificationService>(INotificationService),
onFileCreate: (cb): void => {
getService<IFileService>(IFileService).onAfterOperation((e) => {
if (e.operation === FileOperation.CREATE) {
cb(e.resource.path);
}
});
},
onFileMove: (cb): void => {
getService<IFileService>(IFileService).onAfterOperation((e) => {
if (e.operation === FileOperation.MOVE) {
cb(e.resource.path, e.target ? e.target.resource.path : undefined!);
}
});
},
onFileDelete: (cb): void => {
getService<IFileService>(IFileService).onAfterOperation((e) => {
if (e.operation === FileOperation.DELETE) {
cb(e.resource.path);
}
});
},
onFileSaved: (cb): void => {
getService<ITextFileService>(ITextFileService).models.onModelSaved((e) => {
cb(e.resource.path);
});
},
onFileCopy: (cb): void => {
getService<IFileService>(IFileService).onAfterOperation((e) => {
if (e.operation === FileOperation.COPY) {
cb(e.resource.path, e.target ? e.target.resource.path : undefined!);
}
});
},
onModelAdded: (cb): void => {
getService<IModelService>(IModelService).onModelAdded((e) => {
cb(e.uri.path, e.getLanguageIdentifier().language);
});
},
onModelRemoved: (cb): void => {
getService<IModelService>(IModelService).onModelRemoved((e) => {
cb(e.uri.path, e.getLanguageIdentifier().language);
});
},
onModelLanguageChange: (cb): void => {
getService<IModelService>(IModelService).onModelModeChanged((e) => {
cb(e.model.uri.path, e.model.getLanguageIdentifier().language, e.oldModeId);
});
},
onTerminalAdded: (cb): void => {
getService<ITerminalService>(ITerminalService).onInstanceCreated(() => cb());
},
onTerminalRemoved: (cb): void => {
getService<ITerminalService>(ITerminalService).onInstanceDisposed(() => cb());
},
},
// @ts-ignore

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

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

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

@@ -185,11 +185,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