Compare commits

...

47 Commits

Author SHA1 Message Date
Asher
04adf14146 Move OSX package task into build script
This is to match how the other binaries are built. Also made some
changes to make the Docker containers clean up for when you are running
this locally.
2019-06-06 13:43:37 -05:00
Liudas Sodonis aka lfx aka lso
406ec0ba71 Updated google_cloud to have proper link to ssl.md (#745) 2019-06-05 15:28:10 -05:00
Asher
a2ad3d4ff4 Show hidden files by default
Since there is no other way to enable hidden files, it seems better to
enable it by default otherwise there are some folders/files you simply
can never open from the dialog.
2019-05-20 16:08:54 -05:00
Asher
4a29cd1664 Fix human readable byte size when zero 2019-05-20 15:53:06 -05:00
Anmol Sethi
0462a93f11 Expose actions registry (#701) 2019-05-20 14:35:58 -05:00
Asher
db39eacfa1 Set NODE_ENV and VERSION when building (#700)
* Set NODE_ENV and VERSION when building

Should fix the version flag not reporting correctly as well as enable
the service worker and prevent the 404 hmr requests again.

* Log env vars

To help make sure it's built correctly when looking at the Travis logs.
2019-05-20 11:02:36 -05:00
Asher
c020cd2f2c Don't try to create builtin extensions directory
Since this will be a path in the binary that we don't want to create on
the user's system. I also removed the option to override it; it doesn't
seem like a great idea since you'd always want those builtin extensions.
This way we also don't have to check if the option was passed and only
create it if that was the case.
2019-05-19 19:49:05 -05:00
Asher
81bbfa7fbe Suppress "disconnected" notification on extension host
This isn't a real error event; we artificially emit it just in case
something waiting to start is listening to the error event in order to
clean up and/or restart.
2019-05-19 19:21:25 -05:00
Asher
aa1474b675 Extra extensions directories (#694)
* Allow setting paths for builtin exts and extra dirs

The extra directories aren't used yet, just available from the
environment service and to the shared process.

* Utilize extra builtin extensions path

* Utilize extra extensions directory

* Fix cached mtimes for extra extension dirs

* Simplify extension cache equality check
2019-05-19 17:58:47 -05:00
bastigw
8256252967 Updated Data Directory Flag (#664)
Old Version contained a deprecated flag
2019-05-19 17:26:09 -05:00
Ram
07342bbee7 Remove broken links (#671) 2019-05-19 17:24:57 -05:00
Anmol Sethi
72152f74ab Fix docker oneliner in README.md 2019-05-02 13:32:05 -04:00
Anmol Sethi
420ca76f54 Merge pull request #635 from cdr/rename
Fix macOS release
2019-05-02 12:23:42 -04:00
Anmol Sethi
31503fc853 Fix macOS release 2019-05-02 12:23:17 -04:00
Anmol Sethi
cf399ef6ac Merge pull request #634 from cdr/rename
Rename codercom/code-server to cdr/code-server
2019-05-02 12:07:48 -04:00
Anmol Sethi
bb5836ec61 Rename codercom/code-server to cdr/code-server 2019-05-02 11:25:50 -04:00
Anmol Sethi
f36235e03f Merge pull request #633 from cdr/show-terminal-api
expose terminal service in IDE API
2019-05-02 11:08:23 -04:00
Anmol Sethi
6ef1628acb Expose Terminal Service in API
Will need in sail.
2019-05-02 10:27:28 -04:00
Anmol Sethi
ab8f8a0a22 Merge pull request #520 from nhooyr/volume
Remove chmod on project dir
2019-04-29 19:06:07 -04:00
Asher
cdb900aca8 Make preserveEnv return a new object
Modifying the object didn't feel quite right, plus this makes the code a
bit more compact.
2019-04-29 11:49:59 -05:00
Fedor Kalugin
1622fd4152 Preserve environment when forking shared process (#545) 2019-04-29 10:47:45 -05:00
Kyle Carberry
6c972e855f codercom -> cdr 2019-04-27 16:57:10 -04:00
Kyle Carberry
e332882a88 Package only on darwin 2019-04-26 10:51:38 -05:00
Kyle Carberry
d0142e2536 Include version with target env 2019-04-26 10:29:12 -04:00
Kyle Carberry
e8c8fba91d Add docker service 2019-04-26 10:00:15 -04:00
Kyle Carberry
01a63a7241 Merge branch 'master' of github.com:codercom/code-server 2019-04-26 09:57:16 -04:00
Kyle Carberry
a2e0638c6a Add support for musl and centos 2019-04-26 09:56:14 -04:00
John McCambridge
4e62f938a9 Remove reveal in finder/explorer option from the context menu (#586) 2019-04-25 15:23:03 -05:00
Asher
4c5bb83fc1 Fix open dialog crash when there is a broken link
Fixes #579.
2019-04-25 15:17:22 -05:00
Asher
a3ac4567e3 Only output password if it was generated 2019-04-25 14:08:46 -05:00
Asher
58cf109a83 Fix full screen detection for Chromium 2019-04-25 13:29:11 -05:00
Asher
fab45dedcd Fix toggling full screen 2019-04-25 13:22:30 -05:00
Asher
446573809c Improve size column in dialogs
- Remove size from directories (often always 4K and not very useful).
- Format file sizes to be more human-readable.
2019-04-25 12:07:35 -05:00
Nick Wade
5ad9398b01 Fix typo DigitalOcean (#595) 2019-04-25 07:57:12 -07:00
Kyle Carberry
bcdbd90197 Fix #587 (#588) 2019-04-24 18:34:57 -05:00
Asher
0de7247868 Fix protocol fs test 2019-04-24 18:15:56 -05:00
Asher
c9f91e77cd Fix coping and moving files around using the file tree (#568)
* Implement write/read buffers in electron fill

This makes cutting and copy files from the file tree work.

* Implement fs.createReadStream

This is used by the file tree to copy files.

* Allow passing proxies back from client to server

This makes things like piping streams possible.

* Synchronously bind to proxy events

This eliminates any chance whatsoever of missing events due to binding
too late.

* Make it possible to bind some events on demand

* Add some protocol documentation
2019-04-24 10:38:21 -05:00
John McCambridge
30b8565e2d Fix markdown preview focus (#546)
* Fix hash

* Remove whitespace
2019-04-23 19:14:52 -05:00
John McCambridge
6b887dcc9c Fix no-auth to still use HTTPS, set default for no-auth to false (#573) 2019-04-23 16:38:49 -05:00
John McCambridge
41c7d98b7b Offer https/http url based on schema (#572)
* Let people know when telemetry is disabled, change url to https if secure connection

* Remove --no-auth as a http candidate

* Rename variable, change let to const
2019-04-23 16:38:34 -05:00
Kyle Carberry
b055a26dc3 Remove log statement from copy 2019-04-23 11:33:42 -04:00
Kyle Carberry
2bc6e1a457 Fix clipboard pasting 2019-04-22 20:20:48 -04:00
Kyle Carberry
e61ea796c6 Bundle grammars (#563) 2019-04-22 12:51:05 -05:00
Kyle Carberry
d073622629 Add --socket flag (#564)
* Add --socket flag

* Add msg for already bound socket
2019-04-22 12:47:27 -05:00
Kyle Carberry
5f40ebb845 Update wording for sshcode 2019-04-19 21:22:00 -04:00
Kyle Carberry
c56e2797cc Add sshcode to the readme 2019-04-19 21:20:31 -04:00
Anmol Sethi
cdc5b55a9d Remove chmod on project dir
See #471
2019-04-17 18:36:33 -04:00
55 changed files with 1252 additions and 410 deletions

View File

@@ -1,13 +1,21 @@
language: node_js
node_js:
- 10.15.1
env:
- VSCODE_VERSION="1.33.1" MAJOR_VERSION="1" VERSION="$MAJOR_VERSION.$TRAVIS_BUILD_NUMBER-vsc$VSCODE_VERSION"
services:
- docker
matrix:
include:
- os: linux
dist: trusty
env:
- VSCODE_VERSION="1.33.1" MAJOR_VERSION="1" VERSION="$MAJOR_VERSION.$TRAVIS_BUILD_NUMBER-vsc$VSCODE_VERSION" TARGET="centos"
- os: linux
dist: trusty
env:
- VSCODE_VERSION="1.33.1" MAJOR_VERSION="1" VERSION="$MAJOR_VERSION.$TRAVIS_BUILD_NUMBER-vsc$VSCODE_VERSION" TARGET="alpine"
- os: osx
env:
- VSCODE_VERSION="1.33.1" MAJOR_VERSION="1" VERSION="$MAJOR_VERSION.$TRAVIS_BUILD_NUMBER-vsc$VSCODE_VERSION"
before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install libxkbfile-dev
libsecret-1-dev; fi
@@ -19,7 +27,6 @@ before_deploy:
- git config --local user.name "$USER_NAME"
- git config --local user.email "$USER_EMAIL"
- git tag "$VERSION" "$TRAVIS_COMMIT"
- yarn task package "$VERSION"
deploy:
provider: releases
file_glob: true
@@ -34,7 +41,7 @@ deploy:
- release/*.tar.gz
- release/*.zip
on:
repo: codercom/code-server
repo: cdr/code-server
branch: master
cache:
yarn: true

View File

@@ -39,13 +39,12 @@ RUN adduser --gecos '' --disabled-password coder && \
USER coder
# We create first instead of just using WORKDIR as when WORKDIR creates, the user is root.
RUN mkdir -p /home/coder/project && \
chmod g+rw /home/coder/project;
RUN mkdir -p /home/coder/project
WORKDIR /home/coder/project
# This assures we have a volume mounted even if the user forgot to do bind mount.
# XXX: Workaround for GH-459 and for OpenShift compatibility.
# So that they do not lose their data if they delete the container.
VOLUME [ "/home/coder/project" ]
COPY --from=0 /src/packages/server/cli-linux-x64 /usr/local/bin/code-server

View File

@@ -1,8 +1,8 @@
# code-server
[!["Open Issues"](https://img.shields.io/github/issues-raw/codercom/code-server.svg)](https://github.com/codercom/code-server/issues)
[!["Latest Release"](https://img.shields.io/github/release/codercom/code-server.svg)](https://github.com/codercom/code-server/releases/latest)
[![MIT license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/codercom/code-server/blob/master/LICENSE)
[!["Open Issues"](https://img.shields.io/github/issues-raw/cdr/code-server.svg)](https://github.com/cdr/code-server/issues)
[!["Latest Release"](https://img.shields.io/github/release/cdr/code-server.svg)](https://github.com/cdr/code-server/releases/latest)
[![MIT license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/cdr/code-server/blob/master/LICENSE)
[![Discord](https://img.shields.io/discord/463752820026376202.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/zxSwN8Z)
`code-server` is [VS Code](https://github.com/Microsoft/vscode) running on a remote server, accessible through the browser.
@@ -23,13 +23,17 @@ docker run -it -p 127.0.0.1:8443:8443 -v "${PWD}:/home/coder/project" codercom/c
## Getting Started
### Run over SSH
Use [sshcode](https://github.com/codercom/sshcode) for a simple setup.
### Docker
See docker oneliner mentioned above. Dockerfile is at [/Dockerfile](/Dockerfile).
### Binaries
1. [Download a binary](https://github.com/codercom/code-server/releases) (Linux and OS X supported. Windows coming soon)
1. [Download a binary](https://github.com/cdr/code-server/releases) (Linux and OS X supported. Windows coming soon)
2. Start the binary with the project directory as the first argument
```
@@ -42,7 +46,7 @@ See docker oneliner mentioned above. Dockerfile is at [/Dockerfile](/Dockerfile)
For detailed instructions and troubleshooting, see the [self-hosted quick start guide](doc/self-hosted/index.md).
Quickstart guides for [Google Cloud](doc/admin/install/google_cloud.md), [AWS](doc/admin/install/aws.md), and [Digital Ocean](doc/admin/install/digitalocean.md).
Quickstart guides for [Google Cloud](doc/admin/install/google_cloud.md), [AWS](doc/admin/install/aws.md), and [DigitalOcean](doc/admin/install/digitalocean.md).
How to [secure your setup](/doc/security/ssl.md).

42
build/platform.ts Normal file
View File

@@ -0,0 +1,42 @@
/**
* Script that detects platform name and arch.
* Cannot use os.platform() as that won't detect libc version
*/
import * as cp from "child_process";
import * as fs from "fs";
import * as os from "os";
enum Lib {
GLIBC,
MUSL,
}
const CLIB: Lib | undefined = ((): Lib | undefined => {
if (os.platform() !== "linux") {
return;
}
const glibc = cp.spawnSync("getconf", ["GNU_LIBC_VERSION"]);
if (glibc.status === 0) {
return Lib.GLIBC;
}
const ldd = cp.spawnSync("ldd", ["--version"]);
if (ldd.stdout && ldd.stdout.indexOf("musl") !== -1) {
return Lib.MUSL;
}
const muslFile = fs.readdirSync("/lib").find((value) => value.startsWith("libc.musl"));
if (muslFile) {
return Lib.MUSL;
}
return Lib.GLIBC;
})();
export const platform = (): NodeJS.Platform | "musl" => {
if (CLIB === Lib.MUSL) {
return "musl";
}
return os.platform();
};

View File

@@ -1,7 +1,9 @@
import { register, run } from "@coder/runner";
import { logger, field } from "@coder/logger";
import * as fs from "fs";
import * as fse from "fs-extra";
import * as os from "os";
import { platform } from "./platform";
import * as path from "path";
import * as zlib from "zlib";
import * as https from "https";
@@ -16,6 +18,13 @@ const vscodeVersion = process.env.VSCODE_VERSION || "1.33.1";
const vsSourceUrl = `https://codesrv-ci.cdr.sh/vstar-${vscodeVersion}.tar.gz`;
const buildServerBinary = register("build:server:binary", async (runner) => {
logger.info("Building with environment", field("env", {
NODE_ENV: process.env.NODE_ENV,
VERSION: process.env.VERSION,
OSTYPE: process.env.OSTYPE,
TARGET: process.env.TARGET,
}));
await ensureInstalled();
await Promise.all([
buildBootstrapFork(),
@@ -180,12 +189,12 @@ 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}-${platform()}-${os.arch()}`;
const archiveDir = path.join(releasePath, archiveName);
fse.removeSync(archiveDir);
fse.mkdirpSync(archiveDir);
const binaryPath = path.join(__dirname, `../packages/server/cli-${os.platform()}-${os.arch()}`);
const binaryPath = path.join(__dirname, `../packages/server/cli-${platform()}-${os.arch()}`);
const binaryDestination = path.join(archiveDir, "code-server");
fse.copySync(binaryPath, binaryDestination);
fs.chmodSync(binaryDestination, "755");

View File

@@ -26,7 +26,7 @@ metadata:
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp2
fsType: ext4
fsType: ext4
---
kind: PersistentVolumeClaim
apiVersion: v1
@@ -71,4 +71,4 @@ spec:
- name: code-server-storage
persistentVolumeClaim:
claimName: code-store

View File

@@ -2,7 +2,7 @@
This tutorial shows you how to deploy `code-server` on an EC2 AWS instance.
If you're just starting out, we recommend [installing code-server locally](../../self-hosted/index.md). It takes only a few minutes and lets you try out all of the features. You can also try out the IDE on a container hosted [by Coder](http://coder.com/signup)
If you're just starting out, we recommend [installing code-server locally](../../self-hosted/index.md). It takes only a few minutes and lets you try out all of the features.
---
@@ -35,11 +35,11 @@ If you're just starting out, we recommend [installing code-server locally](../..
- 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:
```
https://github.com/codercom/code-server/releases/latest
https://github.com/cdr/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
wget https://github.com/cdr/code-server/releases/download/{version}/code-server-{version}-linux-x64.tar.gz
```
- Extract the downloaded tar.gz file with this command, for example:
```
@@ -66,4 +66,4 @@ If you're just starting out, we recommend [installing code-server locally](../..
> The `-p 80` flag is necessary in order to make the IDE accessible from the public IP of your instance (also available from the description in the instances page.
---
> 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/cdr/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

@@ -2,7 +2,7 @@
This tutorial shows you how to deploy `code-server` to a single node running on DigitalOcean.
If you're just starting out, we recommend [installing code-server locally](../../self-hosted/index.md). It takes only a few minutes and lets you try out all of the features. You can also try out the IDE on a container hosted [by Coder](http://coder.com/signup)
If you're just starting out, we recommend [installing code-server locally](../../self-hosted/index.md). It takes only a few minutes and lets you try out all of the features.
---
@@ -15,14 +15,14 @@ If you're just starting out, we recommend [installing code-server locally](../..
- Launch your instance
- 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
- Once in the SSH session, visit code-server [releases page](https://github.com/cdr/code-server/releases/) and copy the link to the download for the latest linux release
- Find the latest Linux release from this URL:
```
https://github.com/codercom/code-server/releases/latest
https://github.com/cdr/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
wget https://github.com/cdr/code-server/releases/download/{version}/code-server-{version}-linux-x64.tar.gz
```
- Extract the downloaded tar.gz file with this command, for example:
```
@@ -46,4 +46,4 @@ If you're just starting out, we recommend [installing code-server locally](../..
- 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/cdr/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

@@ -2,7 +2,7 @@
This tutorial shows you how to deploy `code-server` to a single node running on Google Cloud.
If you're just starting out, we recommend [installing code-server locally](../../self-hosted/index.md). It takes only a few minutes and lets you try out all of the features. You can also try out the IDE on a container hosted [by Coder](http://coder.com/signup)
If you're just starting out, we recommend [installing code-server locally](../../self-hosted/index.md). It takes only a few minutes and lets you try out all of the features.
---
@@ -14,7 +14,7 @@ If you're just starting out, we recommend [installing code-server locally](../..
- 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 its public IP address.
- Copy the link to download the latest Linux binary from our [releases page](https://github.com/codercom/code-server/releases)
- Copy the link to download the latest Linux binary from our [releases page](https://github.com/cdr/code-server/releases)
---
@@ -27,12 +27,12 @@ gcloud compute ssh --zone [region] [instance name]
- Find the latest Linux release from this URL:
```
https://github.com/codercom/code-server/releases/latest
https://github.com/cdr/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
wget https://github.com/cdr/code-server/releases/download/{version}/code-server-{version}-linux-x64.tar.gz
```
- Extract the downloaded tar.gz file with this command, for example:
@@ -50,7 +50,7 @@ cd code-server-{version}-linux-x64
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)
- Start the code-server
```
@@ -59,7 +59,7 @@ sudo ./code-server -p 80
> For instructions on how to keep the server running after you end your SSH session please checkout [how to use systemd](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) to start linux based services if they are killed
- 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
- 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">
@@ -68,4 +68,4 @@ sudo ./code-server -p 80
---
> 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/cdr/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

@@ -4,7 +4,7 @@
## 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).
> NOTE: If you get stuck or need help, [file an issue](https://github.com/cdr/code-server/issues/new?&title=Improve+self-hosted+quickstart+guide), [tweet (@coderhq)](https://twitter.com/coderhq) or [email](mailto:support@coder.com?subject=Self-hosted%20quickstart%20guide).
This document pertains to Coder specific implementations of VS Code. For documentation on how to use VS Code itself, please refer to the official [documentation for VS Code](https://code.visualstudio.com/docs)
@@ -17,7 +17,7 @@ It takes just a few minutes to get your own self-hosted server running. If you'v
-->
1. Visit [the releases](https://github.com/codercom/code-server/releases) page and download the latest cli for your operating system
1. Visit [the releases](https://github.com/cdr/code-server/releases) page and download the latest cli for your operating system
2. Double click the executable to run in the current directory
3. Copy the password that appears in the cli<img src="../assets/cli.png">
4. In your browser navigate to `localhost:8443`
@@ -56,7 +56,7 @@ Options:
```
### 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.
Use `code-server -d (path/to/directory)` or `code-server --user-data-dir=(path/to/directory)`, excluding the parentheses to specify the root folder that VS Code will start in.
### Host
By default, code-server will use `0.0.0.0` as its address. This can be changed by using `code-server -h` or `code-server --host=` followed by the address you want to use.

View File

@@ -1,6 +1,6 @@
{
"name": "@coder/code-server",
"repository": "https://github.com/codercom/code-server",
"repository": "https://github.com/cdr/code-server",
"author": "Coder",
"license": "MIT",
"description": "Run VS Code remotely.",

View File

@@ -1,5 +1,10 @@
// tslint:disable no-any
import { ITerminalService } from "vs/workbench/contrib/terminal/common/terminal";
import { IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
import { Action } from 'vs/base/common/actions';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
export interface EvalHelper { }
interface ActiveEvalEmitter {
removeAllListeners(event?: string): void;
@@ -144,11 +149,15 @@ declare namespace ide {
export const client: {};
export const workbench: {
readonly action: Action,
readonly syncActionDescriptor: SyncActionDescriptor,
readonly statusbarService: IStatusbarService;
readonly actionsRegistry: IWorkbenchActionRegistry;
readonly notificationService: INotificationService;
readonly storageService: IStorageService;
readonly menuRegistry: IMenuRegistry;
readonly commandRegistry: ICommandRegistry;
readonly terminalService: ITerminalService;
onFileCreate(cb: (path: string) => void): void;
onFileMove(cb: (path: string, target: string) => void): void;

View File

@@ -171,8 +171,10 @@ const newCreateElement = <K extends keyof HTMLElementTagNameMap>(tagName: K): HT
document.createElement = newCreateElement;
class Clipboard {
public has(): boolean {
return false;
private readonly buffers = new Map<string, Buffer>();
public has(format: string): boolean {
return this.buffers.has(format);
}
public readFindText(): string {
@@ -190,6 +192,14 @@ class Clipboard {
public readText(): Promise<string> {
return clipboard.readText();
}
public writeBuffer(format: string, buffer: Buffer): void {
this.buffers.set(format, buffer);
}
public readBuffer(format: string): Buffer | undefined {
return this.buffers.get(format);
}
}
class Shell {
@@ -368,14 +378,31 @@ class BrowserWindow extends EventEmitter {
public setFullScreen(fullscreen: boolean): void {
if (fullscreen) {
document.documentElement.requestFullscreen();
document.documentElement.requestFullscreen().catch((error) => {
logger.error(error.message);
});
} else {
document.exitFullscreen();
document.exitFullscreen().catch((error) => {
logger.error(error.message);
});
}
}
public isFullScreen(): boolean {
return document.fullscreenEnabled;
// TypeScript doesn't recognize this property.
// tslint:disable no-any
if (typeof (window as any)["fullScreen"] !== "undefined") {
return (window as any)["fullScreen"];
}
// tslint:enable no-any
try {
return window.matchMedia("(display-mode: fullscreen)").matches;
} catch (error) {
logger.error(error.message);
return false;
}
}
public isFocused(): boolean {

View File

@@ -0,0 +1,47 @@
# Protocol
This module provides a way for the browser to run Node modules like `fs`, `net`,
etc.
## Internals
### Server-side proxies
The server-side proxies are regular classes that call native Node functions. The
only thing special about them is that they must return promises and they must
return serializable values.
The only exception to the promise rule are event-related methods such as
`onEvent` and `onDone` (these are synchronous). The server will simply
immediately bind and push all events it can to the client. It doesn't wait for
the client to start listening. This prevents issues with the server not
receiving the client's request to start listening in time.
However, there is a way to specify events that should not bind immediately and
should wait for the client to request it, because some events (like `data` on a
stream) cannot be bound immediately (because doing so changes how the stream
behaves).
### Client-side proxies
Client-side proxies are `Proxy` instances. They simply make remote calls for any
method you call on it. The only exception is for events. Each client proxy has a
local emitter which it uses in place of a remote call (this allows the call to
be completed synchronously on the client). Then when an event is received from
the server, it gets emitted on that local emitter.
When an event is listened to, the proxy also notifies the server so it can start
listening in case it isn't already (see the `data` example above). This only
works for events that only fire after they are bound.
### Client-side fills
The client-side fills implement the Node API and make calls to the server-side
proxies using the client-side proxies.
When a proxy returns a proxy (for example `fs.createWriteStream`), that proxy is
a promise (since communicating with the server is asynchronous). We have to
return the fill from `fs.createWriteStream` synchronously, so that means the
fill has to contain a proxy promise. To eliminate the need for calling `then`
and to keep the code looking clean every time you use the proxy, the proxy is
itself wrapped in another proxy which just calls the method after a `then`. This
works since all the methods return promises (aside from the event methods, but
those are not used by the fills directly—they are only used internally to
forward events to the fill if it is an event emitter).

View File

@@ -4,7 +4,7 @@ import { promisify } from "util";
import { Emitter } from "@coder/events";
import { logger, field } from "@coder/logger";
import { ReadWriteConnection, InitData, SharedProcessData } from "../common/connection";
import { Module, ServerProxy } from "../common/proxy";
import { ClientServerProxy, Module, ServerProxy } from "../common/proxy";
import { argumentToProto, protoToArgument, moduleToProto, protoToModule, protoToOperatingSystem } from "../common/util";
import { Argument, Ping, ServerMessage, ClientMessage, Method, Event, Callback } from "../proto";
import { FsModule, ChildProcessModule, NetModule, NodePtyModule, SpdlogModule, TrashModule } from "./modules";
@@ -224,7 +224,11 @@ export class Client {
field("method", method),
]);
proxyMessage.setArgsList(args.map((a) => argumentToProto(a, storeCallback)));
proxyMessage.setArgsList(args.map((a) => argumentToProto<ClientServerProxy>(
a,
storeCallback,
(p) => p.proxyId,
)));
const clientMessage = new ClientMessage();
clientMessage.setMethod(message);
@@ -274,6 +278,8 @@ export class Client {
shell: init.getShell(),
extensionsDirectory: init.getExtensionsDirectory(),
builtInExtensionsDirectory: init.getBuiltinExtensionsDir(),
extraExtensionDirectories: init.getExtraExtensionDirectoriesList(),
extraBuiltinExtensionDirectories: init.getExtraBuiltinExtensionDirectoriesList(),
};
this.initDataEmitter.emit(this._initData);
break;
@@ -429,7 +435,7 @@ export class Client {
/**
* Return a proxy that makes remote calls.
*/
private createProxy<T>(proxyId: number | Module, promise: Promise<any> = Promise.resolve()): T {
private createProxy<T extends ClientServerProxy>(proxyId: number | Module, promise: Promise<any> = Promise.resolve()): T {
logger.trace(() => [
"creating proxy",
field("proxyId", proxyId),
@@ -449,7 +455,7 @@ export class Client {
cb(event.event, ...event.args);
});
},
}, {
} as ClientServerProxy, {
get: (target: any, name: string): any => {
// When resolving a promise with a proxy, it will check for "then".
if (name === "then") {

View File

@@ -2,13 +2,22 @@ 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";
import { ClientProxy, ClientServerProxy } from "../../common/proxy";
import { ChildProcessModuleProxy, ChildProcessProxy } from "../../node/modules/child_process";
import { ClientWritableProxy, ClientReadableProxy, Readable, Writable } from "./stream";
// tslint:disable completed-docs
export class ChildProcess extends ClientProxy<ChildProcessProxy> implements cp.ChildProcess {
export interface ClientChildProcessProxy extends ChildProcessProxy, ClientServerProxy<cp.ChildProcess> {}
export interface ClientChildProcessProxies {
childProcess: ClientChildProcessProxy;
stdin?: ClientWritableProxy | null;
stdout?: ClientReadableProxy | null;
stderr?: ClientReadableProxy | null;
}
export class ChildProcess extends ClientProxy<ClientChildProcessProxy> implements cp.ChildProcess {
public readonly stdin: stream.Writable;
public readonly stdout: stream.Readable;
public readonly stderr: stream.Readable;
@@ -18,7 +27,7 @@ export class ChildProcess extends ClientProxy<ChildProcessProxy> implements cp.C
private _killed: boolean = false;
private _pid = -1;
public constructor(proxyPromises: Promise<ChildProcessProxies>) {
public constructor(proxyPromises: Promise<ClientChildProcessProxies>) {
super(proxyPromises.then((p) => p.childProcess));
this.stdin = new Writable(proxyPromises.then((p) => p.stdin!));
this.stdout = new Readable(proxyPromises.then((p) => p.stdout!));
@@ -99,8 +108,14 @@ export class ChildProcess extends ClientProxy<ChildProcessProxy> implements cp.C
}
}
interface ClientChildProcessModuleProxy extends ChildProcessModuleProxy, ClientServerProxy {
exec(command: string, options?: { encoding?: string | null } & cp.ExecOptions | null, callback?: ((error: cp.ExecException | null, stdin: string | Buffer, stdout: string | Buffer) => void)): Promise<ClientChildProcessProxies>;
fork(modulePath: string, args?: string[], options?: cp.ForkOptions): Promise<ClientChildProcessProxies>;
spawn(command: string, args?: string[], options?: cp.SpawnOptions): Promise<ClientChildProcessProxies>;
}
export class ChildProcessModule {
public constructor(private readonly proxy: ChildProcessModuleProxy) {}
public constructor(private readonly proxy: ClientChildProcessModuleProxy) {}
public exec = (
command: string,

View File

@@ -1,12 +1,11 @@
import * as fs from "fs";
import { callbackify } from "util";
import { ClientProxy, Batch } from "../../common/proxy";
import { Batch, ClientProxy, ClientServerProxy } from "../../common/proxy";
import { IEncodingOptions, IEncodingOptionsCallback } from "../../common/util";
import { FsModuleProxy, Stats as IStats, WatcherProxy, WriteStreamProxy } from "../../node/modules/fs";
import { Writable } from "./stream";
import { FsModuleProxy, ReadStreamProxy, Stats as IStats, WatcherProxy, WriteStreamProxy } from "../../node/modules/fs";
import { Readable, Writable } from "./stream";
// tslint:disable no-any
// tslint:disable completed-docs
// tslint:disable completed-docs no-any
class StatBatch extends Batch<IStats, { path: fs.PathLike }> {
public constructor(private readonly proxy: FsModuleProxy) {
@@ -38,7 +37,9 @@ class ReaddirBatch extends Batch<Buffer[] | fs.Dirent[] | string[], { path: fs.P
}
}
class Watcher extends ClientProxy<WatcherProxy> implements fs.FSWatcher {
interface ClientWatcherProxy extends WatcherProxy, ClientServerProxy<fs.FSWatcher> {}
class Watcher extends ClientProxy<ClientWatcherProxy> implements fs.FSWatcher {
public close(): void {
this.catch(this.proxy.close());
}
@@ -48,7 +49,25 @@ class Watcher extends ClientProxy<WatcherProxy> implements fs.FSWatcher {
}
}
class WriteStream extends Writable<WriteStreamProxy> implements fs.WriteStream {
interface ClientReadStreamProxy extends ReadStreamProxy, ClientServerProxy<fs.ReadStream> {}
class ReadStream extends Readable<ClientReadStreamProxy> implements fs.ReadStream {
public get bytesRead(): number {
throw new Error("not implemented");
}
public get path(): string | Buffer {
throw new Error("not implemented");
}
public close(): void {
this.catch(this.proxy.close());
}
}
interface ClientWriteStreamProxy extends WriteStreamProxy, ClientServerProxy<fs.WriteStream> {}
class WriteStream extends Writable<ClientWriteStreamProxy> implements fs.WriteStream {
public get bytesWritten(): number {
throw new Error("not implemented");
}
@@ -62,12 +81,18 @@ class WriteStream extends Writable<WriteStreamProxy> implements fs.WriteStream {
}
}
interface ClientFsModuleProxy extends FsModuleProxy, ClientServerProxy {
createReadStream(path: fs.PathLike, options?: any): Promise<ClientReadStreamProxy>;
createWriteStream(path: fs.PathLike, options?: any): Promise<ClientWriteStreamProxy>;
watch(filename: fs.PathLike, options?: IEncodingOptions): Promise<ClientWatcherProxy>;
}
export class FsModule {
private readonly statBatch: StatBatch;
private readonly lstatBatch: LstatBatch;
private readonly readdirBatch: ReaddirBatch;
public constructor(private readonly proxy: FsModuleProxy) {
public constructor(private readonly proxy: ClientFsModuleProxy) {
this.statBatch = new StatBatch(this.proxy);
this.lstatBatch = new LstatBatch(this.proxy);
this.readdirBatch = new ReaddirBatch(this.proxy);
@@ -110,6 +135,10 @@ export class FsModule {
);
}
public createReadStream = (path: fs.PathLike, options?: any): fs.ReadStream => {
return new ReadStream(this.proxy.createReadStream(path, options));
}
public createWriteStream = (path: fs.PathLike, options?: any): fs.WriteStream => {
return new WriteStream(this.proxy.createWriteStream(path, options));
}

View File

@@ -1,16 +1,18 @@
import * as net from "net";
import { callbackify } from "util";
import { ClientProxy } from "../../common/proxy";
import { ClientProxy, ClientServerProxy } from "../../common/proxy";
import { NetModuleProxy, NetServerProxy, NetSocketProxy } from "../../node/modules/net";
import { Duplex } from "./stream";
// tslint:disable completed-docs
export class Socket extends Duplex<NetSocketProxy> implements net.Socket {
interface ClientNetSocketProxy extends NetSocketProxy, ClientServerProxy<net.Socket> {}
export class Socket extends Duplex<ClientNetSocketProxy> implements net.Socket {
private _connecting: boolean = false;
private _destroyed: boolean = false;
public constructor(proxyPromise: Promise<NetSocketProxy> | NetSocketProxy, connecting?: boolean) {
public constructor(proxyPromise: Promise<ClientNetSocketProxy> | ClientNetSocketProxy, connecting?: boolean) {
super(proxyPromise);
if (connecting) {
this._connecting = connecting;
@@ -126,12 +128,16 @@ export class Socket extends Duplex<NetSocketProxy> implements net.Socket {
}
}
export class Server extends ClientProxy<NetServerProxy> implements net.Server {
interface ClientNetServerProxy extends NetServerProxy, ClientServerProxy<net.Server> {
onConnection(cb: (proxy: ClientNetSocketProxy) => void): Promise<void>;
}
export class Server extends ClientProxy<ClientNetServerProxy> implements net.Server {
private socketId = 0;
private readonly sockets = new Map<number, net.Socket>();
private _listening: boolean = false;
public constructor(proxyPromise: Promise<NetServerProxy> | NetServerProxy) {
public constructor(proxyPromise: Promise<ClientNetServerProxy> | ClientNetServerProxy) {
super(proxyPromise);
this.catch(this.proxy.onConnection((socketProxy) => {
@@ -208,11 +214,17 @@ export class Server extends ClientProxy<NetServerProxy> implements net.Server {
type NodeNet = typeof net;
interface ClientNetModuleProxy extends NetModuleProxy, ClientServerProxy {
createSocket(options?: net.SocketConstructorOpts): Promise<ClientNetSocketProxy>;
createConnection(target: string | number | net.NetConnectOpts, host?: string): Promise<ClientNetSocketProxy>;
createServer(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean }): Promise<ClientNetServerProxy>;
}
export class NetModule implements NodeNet {
public readonly Socket: typeof net.Socket;
public readonly Server: typeof net.Server;
public constructor(private readonly proxy: NetModuleProxy) {
public constructor(private readonly proxy: ClientNetModuleProxy) {
// @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.

View File

@@ -1,15 +1,19 @@
import * as pty from "node-pty";
import { ClientProxy } from "../../common/proxy";
import { ClientProxy, ClientServerProxy } from "../../common/proxy";
import { NodePtyModuleProxy, NodePtyProcessProxy } from "../../node/modules/node-pty";
// tslint:disable completed-docs
export class NodePtyProcess extends ClientProxy<NodePtyProcessProxy> implements pty.IPty {
interface ClientNodePtyProcessProxy extends NodePtyProcessProxy, ClientServerProxy {}
export class NodePtyProcess extends ClientProxy<ClientNodePtyProcessProxy> implements pty.IPty {
private _pid = -1;
private _process = "";
private lastCols: number | undefined;
private lastRows: number | undefined;
public constructor(
private readonly moduleProxy: NodePtyModuleProxy,
private readonly moduleProxy: ClientNodePtyModuleProxy,
private readonly file: string,
private readonly args: string[] | string,
private readonly options: pty.IPtyForkOptions,
@@ -18,10 +22,12 @@ export class NodePtyProcess extends ClientProxy<NodePtyProcessProxy> implements
this.on("process", (process) => this._process = process);
}
protected initialize(proxyPromise: Promise<NodePtyProcessProxy>): void {
super.initialize(proxyPromise);
protected initialize(proxyPromise: Promise<ClientNodePtyProcessProxy>): ClientNodePtyProcessProxy {
const proxy = super.initialize(proxyPromise);
this.catch(this.proxy.getPid().then((p) => this._pid = p));
this.catch(this.proxy.getProcess().then((p) => this._process = p));
return proxy;
}
public get pid(): number {
@@ -33,6 +39,9 @@ export class NodePtyProcess extends ClientProxy<NodePtyProcessProxy> implements
}
public resize(columns: number, rows: number): void {
this.lastCols = columns;
this.lastRows = rows;
this.catch(this.proxy.resize(columns, rows));
}
@@ -47,14 +56,22 @@ export class NodePtyProcess extends ClientProxy<NodePtyProcessProxy> implements
protected handleDisconnect(): void {
this._process += " (disconnected)";
this.emit("data", "\r\n\nLost connection...\r\n\n");
this.initialize(this.moduleProxy.spawn(this.file, this.args, this.options));
this.initialize(this.moduleProxy.spawn(this.file, this.args, {
...this.options,
cols: this.lastCols || this.options.cols,
rows: this.lastRows || this.options.rows,
}));
}
}
type NodePty = typeof pty;
interface ClientNodePtyModuleProxy extends NodePtyModuleProxy, ClientServerProxy {
spawn(file: string, args: string[] | string, options: pty.IPtyForkOptions): Promise<ClientNodePtyProcessProxy>;
}
export class NodePtyModule implements NodePty {
public constructor(private readonly proxy: NodePtyModuleProxy) {}
public constructor(private readonly proxy: ClientNodePtyModuleProxy) {}
public spawn = (file: string, args: string[] | string, options: pty.IPtyForkOptions): pty.IPty => {
return new NodePtyProcess(this.proxy, file, args, options);

View File

@@ -1,12 +1,14 @@
import * as spdlog from "spdlog";
import { ClientProxy } from "../../common/proxy";
import { ClientProxy, ClientServerProxy } from "../../common/proxy";
import { RotatingLoggerProxy, SpdlogModuleProxy } from "../../node/modules/spdlog";
// tslint:disable completed-docs
class RotatingLogger extends ClientProxy<RotatingLoggerProxy> implements spdlog.RotatingLogger {
interface ClientRotatingLoggerProxy extends RotatingLoggerProxy, ClientServerProxy {}
class RotatingLogger extends ClientProxy<ClientRotatingLoggerProxy> implements spdlog.RotatingLogger {
public constructor(
private readonly moduleProxy: SpdlogModuleProxy,
private readonly moduleProxy: ClientSpdlogModuleProxy,
private readonly name: string,
private readonly filename: string,
private readonly filesize: number,
@@ -31,10 +33,14 @@ class RotatingLogger extends ClientProxy<RotatingLoggerProxy> implements spdlog.
}
}
interface ClientSpdlogModuleProxy extends SpdlogModuleProxy, ClientServerProxy {
createLogger(name: string, filePath: string, fileSize: number, fileCount: number): Promise<ClientRotatingLoggerProxy>;
}
export class SpdlogModule {
public readonly RotatingLogger: typeof spdlog.RotatingLogger;
public constructor(private readonly proxy: SpdlogModuleProxy) {
public constructor(private readonly proxy: ClientSpdlogModuleProxy) {
this.RotatingLogger = class extends RotatingLogger {
public constructor(name: string, filename: string, filesize: number, filecount: number) {
super(proxy, name, filename, filesize, filecount);

View File

@@ -1,11 +1,14 @@
import * as stream from "stream";
import { callbackify } from "util";
import { ClientProxy } from "../../common/proxy";
import { DuplexProxy, IReadableProxy, WritableProxy } from "../../node/modules/stream";
import { ClientProxy, ClientServerProxy } from "../../common/proxy";
import { isPromise } from "../../common/util";
import { DuplexProxy, ReadableProxy, WritableProxy } from "../../node/modules/stream";
// tslint:disable completed-docs
// tslint:disable completed-docs no-any
export class Writable<T extends WritableProxy = WritableProxy> extends ClientProxy<T> implements stream.Writable {
export interface ClientWritableProxy extends WritableProxy, ClientServerProxy<stream.Writable> {}
export class Writable<T extends ClientWritableProxy = ClientWritableProxy> extends ClientProxy<T> implements stream.Writable {
public get writable(): boolean {
throw new Error("not implemented");
}
@@ -50,7 +53,6 @@ export class Writable<T extends WritableProxy = WritableProxy> extends ClientPro
return this.catch(this.proxy.setDefaultEncoding(encoding));
}
// tslint:disable-next-line no-any
public write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
if (typeof encoding === "function") {
callback = encoding;
@@ -65,7 +67,6 @@ export class Writable<T extends WritableProxy = WritableProxy> extends ClientPro
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;
@@ -88,7 +89,9 @@ export class Writable<T extends WritableProxy = WritableProxy> extends ClientPro
}
}
export class Readable<T extends IReadableProxy = IReadableProxy> extends ClientProxy<T> implements stream.Readable {
export interface ClientReadableProxy extends ReadableProxy, ClientServerProxy<stream.Readable> {}
export class Readable<T extends ClientReadableProxy = ClientReadableProxy> extends ClientProxy<T> implements stream.Readable {
public get readable(): boolean {
throw new Error("not implemented");
}
@@ -141,11 +144,20 @@ export class Readable<T extends IReadableProxy = IReadableProxy> extends ClientP
throw new Error("not implemented");
}
public pipe<T>(): T {
throw new Error("not implemented");
public pipe<P extends NodeJS.WritableStream>(destination: P, options?: { end?: boolean }): P {
const writableProxy = (destination as any as Writable).proxyPromise;
if (!writableProxy) {
throw new Error("can only pipe stream proxies");
}
this.catch(
isPromise(writableProxy)
? writableProxy.then((p) => this.proxy.pipe(p, options))
: this.proxy.pipe(writableProxy, options),
);
return destination;
}
// tslint:disable-next-line no-any
public [Symbol.asyncIterator](): AsyncIterableIterator<any> {
throw new Error("not implemented");
}
@@ -164,7 +176,9 @@ export class Readable<T extends IReadableProxy = IReadableProxy> extends ClientP
}
}
export class Duplex<T extends DuplexProxy = DuplexProxy> extends Writable<T> implements stream.Duplex, stream.Readable {
export interface ClientDuplexProxy extends DuplexProxy, ClientServerProxy<stream.Duplex> {}
export class Duplex<T extends ClientDuplexProxy = ClientDuplexProxy> extends Writable<T> implements stream.Duplex, stream.Readable {
private readonly _readable: Readable;
public constructor(proxyPromise: Promise<T> | T) {
@@ -228,7 +242,6 @@ export class Duplex<T extends DuplexProxy = DuplexProxy> extends Writable<T> imp
this._readable.unshift();
}
// tslint:disable-next-line no-any
public [Symbol.asyncIterator](): AsyncIterableIterator<any> {
return this._readable[Symbol.asyncIterator]();
}

View File

@@ -1,10 +1,13 @@
import * as trash from "trash";
import { ClientServerProxy } from "../../common/proxy";
import { TrashModuleProxy } from "../../node/modules/trash";
// tslint:disable completed-docs
interface ClientTrashModuleProxy extends TrashModuleProxy, ClientServerProxy {}
export class TrashModule {
public constructor(private readonly proxy: TrashModuleProxy) {}
public constructor(private readonly proxy: ClientTrashModuleProxy) {}
public trash = (path: string, options?: trash.Options): Promise<void> => {
return this.proxy.trash(path, options);

View File

@@ -25,6 +25,8 @@ export interface InitData {
readonly shell: string;
readonly extensionsDirectory: string;
readonly builtInExtensionsDirectory: string;
readonly extraExtensionDirectories: string[];
readonly extraBuiltinExtensionDirectories: string[];
}
export interface SharedProcessData {

View File

@@ -1,13 +1,13 @@
import { EventEmitter } from "events";
import { isPromise } from "./util";
import { isPromise, EventCallback } from "./util";
// tslint:disable no-any
/**
* Allow using a proxy like it's returned synchronously. This only works because
* all proxy methods return promises.
* all proxy methods must return promises.
*/
const unpromisify = <T extends ServerProxy>(proxyPromise: Promise<T>): T => {
const unpromisify = <T extends ClientServerProxy>(proxyPromise: Promise<T>): T => {
return new Proxy({}, {
get: (target: any, name: string): any => {
if (typeof target[name] === "undefined") {
@@ -24,23 +24,23 @@ const unpromisify = <T extends ServerProxy>(proxyPromise: Promise<T>): T => {
};
/**
* 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.
* Client-side emitter that just forwards server proxy events to its own
* emitter. It also turns a promisified server proxy into a non-promisified
* proxy so we don't need a bunch of `then` calls everywhere.
*/
export abstract class ClientProxy<T extends ServerProxy> extends EventEmitter {
private _proxy: T | undefined;
export abstract class ClientProxy<T extends ClientServerProxy> extends EventEmitter {
private _proxy: T;
/**
* You can specify not to bind events in order to avoid emitting twice for
* duplex streams.
*/
public constructor(
proxyPromise: Promise<T> | T,
private _proxyPromise: Promise<T> | T,
private readonly bindEvents: boolean = true,
) {
super();
this.initialize(proxyPromise);
this._proxy = this.initialize(this._proxyPromise);
if (this.bindEvents) {
this.on("disconnected", (error) => {
try {
@@ -64,11 +64,34 @@ export abstract class ClientProxy<T extends ServerProxy> extends EventEmitter {
return this;
}
protected get proxy(): T {
if (!this._proxy) {
throw new Error("not initialized");
}
/**
* Bind the event locally and ensure the event is bound on the server.
*/
public addListener(event: string, listener: (...args: any[]) => void): this {
this.catch(this.proxy.bindDelayedEvent(event));
return super.on(event, listener);
}
/**
* Alias for `addListener`.
*/
public on(event: string, listener: (...args: any[]) => void): this {
return this.addListener(event, listener);
}
/**
* Original promise for the server proxy. Can be used to be passed as an
* argument.
*/
public get proxyPromise(): Promise<T> | T {
return this._proxyPromise;
}
/**
* Server proxy.
*/
protected get proxy(): T {
return this._proxy;
}
@@ -76,13 +99,18 @@ export abstract class ClientProxy<T extends ServerProxy> extends EventEmitter {
* Initialize the proxy by unpromisifying if necessary and binding to its
* events.
*/
protected initialize(proxyPromise: Promise<T> | T): void {
this._proxy = isPromise(proxyPromise) ? unpromisify(proxyPromise) : proxyPromise;
protected initialize(proxyPromise: Promise<T> | T): T {
this._proxyPromise = proxyPromise;
this._proxy = isPromise(this._proxyPromise)
? unpromisify(this._proxyPromise)
: this._proxyPromise;
if (this.bindEvents) {
this.catch(this.proxy.onEvent((event, ...args): void => {
this.proxy.onEvent((event, ...args): void => {
this.emit(event, ...args);
}));
});
}
return this._proxy;
}
/**
@@ -102,34 +130,107 @@ export abstract class ClientProxy<T extends ServerProxy> extends EventEmitter {
}
}
export interface ServerProxyOptions<T> {
/**
* The events to bind immediately.
*/
bindEvents: string[];
/**
* Events that signal the proxy is done.
*/
doneEvents: string[];
/**
* Events that should only be bound when asked
*/
delayedEvents?: string[];
/**
* Whatever is emitting events (stream, child process, etc).
*/
instance: T;
}
/**
* 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),
* The actual proxy 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.
*
* Events listeners are added client-side (since all events automatically
* forward to the client), so onDone and onEvent do not need to be asynchronous.
*/
export interface ServerProxy {
export abstract class ServerProxy<T extends EventEmitter = EventEmitter> {
public readonly instance: T;
private readonly callbacks = <EventCallback[]>[];
public constructor(private readonly options: ServerProxyOptions<T>) {
this.instance = options.instance;
}
/**
* Dispose the proxy.
*/
dispose(): Promise<void>;
public async dispose(): Promise<void> {
this.instance.removeAllListeners();
}
/**
* 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>;
public onDone(cb: () => void): void {
this.options.doneEvents.forEach((event) => {
this.instance.on(event, cb);
});
}
/**
* Bind an event that will not fire without first binding it and shouldn't be
* bound immediately.
* For example, binding to `data` switches a stream to flowing mode, so we
* don't want to do it until we're asked. Otherwise something like `pipe`
* won't work because potentially some or all of the data will already have
* been flushed out.
*/
public async bindDelayedEvent(event: string): Promise<void> {
if (this.options.delayedEvents
&& this.options.delayedEvents.includes(event)
&& !this.options.bindEvents.includes(event)) {
this.options.bindEvents.push(event);
this.callbacks.forEach((cb) => {
this.instance.on(event, (...args: any[]) => cb(event, ...args));
});
}
}
/**
* 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.
* 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.
*
* This cannot be async because then we can bind to the events too late.
*/
// tslint:disable-next-line no-any
onEvent(cb: (event: string, ...args: any[]) => void): Promise<void>;
public onEvent(cb: EventCallback): void {
this.callbacks.push(cb);
this.options.bindEvents.forEach((event) => {
this.instance.on(event, (...args: any[]) => cb(event, ...args));
});
}
}
/**
* A server-side proxy stored on the client. The proxy ID only exists on the
* client-side version of the server proxy. The event listeners are handled by
* the client and the remaining methods are proxied to the server.
*/
export interface ClientServerProxy<T extends EventEmitter = EventEmitter> extends ServerProxy<T> {
proxyId: number | Module;
}
/**

View File

@@ -1,6 +1,6 @@
import { Argument, Module as ProtoModule, WorkingInit } from "../proto";
import { OperatingSystem } from "../common/connection";
import { Module, ServerProxy } from "./proxy";
import { ClientServerProxy, Module, ServerProxy } from "./proxy";
// tslint:disable no-any
@@ -19,6 +19,8 @@ export const escapePath = (path: string): string => {
return `'${path.replace(/'/g, "'\\''")}'`;
};
export type EventCallback = (event: string, ...args: any[]) => void;
export type IEncodingOptions = {
encoding?: BufferEncoding | null;
flag?: string;
@@ -34,15 +36,26 @@ export type IEncodingOptionsCallback = IEncodingOptions | ((err: NodeJS.ErrnoExc
* If sending a function is possible, provide `storeFunction`.
* If sending a proxy is possible, provide `storeProxy`.
*/
export const argumentToProto = (
export const argumentToProto = <P = ClientServerProxy | ServerProxy>(
value: any,
storeFunction?: (fn: () => void) => number,
storeProxy?: (proxy: ServerProxy) => number,
storeProxy?: (proxy: P) => number | Module,
): Argument => {
const convert = (currentValue: any): Argument => {
const message = new Argument();
if (currentValue instanceof Error
if (isProxy<P>(currentValue)) {
if (!storeProxy) {
throw new Error("no way to serialize proxy");
}
const arg = new Argument.ProxyValue();
const id = storeProxy(currentValue);
if (typeof id === "string") {
throw new Error("unable to serialize module proxy");
}
arg.setId(id);
message.setProxy(arg);
} else if (currentValue instanceof Error
|| (currentValue && typeof currentValue.message !== "undefined"
&& typeof currentValue.stack !== "undefined")) {
const arg = new Argument.ErrorValue();
@@ -58,13 +71,6 @@ export const argumentToProto = (
const arg = new Argument.ArrayValue();
arg.setDataList(currentValue.map(convert));
message.setArray(arg);
} else if (isProxy(currentValue)) {
if (!storeProxy) {
throw new Error("no way to serialize proxy");
}
const arg = new Argument.ProxyValue();
arg.setId(storeProxy(currentValue));
message.setProxy(arg);
} else if (currentValue instanceof Date
|| (currentValue && typeof currentValue.getTime === "function")) {
const arg = new Argument.DateValue();
@@ -218,7 +224,7 @@ export const platformToProto = (platform: NodeJS.Platform): WorkingInit.Operatin
}
};
export const isProxy = (value: any): value is ServerProxy => {
export const isProxy = <P = ClientServerProxy | ServerProxy>(value: any): value is P => {
return value && typeof value === "object" && typeof value.onEvent === "function";
};
@@ -230,8 +236,11 @@ export const isPromise = (value: any): value is Promise<any> => {
* 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 };
}
export const withEnv = <T extends { env?: NodeJS.ProcessEnv }>(options?: T): T | undefined => {
return options && options.env ? {
...options,
env: {
...process.env, ...options.env,
},
} : options;
};

View File

@@ -1,35 +1,41 @@
import * as cp from "child_process";
import { ServerProxy } from "../../common/proxy";
import { preserveEnv } from "../../common/util";
import { withEnv } from "../../common/util";
import { WritableProxy, ReadableProxy } from "./stream";
// tslint:disable completed-docs
export type ForkProvider = (modulePath: string, args?: string[], options?: cp.ForkOptions) => cp.ChildProcess;
export class ChildProcessProxy implements ServerProxy {
public constructor(private readonly process: cp.ChildProcess) {}
export class ChildProcessProxy extends ServerProxy<cp.ChildProcess> {
public constructor(instance: cp.ChildProcess) {
super({
bindEvents: ["close", "disconnect", "error", "exit", "message"],
doneEvents: ["close"],
instance,
});
}
public async kill(signal?: string): Promise<void> {
this.process.kill(signal);
this.instance.kill(signal);
}
public async disconnect(): Promise<void> {
this.process.disconnect();
this.instance.disconnect();
}
public async ref(): Promise<void> {
this.process.ref();
this.instance.ref();
}
public async unref(): Promise<void> {
this.process.unref();
this.instance.unref();
}
// tslint:disable-next-line no-any
public async send(message: any): Promise<void> {
return new Promise((resolve, reject): void => {
this.process.send(message, (error) => {
this.instance.send(message, (error) => {
if (error) {
reject(error);
} else {
@@ -40,25 +46,13 @@ export class ChildProcessProxy implements ServerProxy {
}
public async getPid(): Promise<number> {
return this.process.pid;
}
public async onDone(cb: () => void): Promise<void> {
this.process.on("close", cb);
return this.instance.pid;
}
public async dispose(): Promise<void> {
this.process.kill();
setTimeout(() => this.process.kill("SIGKILL"), 5000); // Double tap.
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
this.process.on("close", (code, signal) => cb("close", code, signal));
this.process.on("disconnect", () => cb("disconnect"));
this.process.on("error", (error) => cb("error", error));
this.process.on("exit", (exitCode, signal) => cb("exit", exitCode, signal));
this.process.on("message", (message) => cb("message", message));
this.instance.kill();
setTimeout(() => this.instance.kill("SIGKILL"), 5000); // Double tap.
await super.dispose();
}
}
@@ -77,29 +71,25 @@ export class ChildProcessModuleProxy {
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));
return this.returnProxies(cp.exec(command, options && withEnv(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));
return this.returnProxies((this.forkProvider || cp.fork)(modulePath, args, withEnv(options)));
}
public async spawn(command: string, args?: string[], options?: cp.SpawnOptions): Promise<ChildProcessProxies> {
preserveEnv(options);
return this.returnProxies(cp.spawn(command, args, options));
return this.returnProxies(cp.spawn(command, args, withEnv(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),
// Child processes streams appear to immediately flow so we need to bind
// to the data event right away.
stdout: process.stdout && new ReadableProxy(process.stdout, ["data"]),
stderr: process.stderr && new ReadableProxy(process.stderr, ["data"]),
};
}
}

View File

@@ -2,9 +2,9 @@ import * as fs from "fs";
import { promisify } from "util";
import { ServerProxy } from "../../common/proxy";
import { IEncodingOptions } from "../../common/util";
import { WritableProxy } from "./stream";
import { ReadableProxy, WritableProxy } from "./stream";
// tslint:disable completed-docs
// tslint:disable completed-docs no-any
/**
* A serializable version of fs.Stats.
@@ -37,45 +37,52 @@ export interface Stats {
_isSocket: boolean;
}
export class WriteStreamProxy extends WritableProxy<fs.WriteStream> {
export class ReadStreamProxy extends ReadableProxy<fs.ReadStream> {
public constructor(stream: fs.ReadStream) {
super(stream, ["open"]);
}
public async close(): Promise<void> {
this.stream.close();
this.instance.close();
}
public async dispose(): Promise<void> {
this.instance.close();
await super.dispose();
this.stream.close();
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
await super.onEvent(cb);
this.stream.on("open", (fd) => cb("open", fd));
}
}
export class WatcherProxy implements ServerProxy {
public constructor(private readonly watcher: fs.FSWatcher) {}
export class WriteStreamProxy extends WritableProxy<fs.WriteStream> {
public constructor(stream: fs.WriteStream) {
super(stream, ["open"]);
}
public async close(): Promise<void> {
this.watcher.close();
this.instance.close();
}
public async dispose(): Promise<void> {
this.watcher.close();
this.watcher.removeAllListeners();
this.instance.close();
await super.dispose();
}
}
export class WatcherProxy extends ServerProxy<fs.FSWatcher> {
public constructor(watcher: fs.FSWatcher) {
super({
bindEvents: ["change", "close", "error"],
doneEvents: ["close", "error"],
instance: watcher,
});
}
public async onDone(cb: () => void): Promise<void> {
this.watcher.on("close", cb);
this.watcher.on("error", cb);
public async close(): Promise<void> {
this.instance.close();
}
// 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));
public async dispose(): Promise<void> {
this.instance.close();
await super.dispose();
}
}
@@ -84,7 +91,6 @@ export class FsModuleProxy {
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);
}
@@ -105,7 +111,10 @@ export class FsModuleProxy {
return promisify(fs.copyFile)(src, dest, flags);
}
// tslint:disable-next-line no-any
public async createReadStream(path: fs.PathLike, options?: any): Promise<ReadStreamProxy> {
return new ReadStreamProxy(fs.createReadStream(path, options));
}
public async createWriteStream(path: fs.PathLike, options?: any): Promise<WriteStreamProxy> {
return new WriteStreamProxy(fs.createWriteStream(path, options));
}
@@ -236,7 +245,6 @@ export class FsModuleProxy {
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);
}

View File

@@ -2,78 +2,65 @@ import * as net from "net";
import { ServerProxy } from "../../common/proxy";
import { DuplexProxy } from "./stream";
// tslint:disable completed-docs
// tslint:disable completed-docs no-any
export class NetSocketProxy extends DuplexProxy<net.Socket> {
public constructor(socket: net.Socket) {
super(socket, ["connect", "lookup", "timeout"]);
}
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
this.instance.connect(options as any, host as any);
}
public async unref(): Promise<void> {
this.stream.unref();
this.instance.unref();
}
public async ref(): Promise<void> {
this.stream.ref();
this.instance.ref();
}
public async dispose(): Promise<void> {
this.stream.removeAllListeners();
this.stream.end();
this.stream.destroy();
this.stream.unref();
}
public async onDone(cb: () => void): Promise<void> {
this.stream.on("close", cb);
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
await super.onEvent(cb);
this.stream.on("connect", () => cb("connect"));
this.stream.on("lookup", (error, address, family, host) => cb("lookup", error, address, family, host));
this.stream.on("timeout", () => cb("timeout"));
this.instance.end();
this.instance.destroy();
this.instance.unref();
await super.dispose();
}
}
export class NetServerProxy implements ServerProxy {
public constructor(private readonly server: net.Server) {}
export class NetServerProxy extends ServerProxy<net.Server> {
public constructor(instance: net.Server) {
super({
bindEvents: ["close", "error", "listening"],
doneEvents: ["close"],
instance,
});
}
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
this.instance.listen(handle, hostname as any, backlog as any);
}
public async ref(): Promise<void> {
this.server.ref();
this.instance.ref();
}
public async unref(): Promise<void> {
this.server.unref();
this.instance.unref();
}
public async close(): Promise<void> {
this.server.close();
this.instance.close();
}
public async onConnection(cb: (proxy: NetSocketProxy) => void): Promise<void> {
this.server.on("connection", (socket) => cb(new NetSocketProxy(socket)));
this.instance.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"));
this.instance.close();
this.instance.removeAllListeners();
}
}
@@ -83,7 +70,7 @@ export class NetModuleProxy {
}
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
return new NetSocketProxy(net.createConnection(target as any, host));
}
public async createServer(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean }): Promise<NetServerProxy> {

View File

@@ -2,25 +2,32 @@
import { EventEmitter } from "events";
import * as pty from "node-pty";
import { ServerProxy } from "../../common/proxy";
import { preserveEnv } from "../../common/util";
import { withEnv } from "../../common/util";
// tslint:disable completed-docs
/**
* Server-side IPty proxy.
*/
export class NodePtyProcessProxy implements ServerProxy {
private readonly emitter = new EventEmitter();
export class NodePtyProcessProxy extends ServerProxy {
public constructor(private readonly process: pty.IPty) {
super({
bindEvents: ["process", "data", "exit"],
doneEvents: ["exit"],
instance: new EventEmitter(),
});
this.process.on("data", (data) => this.instance.emit("data", data));
this.process.on("exit", (exitCode, signal) => this.instance.emit("exit", exitCode, signal));
let name = process.process;
setTimeout(() => { // Need to wait for the caller to listen to the event.
this.emitter.emit("process", name);
this.instance.emit("process", name);
}, 1);
const timer = setInterval(() => {
if (process.process !== name) {
name = process.process;
this.emitter.emit("process", name);
this.instance.emit("process", name);
}
}, 200);
@@ -47,21 +54,10 @@ export class NodePtyProcessProxy implements ServerProxy {
this.process.write(data);
}
public async onDone(cb: () => void): Promise<void> {
this.process.on("exit", cb);
}
public async dispose(): Promise<void> {
this.process.kill();
setTimeout(() => this.process.kill("SIGKILL"), 5000); // Double tap.
this.emitter.removeAllListeners();
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
this.emitter.on("process", (process) => cb("process", process));
this.process.on("data", (data) => cb("data", data));
this.process.on("exit", (exitCode, signal) => cb("exit", exitCode, signal));
await super.dispose();
}
}
@@ -70,8 +66,6 @@ export class NodePtyProcessProxy implements ServerProxy {
*/
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));
return new NodePtyProcessProxy(require("node-pty").spawn(file, args, withEnv(options)));
}
}

View File

@@ -5,10 +5,14 @@ import { ServerProxy } from "../../common/proxy";
// tslint:disable completed-docs
export class RotatingLoggerProxy implements ServerProxy {
private readonly emitter = new EventEmitter();
public constructor(private readonly logger: spdlog.RotatingLogger) {}
export class RotatingLoggerProxy extends ServerProxy<EventEmitter> {
public constructor(private readonly logger: spdlog.RotatingLogger) {
super({
bindEvents: [],
doneEvents: ["dispose"],
instance: new EventEmitter(),
});
}
public async trace (message: string): Promise<void> { this.logger.trace(message); }
public async debug (message: string): Promise<void> { this.logger.debug(message); }
@@ -21,19 +25,10 @@ export class RotatingLoggerProxy implements ServerProxy {
public async flush (): Promise<void> { this.logger.flush(); }
public async drop (): Promise<void> { this.logger.drop(); }
public async onDone(cb: () => void): Promise<void> {
this.emitter.on("dispose", cb);
}
public async dispose(): Promise<void> {
await this.flush();
this.emitter.emit("dispose");
this.emitter.removeAllListeners();
}
// tslint:disable-next-line no-any
public async onEvent(_cb: (event: string, ...args: any[]) => void): Promise<void> {
// No events.
this.instance.emit("dispose");
await super.dispose();
}
}

View File

@@ -1,32 +1,38 @@
import { EventEmitter } from "events";
import * as stream from "stream";
import { ServerProxy } from "../../common/proxy";
// tslint:disable completed-docs
// tslint:disable completed-docs no-any
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();
export class WritableProxy<T extends stream.Writable = stream.Writable> extends ServerProxy<T> {
public constructor(instance: T, bindEvents: string[] = [], delayedEvents?: string[]) {
super({
bindEvents: ["close", "drain", "error", "finish"].concat(bindEvents),
doneEvents: ["close"],
delayedEvents,
instance,
});
}
public async destroy(): Promise<void> {
this.instance.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, () => {
this.instance.end(data, encoding, () => {
resolve();
});
});
}
public async setDefaultEncoding(encoding: string): Promise<void> {
this.stream.setDefaultEncoding(encoding);
this.instance.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) => {
this.instance.write(data, encoding, (error) => {
if (error) {
reject(error);
} else {
@@ -37,22 +43,8 @@ export class WritableProxy<T extends stream.Writable = stream.Writable> implemen
}
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.instance.end();
await super.dispose();
}
}
@@ -60,50 +52,58 @@ export class WritableProxy<T extends stream.Writable = stream.Writable> implemen
* 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>;
export interface IReadableProxy<T extends EventEmitter> extends ServerProxy<T> {
pipe<P extends WritableProxy>(destination: P, options?: { end?: boolean; }): 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) {}
export class ReadableProxy<T extends stream.Readable = stream.Readable> extends ServerProxy<T> implements IReadableProxy<T> {
public constructor(instance: T, bindEvents: string[] = []) {
super({
bindEvents: ["close", "end", "error"].concat(bindEvents),
doneEvents: ["close"],
delayedEvents: ["data"],
instance,
});
}
public async pipe<P extends WritableProxy>(destination: P, options?: { end?: boolean; }): Promise<void> {
this.instance.pipe(destination.instance, options);
// `pipe` switches the stream to flowing mode and makes data start emitting.
await this.bindDelayedEvent("data");
}
public async destroy(): Promise<void> {
this.stream.destroy();
this.instance.destroy();
}
public async setEncoding(encoding: string): Promise<void> {
this.stream.setEncoding(encoding);
this.instance.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));
this.instance.destroy();
await super.dispose();
}
}
export class DuplexProxy<T extends stream.Duplex = stream.Duplex> extends WritableProxy<T> implements IReadableProxy {
export class DuplexProxy<T extends stream.Duplex = stream.Duplex> extends WritableProxy<T> implements IReadableProxy<T> {
public constructor(stream: T, bindEvents: string[] = []) {
super(stream, ["end"].concat(bindEvents), ["data"]);
}
public async pipe<P extends WritableProxy>(destination: P, options?: { end?: boolean; }): Promise<void> {
this.instance.pipe(destination.instance, options);
// `pipe` switches the stream to flowing mode and makes data start emitting.
await this.bindDelayedEvent("data");
}
public async setEncoding(encoding: string): Promise<void> {
this.stream.setEncoding(encoding);
this.instance.setEncoding(encoding);
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
await super.onEvent(cb);
this.stream.on("data", (chunk) => cb("data", chunk));
this.stream.on("end", () => cb("end"));
public async dispose(): Promise<void> {
this.instance.destroy();
await super.dispose();
}
}

View File

@@ -15,6 +15,8 @@ export interface ServerOptions {
readonly cacheDirectory: string;
readonly builtInExtensionsDirectory: string;
readonly extensionsDirectory: string;
readonly extraExtensionDirectories?: string[];
readonly extraBuiltinExtensionDirectories?: string[];
readonly fork?: ForkProvider;
}
@@ -99,6 +101,8 @@ export class Server {
initMsg.setTmpDirectory(os.tmpdir());
initMsg.setOperatingSystem(platformToProto(os.platform()));
initMsg.setShell(os.userInfo().shell || global.process.env.SHELL || "");
initMsg.setExtraExtensionDirectoriesList(this.options.extraExtensionDirectories || []);
initMsg.setExtraBuiltinExtensionDirectoriesList(this.options.extraBuiltinExtensionDirectories || []);
const srvMsg = new ServerMessage();
srvMsg.setInit(initMsg);
connection.send(srvMsg.serializeBinary());
@@ -136,6 +140,7 @@ export class Server {
const args = proxyMessage.getArgsList().map((a) => protoToArgument(
a,
(id, args) => this.sendCallback(proxyId, id, args),
(id) => this.getProxy(id).instance,
));
logger.trace(() => [
@@ -241,9 +246,7 @@ export class Server {
this.proxies.set(proxyId, { instance });
if (isProxy(instance)) {
instance.onEvent((event, ...args) => this.sendEvent(proxyId, event, ...args)).catch((error) => {
logger.error(error.message);
});
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) {
@@ -255,8 +258,6 @@ export class Server {
this.removeProxy(proxyId);
}, this.responseTimeout);
}
}).catch((error) => {
logger.error(error.message);
});
}

View File

@@ -42,4 +42,6 @@ message WorkingInit {
string shell = 6;
string builtin_extensions_dir = 7;
string extensions_directory = 8;
repeated string extra_extension_directories = 9;
repeated string extra_builtin_extension_directories = 10;
}

View File

@@ -135,6 +135,16 @@ export class WorkingInit extends jspb.Message {
getExtensionsDirectory(): string;
setExtensionsDirectory(value: string): void;
clearExtraExtensionDirectoriesList(): void;
getExtraExtensionDirectoriesList(): Array<string>;
setExtraExtensionDirectoriesList(value: Array<string>): void;
addExtraExtensionDirectories(value: string, index?: number): string;
clearExtraBuiltinExtensionDirectoriesList(): void;
getExtraBuiltinExtensionDirectoriesList(): Array<string>;
setExtraBuiltinExtensionDirectoriesList(value: Array<string>): void;
addExtraBuiltinExtensionDirectories(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): WorkingInit.AsObject;
static toObject(includeInstance: boolean, msg: WorkingInit): WorkingInit.AsObject;
@@ -155,6 +165,8 @@ export namespace WorkingInit {
shell: string,
builtinExtensionsDir: string,
extensionsDirectory: string,
extraExtensionDirectoriesList: Array<string>,
extraBuiltinExtensionDirectoriesList: Array<string>,
}
export enum OperatingSystem {

View File

@@ -72,7 +72,7 @@ if (goog.DEBUG && !COMPILED) {
* @constructor
*/
proto.WorkingInit = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
jspb.Message.initialize(this, opt_data, 0, -1, proto.WorkingInit.repeatedFields_, null);
};
goog.inherits(proto.WorkingInit, jspb.Message);
if (goog.DEBUG && !COMPILED) {
@@ -759,6 +759,13 @@ proto.ServerMessage.prototype.hasSharedProcessActive = function() {
/**
* List of repeated fields within this message type.
* @private {!Array<number>}
* @const
*/
proto.WorkingInit.repeatedFields_ = [9,10];
if (jspb.Message.GENERATE_TO_OBJECT) {
@@ -795,7 +802,9 @@ proto.WorkingInit.toObject = function(includeInstance, msg) {
operatingSystem: jspb.Message.getFieldWithDefault(msg, 5, 0),
shell: jspb.Message.getFieldWithDefault(msg, 6, ""),
builtinExtensionsDir: jspb.Message.getFieldWithDefault(msg, 7, ""),
extensionsDirectory: jspb.Message.getFieldWithDefault(msg, 8, "")
extensionsDirectory: jspb.Message.getFieldWithDefault(msg, 8, ""),
extraExtensionDirectoriesList: jspb.Message.getRepeatedField(msg, 9),
extraBuiltinExtensionDirectoriesList: jspb.Message.getRepeatedField(msg, 10)
};
if (includeInstance) {
@@ -864,6 +873,14 @@ proto.WorkingInit.deserializeBinaryFromReader = function(msg, reader) {
var value = /** @type {string} */ (reader.readString());
msg.setExtensionsDirectory(value);
break;
case 9:
var value = /** @type {string} */ (reader.readString());
msg.addExtraExtensionDirectories(value);
break;
case 10:
var value = /** @type {string} */ (reader.readString());
msg.addExtraBuiltinExtensionDirectories(value);
break;
default:
reader.skipField();
break;
@@ -949,6 +966,20 @@ proto.WorkingInit.serializeBinaryToWriter = function(message, writer) {
f
);
}
f = message.getExtraExtensionDirectoriesList();
if (f.length > 0) {
writer.writeRepeatedString(
9,
f
);
}
f = message.getExtraBuiltinExtensionDirectoriesList();
if (f.length > 0) {
writer.writeRepeatedString(
10,
f
);
}
};
@@ -1081,4 +1112,68 @@ proto.WorkingInit.prototype.setExtensionsDirectory = function(value) {
};
/**
* repeated string extra_extension_directories = 9;
* @return {!Array<string>}
*/
proto.WorkingInit.prototype.getExtraExtensionDirectoriesList = function() {
return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 9));
};
/** @param {!Array<string>} value */
proto.WorkingInit.prototype.setExtraExtensionDirectoriesList = function(value) {
jspb.Message.setField(this, 9, value || []);
};
/**
* @param {string} value
* @param {number=} opt_index
*/
proto.WorkingInit.prototype.addExtraExtensionDirectories = function(value, opt_index) {
jspb.Message.addToRepeatedField(this, 9, value, opt_index);
};
/**
* Clears the list making it empty but non-null.
*/
proto.WorkingInit.prototype.clearExtraExtensionDirectoriesList = function() {
this.setExtraExtensionDirectoriesList([]);
};
/**
* repeated string extra_builtin_extension_directories = 10;
* @return {!Array<string>}
*/
proto.WorkingInit.prototype.getExtraBuiltinExtensionDirectoriesList = function() {
return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 10));
};
/** @param {!Array<string>} value */
proto.WorkingInit.prototype.setExtraBuiltinExtensionDirectoriesList = function(value) {
jspb.Message.setField(this, 10, value || []);
};
/**
* @param {string} value
* @param {number=} opt_index
*/
proto.WorkingInit.prototype.addExtraBuiltinExtensionDirectories = function(value, opt_index) {
jspb.Message.addToRepeatedField(this, 10, value, opt_index);
};
/**
* Clears the list making it empty but non-null.
*/
proto.WorkingInit.prototype.clearExtraBuiltinExtensionDirectoriesList = function() {
this.setExtraBuiltinExtensionDirectoriesList([]);
};
goog.object.extend(exports, proto);

View File

@@ -10,7 +10,7 @@ describe("child_process", () => {
const cp = client.modules[Module.ChildProcess];
const getStdout = async (proc: ChildProcess): Promise<string> => {
return new Promise((r): Readable => proc.stdout!.on("data", r))
return new Promise((r): Readable => proc.stdout!.once("data", r))
.then((s) => s.toString());
};

View File

@@ -70,12 +70,12 @@ describe("fs", () => {
describe("chown", () => {
it("should chown existing file", async () => {
const file = await helper.createTmpFile();
await expect(util.promisify(fs.chown)(file, 1, 1))
await expect(util.promisify(nativeFs.chown)(file, 1000, 1000))
.resolves.toBeUndefined();
});
it("should fail to chown nonexistent file", async () => {
await expect(util.promisify(fs.chown)(helper.tmpFile(), 1, 1))
await expect(util.promisify(fs.chown)(helper.tmpFile(), 1000, 1000))
.rejects.toThrow("ENOENT");
});
});
@@ -131,6 +131,42 @@ describe("fs", () => {
});
});
describe("createReadStream", () => {
it("should read a file", async () => {
const file = helper.tmpFile();
const content = "foobar";
await util.promisify(nativeFs.writeFile)(file, content);
const reader = fs.createReadStream(file);
await expect(new Promise((resolve, reject): void => {
let data = "";
reader.once("error", reject);
reader.once("end", () => resolve(data));
reader.on("data", (d) => data += d.toString());
})).resolves.toBe(content);
});
it("should pipe to a writable stream", async () => {
const source = helper.tmpFile();
const content = "foo";
await util.promisify(nativeFs.writeFile)(source, content);
const destination = helper.tmpFile();
const reader = fs.createReadStream(source);
const writer = fs.createWriteStream(destination);
await new Promise((resolve, reject): void => {
reader.once("error", reject);
writer.once("error", reject);
writer.once("close", resolve);
reader.pipe(writer);
});
await expect(util.promisify(nativeFs.readFile)(destination, "utf8")).resolves.toBe(content);
});
});
describe("exists", () => {
it("should output file exists", async () => {
await expect(util.promisify(fs.exists)(__filename))
@@ -162,13 +198,13 @@ describe("fs", () => {
it("should fchown existing file", async () => {
const file = await helper.createTmpFile();
const fd = await util.promisify(nativeFs.open)(file, "r");
await expect(util.promisify(fs.fchown)(fd, 1, 1))
await expect(util.promisify(fs.fchown)(fd, 1000, 1000))
.resolves.toBeUndefined();
await util.promisify(nativeFs.close)(fd);
});
it("should fail to fchown nonexistent file", async () => {
await expect(util.promisify(fs.fchown)(99999, 1, 1))
await expect(util.promisify(fs.fchown)(99999, 1000, 1000))
.rejects.toThrow("EBADF");
});
});
@@ -239,7 +275,7 @@ describe("fs", () => {
it("should futimes existing file", async () => {
const file = await helper.createTmpFile();
const fd = await util.promisify(nativeFs.open)(file, "w");
await expect(util.promisify(fs.futimes)(fd, 1, 1))
await expect(util.promisify(fs.futimes)(fd, 1000, 1000))
.resolves.toBeUndefined();
await util.promisify(nativeFs.close)(fd);
});
@@ -275,14 +311,13 @@ describe("fs", () => {
describe("lchown", () => {
it("should lchown existing file", async () => {
const file = await helper.createTmpFile();
await expect(util.promisify(fs.lchown)(file, 1, 1))
await expect(util.promisify(fs.lchown)(file, 1000, 1000))
.resolves.toBeUndefined();
});
// TODO: Doesn't fail on my system?
it("should fail to lchown nonexistent file", async () => {
await expect(util.promisify(fs.lchown)(helper.tmpFile(), 1, 1))
.resolves.toBeUndefined();
await expect(util.promisify(fs.lchown)(helper.tmpFile(), 1000, 1000))
.rejects.toThrow("ENOENT");
});
});
@@ -586,7 +621,10 @@ describe("fs", () => {
});
});
it("should dispose", () => {
client.dispose();
it("should dispose", (done) => {
setTimeout(() => {
client.dispose();
done();
}, 100);
});
});

View File

@@ -3,9 +3,11 @@ import { createClient } from "./helpers";
describe("Server", () => {
const dataDirectory = "/tmp/example";
const workingDirectory = "/working/dir";
const extensionsDirectory = "/tmp/example";
const builtInExtensionsDirectory = "/tmp/example";
const cacheDirectory = "/tmp/cache";
const client = createClient({
extensionsDirectory,
builtInExtensionsDirectory,
cacheDirectory,
dataDirectory,

View File

@@ -27,9 +27,10 @@ describe("spdlog", () => {
.toContain("critical");
});
it("should dispose", () => {
it("should dispose", (done) => {
setTimeout(() => {
client.dispose();
done();
}, 100);
});
});

View File

@@ -3,6 +3,8 @@ import * as util from "util";
import { Module } from "../src/common/proxy";
import { createClient, Helper } from "./helpers";
// tslint:disable deprecation to use fs.exists
describe("trash", () => {
const client = createClient();
const trash = client.modules[Module.Trash];
@@ -18,9 +20,10 @@ describe("trash", () => {
expect(await util.promisify(fs.exists)(file)).toBeFalsy();
});
it("should dispose", () => {
it("should dispose", (done) => {
setTimeout(() => {
client.dispose();
done();
}, 100);
});
});

View File

@@ -2,11 +2,13 @@ import { Binary } from "@coder/nbin";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { platform } from "../../../build/platform";
const target = `${os.platform()}-${os.arch()}`;
const target = `${platform()}-${os.arch()}`;
const rootDir = path.join(__dirname, "..");
const bin = new Binary({
mainFile: path.join(rootDir, "out", "cli.js"),
target: platform() === "darwin" ? "darwin" : platform() === "musl" ? "alpine" : "linux",
});
bin.writeFiles(path.join(rootDir, "build", "**"));
bin.writeFiles(path.join(rootDir, "out", "**"));

View File

@@ -1,5 +1,6 @@
import { field, logger } from "@coder/logger";
import { ServerMessage, SharedProcessActive } from "@coder/protocol/src/proto";
import { withEnv } from "@coder/protocol";
import { ChildProcess, fork, ForkOptions } from "child_process";
import { randomFillSync } from "crypto";
import * as fs from "fs";
@@ -15,21 +16,28 @@ import opn = require("opn");
import * as commander from "commander";
const collect = <T>(value: T, previous: T[]): T[] => {
return previous.concat(value);
};
commander.version(process.env.VERSION || "development")
.name("code-server")
.description("Run VS Code on a remote server.")
.option("--cert <value>")
.option("--cert-key <value>")
.option("-e, --extensions-dir <dir>", "Set the root path for extensions.")
.option("-d --user-data-dir <dir>", " Specifies the directory that user data is kept in, useful when running as root.")
.option("-e, --extensions-dir <dir>", "Override the main default path for user extensions.")
.option("--extra-extensions-dir [dir]", "Path to an extra user extension directory (repeatable).", collect, [])
.option("--extra-builtin-extensions-dir [dir]", "Path to an extra built-in extension directory (repeatable).", collect, [])
.option("-d --user-data-dir <dir>", "Specifies the directory that user data is kept in, useful when running as root.")
.option("--data-dir <value>", "DEPRECATED: Use '--user-data-dir' instead. Customize where user-data is stored.")
.option("-h, --host <value>", "Customize the hostname.", "0.0.0.0")
.option("-o, --open", "Open in the browser on startup.", false)
.option("-p, --port <number>", "Port to bind on.", parseInt(process.env.PORT!, 10) || 8443)
.option("-N, --no-auth", "Start without requiring authentication.", undefined)
.option("-N, --no-auth", "Start without requiring authentication.", false)
.option("-H, --allow-http", "Allow http connections.", false)
.option("-P, --password <value>", "DEPRECATED: Use the PASSWORD environment variable instead. Specify a password for authentication.")
.option("--disable-telemetry", "Disables ALL telemetry.", false)
.option("--socket <value>", "Listen on a UNIX socket. Host and port will be ignored when set.")
.option("--install-extension <value>", "Install an extension by its ID.")
.option("--bootstrap-fork <name>", "Used for development. Never set.")
.option("--extra-args <args>", "Used for development. Never set.")
@@ -57,12 +65,15 @@ const bold = (text: string | number): string | number => {
readonly userDataDir?: string;
readonly extensionsDir?: string;
readonly extraExtensionsDir?: string[];
readonly extraBuiltinExtensionsDir?: string[];
readonly dataDir?: string;
readonly password?: string;
readonly open?: boolean;
readonly cert?: string;
readonly certKey?: string;
readonly socket?: string;
readonly installExtension?: string;
@@ -81,6 +92,9 @@ const bold = (text: string | number): string | number => {
const dataDir = path.resolve(options.userDataDir || options.dataDir || path.join(dataHome, "code-server"));
const extensionsDir = options.extensionsDir ? path.resolve(options.extensionsDir) : path.resolve(dataDir, "extensions");
const builtInExtensionsDir = path.resolve(buildDir || path.join(__dirname, ".."), "build/extensions");
const extraExtensionDirs = options.extraExtensionsDir ? options.extraExtensionsDir.map((p) => path.resolve(p)) : [];
const extraBuiltinExtensionDirs = options.extraBuiltinExtensionsDir ? options.extraBuiltinExtensionsDir.map((p) => path.resolve(p)) : [];
const workingDir = path.resolve(args[0] || process.cwd());
const dependenciesDir = path.join(os.tmpdir(), "code-server/dependencies");
@@ -98,6 +112,8 @@ const bold = (text: string | number): string | number => {
fse.mkdirp(extensionsDir),
fse.mkdirp(workingDir),
fse.mkdirp(dependenciesDir),
...extraExtensionDirs.map((p) => fse.mkdirp(p)),
...extraBuiltinExtensionDirs.map((p) => fse.mkdirp(p)),
]);
const unpackExecutable = (binaryName: string): void => {
@@ -113,7 +129,6 @@ const bold = (text: string | number): string | number => {
// tslint:disable-next-line no-any
(<any>global).RIPGREP_LOCATION = path.join(dependenciesDir, "rg");
const builtInExtensionsDir = path.resolve(buildDir || path.join(__dirname, ".."), "build/extensions");
if (options.bootstrapFork) {
const modulePath = options.bootstrapFork;
if (!modulePath) {
@@ -177,12 +192,7 @@ const bold = (text: string | number): string | number => {
"--builtin-extensions-dir", builtInExtensionsDir,
"--extensions-dir", extensionsDir,
"--install-extension", options.installExtension,
], {
env: {
VSCODE_ALLOW_IO: "true",
VSCODE_LOGS: process.env.VSCODE_LOGS,
},
}, dataDir);
], withEnv({ env: { VSCODE_ALLOW_IO: "true" } }), dataDir);
fork.stdout.on("data", (d: Buffer) => d.toString().split("\n").forEach((l) => logger.info(l)));
fork.stderr.on("data", (d: Buffer) => d.toString().split("\n").forEach((l) => logger.error(l)));
@@ -192,9 +202,9 @@ const bold = (text: string | number): string | number => {
}
// TODO: fill in appropriate doc url
logger.info("Additional documentation: http://github.com/codercom/code-server");
logger.info("Additional documentation: http://github.com/cdr/code-server");
logger.info("Initializing", field("data-dir", dataDir), field("extensions-dir", extensionsDir), field("working-dir", workingDir), field("log-dir", logDir));
const sharedProcess = new SharedProcess(dataDir, extensionsDir, builtInExtensionsDir);
const sharedProcess = new SharedProcess(dataDir, extensionsDir, builtInExtensionsDir, extraExtensionDirs, extraBuiltinExtensionDirs);
const sendSharedProcessReady = (socket: WebSocket): void => {
const active = new SharedProcessActive();
active.setSocketPath(sharedProcess.socketPath);
@@ -214,6 +224,7 @@ const bold = (text: string | number): string | number => {
}
let password = options.password || process.env.PASSWORD;
const usingCustomPassword = !!password;
if (!password) {
// Generate a random password with a length of 24.
const buffer = Buffer.alloc(12);
@@ -248,6 +259,8 @@ const bold = (text: string | number): string | number => {
serverOptions: {
extensionsDirectory: extensionsDir,
builtInExtensionsDirectory: builtInExtensionsDir,
extraExtensionDirectories: extraExtensionDirs,
extraBuiltinExtensionDirectories: extraBuiltinExtensionDirs,
dataDirectory: dataDir,
workingDirectory: workingDir,
cacheDirectory: cacheHome,
@@ -267,7 +280,11 @@ const bold = (text: string | number): string | number => {
});
logger.info("Starting webserver...", field("host", options.host), field("port", options.port));
app.server.listen(options.port, options.host);
if (options.socket) {
app.server.listen(options.socket);
} else {
app.server.listen(options.port, options.host);
}
let clientId = 1;
app.wss.on("connection", (ws, req) => {
const id = clientId++;
@@ -284,24 +301,32 @@ const bold = (text: string | number): string | number => {
});
app.wss.on("error", (err: NodeJS.ErrnoException) => {
if (err.code === "EADDRINUSE") {
logger.error(`Port ${bold(options.port)} is in use. Please free up port ${options.port} or specify a different port with the -p flag`);
if (options.socket) {
logger.error(`Socket ${bold(options.socket)} is in use. Please specify a different socket.`);
} else {
logger.error(`Port ${bold(options.port)} is in use. Please free up port ${options.port} or specify a different port with the -p flag`);
}
process.exit(1);
}
});
if (!options.certKey && !options.cert) {
logger.warn("No certificate specified. \u001B[1mThis could be insecure.");
// TODO: fill in appropriate doc url
logger.warn("Documentation on securing your setup: https://github.com/codercom/code-server/blob/master/doc/security/ssl.md");
logger.warn("Documentation on securing your setup: https://github.com/cdr/code-server/blob/master/doc/security/ssl.md");
}
if (!options.noAuth) {
if (!options.noAuth && !usingCustomPassword) {
logger.info(" ");
logger.info(`Password:\u001B[1m ${password}`);
} else {
logger.warn("Launched without authentication.");
}
if (options.disableTelemetry) {
logger.info("Telemetry is disabled");
}
const url = `http://localhost:${options.port}/`;
const protocol = options.allowHttp ? "http" : "https";
const url = `${protocol}://localhost:${options.port}/`;
logger.info(" ");
logger.info("Started (click the link below to open):");
logger.info(url);

View File

@@ -133,7 +133,7 @@ export const createApp = async (options: CreateAppOptions): Promise<{
});
});
const server = httpolyglot.createServer(options.bypassAuth ? {} : options.httpsOptions || certs, app) as http.Server;
const server = httpolyglot.createServer(options.allowHttp ? {} : options.httpsOptions || certs, app) as http.Server;
const wss = new ws.Server({ server });
wss.shouldHandle = (req): boolean => {

View File

@@ -7,6 +7,7 @@ import { ParsedArgs } from "vs/platform/environment/common/environment";
import { Emitter } from "@coder/events/src";
import { retry } from "@coder/ide/src/retry";
import { logger, field, Level } from "@coder/logger";
import { withEnv } from "@coder/protocol";
export enum SharedProcessState {
Stopped,
@@ -38,6 +39,8 @@ export class SharedProcess {
private readonly userDataDir: string,
private readonly extensionsDir: string,
private readonly builtInExtensionsDir: string,
private readonly extraExtensionDirs: string[],
private readonly extraBuiltinExtensionDirs: string[],
) {
this.retry.run();
}
@@ -88,13 +91,10 @@ export class SharedProcess {
this.activeProcess.kill();
}
const activeProcess = forkModule("vs/code/electron-browser/sharedProcess/sharedProcessMain", [], {
env: {
VSCODE_ALLOW_IO: "true",
VSCODE_LOGS: process.env.VSCODE_LOGS,
DISABLE_TELEMETRY: process.env.DISABLE_TELEMETRY,
},
}, this.userDataDir);
const activeProcess = forkModule(
"vs/code/electron-browser/sharedProcess/sharedProcessMain", [],
withEnv({ env: { VSCODE_ALLOW_IO: "true" } }), this.userDataDir,
);
this.activeProcess = activeProcess;
await new Promise((resolve, reject): void => {
@@ -136,6 +136,8 @@ export class SharedProcess {
"builtin-extensions-dir": this.builtInExtensionsDir,
"user-data-dir": this.userDataDir,
"extensions-dir": this.extensionsDir,
"extra-extension-dirs": this.extraExtensionDirs,
"extra-builtin-extension-dirs": this.extraBuiltinExtensionDirs,
},
logLevel: this.logger.level,
sharedIPCHandle: this.socketPath,

View File

@@ -6,13 +6,17 @@ import { IStatusbarService, StatusbarAlignment } from "vs/platform/statusbar/com
import * as paths from "./fill/paths";
import product from "./fill/product";
import "./vscode.scss";
import { MenuId, MenuRegistry } from "vs/platform/actions/common/actions";
import { Action } from 'vs/base/common/actions';
import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/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";
import { IStorageService } from "vs/platform/storage/common/storage";
// NOTE: shouldn't import anything from VS Code here or anything that will
// depend on a synchronous fill like `os`.
@@ -33,12 +37,14 @@ class VSClient extends IdeClient {
window.ide = {
client: ideClientInstance,
workbench: {
action: Action,
syncActionDescriptor: SyncActionDescriptor,
commandRegistry: CommandsRegistry,
// tslint:disable-next-line:no-any
menuRegistry: MenuRegistry as any,
// tslint:disable-next-line:no-any
statusbarService: getService<IStatusbarService>(IStatusbarService) as any,
actionsRegistry: Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions),
menuRegistry: MenuRegistry,
statusbarService: getService<IStatusbarService>(IStatusbarService),
notificationService: getService<INotificationService>(INotificationService),
terminalService: getService<ITerminalService>(ITerminalService),
storageService: {
save: (): Promise<void> => {
// tslint:disable-next-line:no-any

View File

@@ -79,9 +79,7 @@
.dialog-entry {
cursor: pointer;
font-size: 1.02em;
padding: 0px;
padding-left: 8px;
padding-right: 8px;
padding: 0px 8px;
.dialog-entry-info {
display: flex;
@@ -94,6 +92,14 @@
margin-right: 5px;
}
.dialog-entry-size {
text-align: right;
}
.dialog-entry-mtime {
padding-left: 8px;
}
&:hover {
background-color: var(--list-hover-background);
}

View File

@@ -52,7 +52,15 @@ export type DialogOptions = OpenDialogOptions | SaveDialogOptions;
export const showOpenDialog = (options: OpenDialogOptions): Promise<string> => {
return new Promise<string>((resolve, reject): void => {
const dialog = new Dialog(DialogType.Open, options);
// Make the default to show hidden files and directories since there is no
// other way to make them visible in the dialogs currently.
const dialog = new Dialog(DialogType.Open, typeof options.properties.showHiddenFiles === "undefined" ? {
...options,
properties: {
...options.properties,
showHiddenFiles: true,
},
} : options);
dialog.onSelect((e) => {
dialog.dispose();
resolve(e);
@@ -404,7 +412,7 @@ class Dialog {
*/
private async list(directory: string): Promise<ReadonlyArray<DialogEntry>> {
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))));
const stats = await Promise.all(paths.map(p => util.promisify(fs.lstat)(path.join(directory, p))));
return stats.map((stat, index): DialogEntry => ({
fullPath: path.join(directory, paths[index]),
@@ -480,7 +488,7 @@ class DialogEntryRenderer implements ITreeRenderer<DialogEntry, string, DialogEn
start: 0,
end: node.filterData.length,
}] : []);
templateData.size.innerText = node.element.size.toString();
templateData.size.innerText = !node.element.isDirectory ? this.humanReadableSize(node.element.size) : "";
templateData.lastModified.innerText = node.element.lastModified;
// We know this exists because we created the template.
@@ -498,4 +506,15 @@ class DialogEntryRenderer implements ITreeRenderer<DialogEntry, string, DialogEn
public disposeTemplate(_templateData: DialogEntryData): void {
// throw new Error("Method not implemented.");
}
/**
* Given a positive size in bytes, return a string that is more readable for
* humans.
*/
private humanReadableSize(bytes: number): string {
const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.min(Math.floor(bytes && Math.log(bytes) / Math.log(1000)), units.length - 1);
return (bytes / Math.pow(1000, i)).toFixed(2) + " " + units[i];
}
}

View File

@@ -12,6 +12,18 @@ export class EnvironmentService extends environment.EnvironmentService {
public get extensionsPath(): string {
return paths.getExtensionsDirectory();
}
public get builtinExtensionsPath(): string {
return paths.getBuiltInExtensionsDirectory();
}
public get extraExtensionPaths(): string[] {
return paths.getExtraExtensionDirectories();
}
public get extraBuiltinExtensionPaths(): string[] {
return paths.getExtraBuiltinExtensionDirectories();
}
}
const target = environment as typeof environment;

View File

@@ -5,6 +5,7 @@ import { OpenProcessExplorer } from "vs/workbench/contrib/issue/electron-browser
import { ToggleDevToolsAction } from "vs/workbench/electron-browser/actions/developerActions";
import { OpenPrivacyStatementUrlAction, OpenRequestFeatureUrlAction, OpenTwitterUrlAction } from "vs/workbench/electron-browser/actions/helpActions";
import { CloseCurrentWindowAction, NewWindowAction, ShowAboutDialogAction } from "vs/workbench/electron-browser/actions/windowActions";
import { REVEAL_IN_OS_COMMAND_ID } from "vs/workbench/contrib/files/browser/fileCommands";
const toSkip = [
ToggleDevToolsAction.ID,
@@ -16,6 +17,7 @@ const toSkip = [
NewWindowAction.ID,
CloseCurrentWindowAction.ID,
CloseWorkspaceAction.ID,
REVEAL_IN_OS_COMMAND_ID,
// Unfortunately referenced as a string
"update.showCurrentReleaseNotes",

View File

@@ -1,5 +1,8 @@
import { InitData, SharedProcessData } from "@coder/protocol";
/**
* Provides paths.
*/
class Paths {
private _appData: string | undefined;
private _defaultUserData: string | undefined;
@@ -7,6 +10,8 @@ class Paths {
private _extensionsDirectory: string | undefined;
private _builtInExtensionsDirectory: string | undefined;
private _workingDirectory: string | undefined;
private _extraExtensionDirectories: string[] | undefined;
private _extraBuiltinExtensionDirectories: string[] | undefined;
public get appData(): string {
if (typeof this._appData === "undefined") {
@@ -48,6 +53,22 @@ class Paths {
return this._builtInExtensionsDirectory;
}
public get extraExtensionDirectories(): string[] {
if (!this._extraExtensionDirectories) {
throw new Error("trying to access extra extension directories before they have been set");
}
return this._extraExtensionDirectories;
}
public get extraBuiltinExtensionDirectories(): string[] {
if (!this._extraBuiltinExtensionDirectories) {
throw new Error("trying to access extra builtin extension directories before they have been set");
}
return this._extraBuiltinExtensionDirectories;
}
public get workingDirectory(): string {
if (!this._workingDirectory) {
throw new Error("trying to access working directory before it has been set");
@@ -56,6 +77,9 @@ class Paths {
return this._workingDirectory;
}
/**
* Initialize paths using the provided data.
*/
public initialize(data: InitData, sharedData: SharedProcessData): void {
process.env.VSCODE_LOGS = sharedData.logPath;
this._appData = data.dataDirectory;
@@ -64,6 +88,8 @@ class Paths {
this._extensionsDirectory = data.extensionsDirectory;
this._builtInExtensionsDirectory = data.builtInExtensionsDirectory;
this._workingDirectory = data.workingDirectory;
this._extraExtensionDirectories = data.extraExtensionDirectories;
this._extraBuiltinExtensionDirectories = data.extraBuiltinExtensionDirectories;
}
}
@@ -73,4 +99,6 @@ export const getDefaultUserDataPath = (): string => _paths.defaultUserData;
export const getWorkingDirectory = (): string => _paths.workingDirectory;
export const getExtensionsDirectory = (): string => _paths.extensionsDirectory;
export const getBuiltInExtensionsDirectory = (): string => _paths.builtInExtensionsDirectory;
export const getExtraExtensionDirectories = (): string[] => _paths.extraExtensionDirectories;
export const getExtraBuiltinExtensionDirectories = (): string[] => _paths.extraBuiltinExtensionDirectories;
export const getSocketPath = (): string => _paths.socketPath;

View File

@@ -11,7 +11,7 @@ class Product implements IProductConfiguration {
public introductoryVideosUrl = "https://code.visualstudio.com/docs/getstarted/introvideos";
public tipsAndTricksUrl = "https://code.visualstudio.com/docs/getstarted/tips-and-tricks";
public twitterUrl = "https://twitter.com/code";
public licenseUrl = "https://github.com/codercom/code-server/blob/master/LICENSE";
public licenseUrl = "https://github.com/cdr/code-server/blob/master/LICENSE";
public aiConfig = process.env.DISABLE_TELEMETRY ? undefined! : {
// Only needed so vscode can see that content exists for this value.
// We override the application insights module.

View File

@@ -2,6 +2,17 @@ import * as vscodeTextmate from "../../../../lib/vscode/node_modules/vscode-text
const target = vscodeTextmate as typeof vscodeTextmate;
const ctx = (require as any).context("../../../../lib/extensions", true, /.*\.tmLanguage.json$/);
// Maps grammar scope to loaded grammar
const scopeToGrammar = {} as any;
ctx.keys().forEach((key: string) => {
const value = ctx(key);
if (value.scopeName) {
scopeToGrammar[value.scopeName] = value;
}
});
target.Registry = class Registry extends vscodeTextmate.Registry {
public constructor(opts: vscodeTextmate.RegistryOptions) {
super({
@@ -21,6 +32,13 @@ target.Registry = class Registry extends vscodeTextmate.Registry {
}).catch(reason => rej(reason));
});
},
loadGrammar: async (scopeName: string) => {
if (scopeToGrammar[scopeName]) {
return scopeToGrammar[scopeName];
}
return opts.loadGrammar(scopeName);
},
});
}
};

View File

@@ -1,4 +1,51 @@
#!/bin/bash
set -e
set -euxo pipefail
yarn task build:server:binary
# Build using a Docker container using the specified image and version.
function docker_build() {
local image="${1}" ; shift
local version="${1}" ; shift
local containerId
containerId=$(docker create --network=host --rm -it -v "$(pwd)"/.cache:/src/.cache "${image}")
docker start "${containerId}"
docker exec "${containerId}" mkdir -p /src
function docker_exec() {
docker exec "${containerId}" bash -c "$@"
}
docker cp ./. "${containerId}":/src
docker_exec "cd /src && yarn"
docker_exec "cd /src && npm rebuild"
docker_exec "cd /src && NODE_ENV=production VERSION=${version} yarn task build:server:binary"
docker_exec "cd /src && yarn task package ${version}"
docker cp "${containerId}":/src/release/. ./release/
docker stop "${containerId}"
}
function main() {
local version=${VERSION:-}
local ostype=${OSTYPE:-}
if [[ -z "${version}" ]] ; then
>&2 echo "Must set VERSION environment variable"
exit 1
fi
if [[ "${ostype}" == "darwin"* ]]; then
NODE_ENV=production VERSION="${version}" yarn task build:server:binary
yarn task package "${version}"
else
local image
if [[ "$TARGET" == "alpine" ]]; then
image="codercom/nbin-alpine"
else
image="codercom/nbin-centos"
fi
docker_build "${image}" "${version}"
fi
}
main "$@"

View File

@@ -21,3 +21,15 @@ Object.defineProperty(fs.read, util.promisify.custom, {
global.requestAnimationFrame = (cb) => {
setTimeout(cb, 0);
};
// lchmod might not be available. Jest runs graceful-fs which makes this a no-op
// when it doesn't exist but that doesn't seem to always run when running
// multiple tests (or maybe it gets undone after a test).
if (!fs.lchmod) {
fs.lchmod = function (path, mode, cb) {
if (cb) {
process.nextTick(cb);
}
};
fs.lchmodSync = function () {};
}

View File

@@ -129,9 +129,12 @@ index f91ca2b..ef6fce9 100644
- const isMac = platform.isMacintosh;
+ const isMac = browser.isMacintosh;
diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts
index f08c996..f9de58c 100644
index f08c996..7db13fa 100644
--- a/src/vs/code/electron-browser/issue/issueReporterMain.ts
+++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts
@@ -296 +296 @@ export class IssueReporter extends Disposable {
- const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath];
+ const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath, ...this.environmentService.extraExtensionPaths];
@@ -425 +425 @@ export class IssueReporter extends Disposable {
- const cmdOrCtrlKey = platform.isMacintosh ? e.metaKey : e.ctrlKey;
+ const cmdOrCtrlKey = browser.isMacintosh ? e.metaKey : e.ctrlKey;
@@ -146,25 +149,33 @@ index e0ff793..885de12 100644
- const cmdOrCtrlKey = platform.isMacintosh ? e.metaKey : e.ctrlKey;
+ const cmdOrCtrlKey = browser.isMacintosh ? e.metaKey : e.ctrlKey;
diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts
index 6fd8249..04c0933 100644
index 6fd8249..6ae6b11 100644
--- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts
+++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts
@@ -50,0 +51,2 @@ import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiatio
+import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService';
+import { mkdirp } from 'vs/base/node/pfs';
@@ -93,0 +96,8 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I
@@ -93,0 +96,10 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I
+ Promise.all<boolean | undefined>([ // Copied from src/vs/code/electron-main/main.ts
+ environmentService.extensionsPath,
+ environmentService.nodeCachedDataDir,
+ environmentService.logsPath,
+ environmentService.globalStorageHome,
+ environmentService.workspaceStorageHome,
+ environmentService.backupHome
+ environmentService.backupHome,
+ ...environmentService.extraExtensionPaths,
+ ...environmentService.extraBuiltinExtensionPaths,
+ ].map((path): undefined | Promise<boolean> => path ? mkdirp(path) : undefined));
@@ -119,0 +130,2 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I
@@ -119,0 +132,2 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I
+ const backupMainService = instantiationService.createInstance(BackupMainService) as BackupMainService;
+ backupMainService.initialize().catch(console.error);
@@ -223,0 +236 @@ async function handshake(configuration: ISharedProcessConfiguration): Promise<vo
@@ -124 +138 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I
- const { appRoot, extensionsPath, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService;
+ const { appRoot, extensionsPath, extraExtensionPaths, extensionDevelopmentLocationURI, isBuilt, installSourcePath } = environmentService;
@@ -138 +152 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I
- piiPaths: [appRoot, extensionsPath]
+ piiPaths: [appRoot, extensionsPath, ...extraExtensionPaths]
@@ -223,0 +238 @@ async function handshake(configuration: ISharedProcessConfiguration): Promise<vo
+startup({ machineId: "1" });
diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts
index 1f8b17a..2a875f9 100644
@@ -175,7 +186,7 @@ index 1f8b17a..2a875f9 100644
+ await cli.main(args);
+ return; // Always just do this for now.
diff --git a/src/vs/editor/browser/config/configuration.ts b/src/vs/editor/browser/config/configuration.ts
index f97a692..0206957 100644
index f97a692..8059a67 100644
--- a/src/vs/editor/browser/config/configuration.ts
+++ b/src/vs/editor/browser/config/configuration.ts
@@ -10 +9,0 @@ import { Disposable } from 'vs/base/common/lifecycle';
@@ -187,9 +198,6 @@ index f97a692..0206957 100644
@@ -367 +366 @@ export class Configuration extends CommonEditorConfiguration {
- if (platform.isMacintosh) {
+ if (browser.isMacintosh) {
@@ -378 +377 @@ export class Configuration extends CommonEditorConfiguration {
- emptySelectionClipboard: browser.isWebKit || browser.isFirefox,
+ emptySelectionClipboard: false, // browser.isWebKit || browser.isFirefox,
diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts
index b3b4472..f888d63 100644
--- a/src/vs/editor/browser/controller/mouseHandler.ts
@@ -250,32 +258,55 @@ index c69ea3f..b8d87f7 100644
-const GOLDEN_LINE_HEIGHT_RATIO = platform.isMacintosh ? 1.5 : 1.35;
+const GOLDEN_LINE_HEIGHT_RATIO = browser.isMacintosh ? 1.5 : 1.35;
diff --git a/src/vs/editor/contrib/clipboard/clipboard.ts b/src/vs/editor/contrib/clipboard/clipboard.ts
index 990be3a..8a326c6 100644
index 990be3a..18ae0d5 100644
--- a/src/vs/editor/contrib/clipboard/clipboard.ts
+++ b/src/vs/editor/contrib/clipboard/clipboard.ts
@@ -29 +29,2 @@ const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdgeOrIE)
@@ -18,0 +19 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis
+import { clipboard } from 'electron';
@@ -29 +30,2 @@ const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdgeOrIE)
-const supportsPaste = (platform.isNative || (!browser.isChrome && document.queryCommandSupported('paste')));
+// const supportsPaste = (platform.isNative || (!browser.isChrome && document.queryCommandSupported('paste')));
+const supportsPaste = true;
@@ -176,0 +178 @@ class ExecCommandPasteAction extends ExecCommandAction {
@@ -71 +73 @@ class ExecCommandCutAction extends ExecCommandAction {
- kbOpts = null;
+ // kbOpts = null;
@@ -119 +121 @@ class ExecCommandCopyAction extends ExecCommandAction {
- kbOpts = null;
+ // kbOpts = null;
@@ -174 +176 @@ class ExecCommandPasteAction extends ExecCommandAction {
- kbOpts = null;
+ // kbOpts = null;
@@ -176,0 +179 @@ class ExecCommandPasteAction extends ExecCommandAction {
+ const { workbench } = require('vs/../../../../packages/vscode/src/workbench') as typeof import ('vs/../../../../packages/vscode/src/workbench');
@@ -181 +183 @@ class ExecCommandPasteAction extends ExecCommandAction {
@@ -181 +184 @@ class ExecCommandPasteAction extends ExecCommandAction {
- precondition: EditorContextKeys.writable,
+ precondition: (require('vs/platform/contextkey/common/contextkey') as typeof import('vs/platform/contextkey/common/contextkey')).ContextKeyExpr.and(EditorContextKeys.writable, workbench.clipboardContextKey),
@@ -191 +193,2 @@ class ExecCommandPasteAction extends ExecCommandAction {
@@ -191 +194,2 @@ class ExecCommandPasteAction extends ExecCommandAction {
- order: 3
+ order: 3,
+ when: workbench.clipboardContextKey,
@@ -194,0 +198,14 @@ class ExecCommandPasteAction extends ExecCommandAction {
@@ -194,0 +199,26 @@ class ExecCommandPasteAction extends ExecCommandAction {
+
+ public async run(accessor, editor: ICodeEditor): Promise<void> {
+ if (editor instanceof (require('vs/editor/browser/widget/codeEditorWidget') as typeof import('vs/editor/browser/widget/codeEditorWidget')).CodeEditorWidget) {
+ try {
+ editor.trigger('', (require('vs/editor/common/editorCommon') as typeof import ('vs/editor/common/editorCommon')).Handler.Paste, {
+ text: await (require('vs/../../../../packages/vscode/src/workbench') as typeof import ('vs/../../../../packages/vscode/src/workbench')).workbench.clipboardText,
+ editor.focus();
+ const textInput = document.activeElement! as HTMLTextAreaElement;
+ const dataTransfer = new DataTransfer();
+ const value = await clipboard.readText();
+ dataTransfer.setData("text/plain", value);
+ const pasteEvent = new ClipboardEvent("paste", {
+ clipboardData: dataTransfer,
+ });
+ textInput.dispatchEvent(pasteEvent);
+ } catch (ex) {
+ super.run(accessor, editor);
+ try {
+ editor.trigger('', (require('vs/editor/common/editorCommon') as typeof import ('vs/editor/common/editorCommon')).Handler.Paste, {
+ text: await (require('vs/../../../../packages/vscode/src/workbench') as typeof import ('vs/../../../../packages/vscode/src/workbench')).workbench.clipboardText,
+ });
+ } catch (ex) {
+ super.run(accessor, editor);
+ }
+ }
+ } else {
+ super.run(accessor, editor);
@@ -363,6 +394,81 @@ index 9952574..908a9ae 100644
@@ -9 +9 @@ import { URI } from 'vs/base/common/uri';
-import { isMacintosh } from 'vs/base/common/platform';
+import { isMacintosh } from 'vs/base/browser/browser';
diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts
index eb1873c..dbbacd0 100644
--- a/src/vs/platform/environment/common/environment.ts
+++ b/src/vs/platform/environment/common/environment.ts
@@ -120,0 +121,2 @@ export interface IEnvironmentService {
+ extraExtensionPaths: string[];
+ extraBuiltinExtensionPaths: string[];
diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts
index 43866f8..e69b513 100644
--- a/src/vs/platform/environment/node/environmentService.ts
+++ b/src/vs/platform/environment/node/environmentService.ts
@@ -172,0 +173,8 @@ export class EnvironmentService implements IEnvironmentService {
+ @memoize
+ get extraExtensionPaths(): string[] {
+ return this._args['extra-extension-dirs'] || [];
+ }
+ @memoize
+ get extraBuiltinExtensionPaths(): string[] {
+ return this._args['extra-builtin-extension-dirs'] || [];
+ }
diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts
index c897029..f84d9b6 100644
--- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts
+++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts
@@ -733,5 +733,9 @@ export class ExtensionManagementService extends Disposable implements IExtension
- const systemExtensionsPromise = this.scanExtensions(this.systemExtensionsPath, ExtensionType.System)
- .then(result => {
- this.logService.info('Scanned system extensions:', result.length);
- return result;
- });
+ const systemExtensionsPromise = Promise.all([
+ this.scanExtensions(this.systemExtensionsPath, ExtensionType.System),
+ ...this.environmentService.extraBuiltinExtensionPaths
+ .map((path) => this.scanExtensions(path, ExtensionType.System))
+ ]).then((results) => {
+ const result = results.reduce((flat, current) => flat.concat(current), []);
+ this.logService.info('Scanned system extensions:', result.length);
+ return result;
+ });
@@ -761 +765 @@ export class ExtensionManagementService extends Disposable implements IExtension
- return Promise.all([this.getUninstalledExtensions(), this.scanExtensions(this.extensionsPath, ExtensionType.User)])
+ return Promise.all([this.getUninstalledExtensions(), this.scanAllUserExtensions(this.extensionsPath, ExtensionType.User)])
@@ -772,0 +777,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
+ private scanAllUserExtensions(folderName: string, type: ExtensionType): Promise<ILocalExtension[]> {
+ return Promise.all([
+ this.scanExtensions(folderName, type),
+ ...this.environmentService.extraExtensionPaths.map((p) => this.scanExtensions(p, ExtensionType.User))
+ ]).then((results) => results.reduce((flat, current) => flat.concat(current), []));
+ }
+
@@ -805 +816 @@ export class ExtensionManagementService extends Disposable implements IExtension
- .then(uninstalled => this.scanExtensions(this.extensionsPath, ExtensionType.User) // All user extensions
+ .then(uninstalled => this.scanAllUserExtensions(this.extensionsPath, ExtensionType.User) // All user extensions
@@ -814 +825 @@ export class ExtensionManagementService extends Disposable implements IExtension
- return this.scanExtensions(this.extensionsPath, ExtensionType.User) // All user extensions
+ return this.scanAllUserExtensions(this.extensionsPath, ExtensionType.User) // All user extensions
diff --git a/src/vs/platform/storage/node/storageMainService.ts b/src/vs/platform/storage/node/storageMainService.ts
index 9845da1..567c195 100644
--- a/src/vs/platform/storage/node/storageMainService.ts
+++ b/src/vs/platform/storage/node/storageMainService.ts
@@ -169 +169,6 @@ export class StorageMainService extends Disposable implements IStorageMainServic
- return readdir(this.environmentService.extensionsPath).then(extensions => {
+ return Promise.all([
+ this.environmentService.extensionsPath,
+ ...this.environmentService.extraExtensionPaths,
+ ].map((p) => readdir(p)))
+ .then((results) => results.reduce((flat, current) => flat.concat(current), []))
+ .then(extensions => {
diff --git a/src/vs/platform/telemetry/electron-browser/telemetryService.ts b/src/vs/platform/telemetry/electron-browser/telemetryService.ts
index 31d0309..5b166af 100644
--- a/src/vs/platform/telemetry/electron-browser/telemetryService.ts
+++ b/src/vs/platform/telemetry/electron-browser/telemetryService.ts
@@ -42 +42 @@ export class TelemetryService extends Disposable implements ITelemetryService {
- piiPaths: [environmentService.appRoot, environmentService.extensionsPath]
+ piiPaths: [environmentService.appRoot, environmentService.extensionsPath, ...environmentService.extraExtensionPaths]
diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts
index cbc55b3..9d27c01 100644
--- a/src/vs/platform/windows/common/windows.ts
@@ -807,7 +913,7 @@ index 780147c..2e8c9af 100644
- if (platform.isMacintosh) {
+ if (browser.isMacintosh) {
diff --git a/src/vs/workbench/contrib/webview/electron-browser/webview-pre.js b/src/vs/workbench/contrib/webview/electron-browser/webview-pre.js
index 74fc798..03728d0 100644
index 74fc798..0b6b5eb 100644
--- a/src/vs/workbench/contrib/webview/electron-browser/webview-pre.js
+++ b/src/vs/workbench/contrib/webview/electron-browser/webview-pre.js
@@ -10 +10,19 @@
@@ -846,6 +952,11 @@ index 74fc798..03728d0 100644
+ // supportFetchAPI: true,
+ // corsEnabled: true
+ // });
@@ -328 +346,3 @@
- newFrame.contentWindow.focus();
+ if (document.hasFocus()) {
+ newFrame.contentWindow.focus();
+ }
diff --git a/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts
index 484ff86..f3f57cb 100644
--- a/src/vs/workbench/contrib/webview/electron-browser/webview.contribution.ts
@@ -974,11 +1085,30 @@ index 75f0026..2e94683 100644
- if (!isMacintosh && getTitleBarStyle(configurationService, environmentService) === 'custom') {
+ if (!(isNative && isMacintosh) && getTitleBarStyle(configurationService, environmentService) === 'custom') {
diff --git a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts
index 059f821..2dde675 100644
index 059f821..b19f292 100644
--- a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts
+++ b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts
@@ -32,0 +33 @@ function getSystemExtensionsRoot(): string {
+ return (require('vs/../../../../packages/vscode/src/fill/paths') as typeof import ('vs/../../../../packages/vscode/src/fill/paths')).getBuiltInExtensionsDirectory();
@@ -191,2 +192,3 @@ export class CachedExtensionScanner {
- const folderStat = await pfs.stat(input.absoluteFolderPath);
- input.mtime = folderStat.mtime.getTime();
+ const folderStats = await Promise.all([pfs.stat(input.absoluteFolderPath), ...input.extraFolderPaths.map((p) => pfs.stat(p))]);
+ input.mtime = folderStats[0].mtime.getTime();
+ input.extraMtimes = folderStats.slice(1).map((s) => s.mtime.getTime());
@@ -259 +261 @@ export class CachedExtensionScanner {
- new ExtensionScannerInput(version, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations),
+ new ExtensionScannerInput(version, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations, environmentService.extraBuiltinExtensionPaths),
@@ -290 +292 @@ export class CachedExtensionScanner {
- new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, false, translations),
+ new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, false, translations, environmentService.extraExtensionPaths),
diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts
index 9133b7e..8c801b7 100644
--- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts
+++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts
@@ -461 +461 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
- if (errorMessage === this._lastExtensionHostError) {
+ if (errorMessage === this._lastExtensionHostError || errorMessage === "disconnected") {
diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts
index b337206..0477464 100644
--- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts
@@ -996,6 +1126,54 @@ index 838a9c7..2308cee 100644
@@ -192 +192 @@ function connectToRenderer(protocol: IMessagePassingProtocol): Promise<IRenderer
- process.kill(initData.parentPid, 0); // throws an exception if the main process doesn't exist anymore.
+ // process.kill(initData.parentPid, 0); // throws an exception if the main process doesn't exist anymore.
diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts
index 6e2179d..e6f38c9 100644
--- a/src/vs/workbench/services/extensions/node/extensionPoints.ts
+++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts
@@ -445,0 +446 @@ export class ExtensionScannerInput {
+ public extraMtimes: number[] = [];
@@ -455 +456,2 @@ export class ExtensionScannerInput {
- public readonly tanslations: Translations
+ public readonly tanslations: Translations,
+ public readonly extraFolderPaths: string[] = [],
@@ -469,0 +472,16 @@ export class ExtensionScannerInput {
+ // Allow extra folder paths in any order. Doesn't account for duplicates though.
+ const eq = (a: string[] = [], b: string[] = [], atimes: number[] = [], btimes: number[] = []): boolean => {
+ if (a.length !== b.length || atimes.length !== btimes.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; ++i) {
+ const index = b.indexOf(a[i]);
+ if (index === -1) {
+ return false;
+ }
+ if (atimes[i] !== btimes[index]) {
+ return false;
+ }
+ }
+ return true;
+ };
@@ -479,0 +498 @@ export class ExtensionScannerInput {
+ && eq(a.extraFolderPaths, b.extraFolderPaths, a.extraMtimes, b.extraMtimes)
@@ -530 +549 @@ export class ExtensionScanner {
- * Scan a list of extensions defined in `absoluteFolderPath`
+ * Scan a list of extensions defined in `absoluteFolderPath` and `extraFolderPaths`
@@ -532 +551 @@ export class ExtensionScanner {
- public static async scanExtensions(input: ExtensionScannerInput, log: ILog, resolver: IExtensionResolver | null = null): Promise<IExtensionDescription[]> {
+ public static async scanExtensions(input: ExtensionScannerInput, log: ILog, resolvers: IExtensionResolver | IExtensionResolver[] | null = null): Promise<IExtensionDescription[]> {
@@ -533,0 +553 @@ export class ExtensionScanner {
+ const extraFolderPaths = input.extraFolderPaths;
@@ -537,2 +557,4 @@ export class ExtensionScanner {
- if (!resolver) {
- resolver = new DefaultExtensionResolver(absoluteFolderPath);
+ if (!resolvers) {
+ resolvers = [absoluteFolderPath, ...extraFolderPaths].map((p) => new DefaultExtensionResolver(p));
+ } else if (!Array.isArray(resolvers)) {
+ resolvers = [resolvers];
@@ -552 +574,2 @@ export class ExtensionScanner {
- let refs = await resolver.resolveExtensions();
+ let refs = await Promise.all(resolvers.map((resolver) => resolver.resolveExtensions()))
+ .then((results) => results.reduce((flat, current) => flat.concat(current), []));
diff --git a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts b/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts
index 33d3697..af71b01 100644
--- a/src/vs/workbench/services/files/node/watcher/nsfw/watcherService.ts