Compare commits

...

231 Commits

Author SHA1 Message Date
Asher
cf63bbd003 Fix "no authentication" message with custom password
Fixes #757.
2019-06-11 12:08:02 -05:00
Anmol Sethi
02f62882b8 Propagate full env to browser (#756)
* Propogate full env to browser

* Add support for setting $ITEM_URL

* Add serviceURL getter
2019-06-11 11:54:49 -05:00
Asher
362715bbeb Fix dialog items getting cut off
Fixes #336.
2019-06-07 14:30:45 -05:00
Dean Sheather
ec70ea6994 Change documentation to use port 8443 (#740) 2019-06-07 14:22:33 -05:00
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
Kyle Carberry
166efcb17e Hide titlebar controls and fix menubar toggle (#550) 2019-04-19 19:54:50 -05:00
Kyle Carberry
206e107a9a Merge branch 'master' of github.com:codercom/code-server 2019-04-19 20:03:54 -04:00
Kyle Carberry
12c8b5d337 Closes #466 2019-04-19 20:03:52 -04:00
Asher
4aa20fd864 Fix require.toUrl on the Node side
- Fixes #542
2019-04-19 13:32:10 -05:00
Illia Poplawski
0cd4e46055 Fix self hosted flags (#523)
* Remove -h flag for help information

* Add tab after removing -h option

* Remove spaces after --user-data-dir option to line up with other menu items
2019-04-18 13:02:29 -05:00
Asher
f51823b51f Fix open dialog
- Fixes #508
2019-04-18 12:08:44 -05:00
Asher
55bfeab208 Deprecate password flag in favor of an environment variable 2019-04-18 11:10:55 -05:00
Asher
309d15cefd Use file/folder open commands for all operating systems
Mac was using its own thing.

- Fixes #535
- Fixes #501
2019-04-18 10:50:23 -05:00
Kyle Carberry
95006a435a Merge branch 'master' of github.com:codercom/code-server 2019-04-17 21:01:25 -04:00
Kyle Carberry
4d576ab4d4 Trigger configuration update when loaded 2019-04-17 21:01:23 -04:00
Asher
d5b03ef60e Remove version from Docker command 2019-04-17 17:38:22 -05:00
Anmol Sethi
cdc5b55a9d Remove chmod on project dir
See #471
2019-04-17 18:36:33 -04:00
Kyle Carberry
c3a38e3fea Add telemetry and option to disable (#519)
* Add telemetry and option to disable

* Update readme and getting-started guide

* Update lockfile

* Update getting started guide
2019-04-17 17:18:35 -05:00
Asher
cc8c7e2cee Make assets unique (#518)
* Make all assets unique

All CSS and JavaScript files have unique names now. I also moved the
login to the /login path in order to ensure the HTML for each page is
also unique.

* Explicitly include assets to cache
2019-04-17 17:18:20 -05:00
Kyle Carberry
e0f1787ce6 Update logger 2019-04-17 17:47:02 -04:00
John McCambridge
50e6108012 Add confirmation for closing window (#463)
* Add confirmation for closing window

* Make function an event listener for consistency

* Change let to const

* Refactor for compatability
2019-04-17 16:42:36 -05:00
Asher
630ccfcacc Add flag to install an extension from the command line (#504) 2019-04-17 16:30:50 -05:00
Patrick Smith
c4c26058ef Use port from PORT env variable if available (#502) 2019-04-17 16:30:39 -05:00
Kyle Carberry
8a4da542ae Add GCP logging extender (#503)
* Add GCP logging extender

* Minor version bump for new API

* Update packages/logger/src/logger.ts

Co-Authored-By: kylecarbs <kyle@kwc.io>
2019-04-17 13:41:10 -05:00
Luca Casonato
e907dbe7e6 fixe password signin with service-worker (#511) 2019-04-17 10:27:24 -05:00
Luca Casonato
22b485acd9 Added serviceworker and web-manifest (#154)
* Added serviceworker and manifest.json

* added deps in package.json

* fixed image link

* actually fixed images i think

* added assets to individual module folders

* added caching

* Serviceworker now properly loads

* Changed single to double quotes

* Update lock

* moved the service worker back into prod only

* removed sw from general

* changed background color of splash screen

* added logo to server

* centralized logo into single assets folder
2019-04-16 15:44:57 -05:00
Asher
b8f222acf2 Ensure all environment paths are created
This normally happens in electron-main/main.ts but we don't run anything
from there.

Fixes #499.
2019-04-16 10:58:13 -05:00
Asher
aabb2ecda7 Update node to 10.15.1 (#472)
* Update Node to 10.15.1

* Remove string replace that was used for oclif

* Update nbin

* Package node-pty and spdlog with nbin

* Label stderr/stdout from shared process

* Remove fork override

* Prevent "already disposed" errors when trying to kill disposed proxies

* Include spdlog dependencies

* Shim /node_modules

* Add node_modules to Docker ignore

It keeps using my already-built .node files which results in a
mismatching GLIBC version error.

* Update nbin
2019-04-15 19:48:12 -05:00
Kibo Hikari
dfabc070b9 fix(dockerfile): explicitly create a volume and set permission for project dir (#471)
Signed-off-by: Kibo Hikari <enra@sayonika.moe>
2019-04-13 23:34:25 -05:00
Kyle Carberry
da420cdda9 Merge branch 'master' of github.com:codercom/code-server 2019-04-14 00:32:06 -04:00
Kyle Carberry
c6a46e4753 Fix reading text from clipboard via electron fill 2019-04-14 00:32:03 -04:00
Asher
742dd6f597 Upgrade VS Code to 1.33.1 2019-04-12 16:10:45 -05:00
Yen-Chi Chen
6c3ff1d6f0 Fix typo: doc/self-hosted/index.md (#477) 2019-04-12 10:35:54 -05:00
John McCambridge
db57aa229f Allow use of the enter key for password input for code-server (#479)
* Allow use of the enter key for password input for code-server

* Remove function, make html form

* Remove function and create html form

* Handle form submit action

* Remove button listener

* Check if form exists
2019-04-12 10:35:30 -05:00
David Pinezich
f7342ede69 Update digitalocean.md (#486)
Seems that   sudo ./code-server-linux -p 80 is outdated. The "linux" is not anymore given.
2019-04-11 16:18:13 -05:00
Anmol Sethi
b781ccde9c Add common utilities to Dockerfile (#488) 2019-04-11 15:30:21 -05:00
Asher
8f62b2bff2 Decrease max retry time to three seconds 2019-04-11 14:58:29 -05:00
Asher
7047be859c Fix some open file/folder issues
- "Open folder" now says "open folder" instead of "open file"
- "Open folder" won't allow you to open files
- "Open file" won't allow you to open directories

Fixes #249.
2019-04-11 14:55:06 -05:00
Kyle Carberry
2785e2219a Remove hosted from the readme 2019-04-11 02:11:51 +00:00
majik
4b217fba00 Feature/relative url (#307)
* resrouce relative url

* add url pathname for require.toUrl (fix webview url)

* rest resrouce relative url

* fix resource url

* ignore .vscode
2019-04-10 20:07:53 -05:00
Kyle Carberry
3fae68bbab Add storage save to ide-api 2019-04-09 12:24:53 -04:00
Asher
a2f20aa25c Update window service fill 2019-04-08 18:04:41 -05:00
Asher
94b8b9a5cf Fix window.open infinite loop 2019-04-08 17:44:10 -05:00
Asher
bbd8b27fc7 Explicitly mention if netstat isn't installed; downgrade to warning 2019-04-08 11:15:25 -05:00
Asher
6361635b55 Move backup service init to the shared process
Since this is where it attempts to read it, this should solve the JSON
errors for good.
2019-04-08 11:03:33 -05:00
Kyle Carberry
d2da4cfc43 Fix errors appearing 2019-04-07 17:04:49 -04:00
Asher
a1136b3a02 Update VS Code to 1.33.0 (#445)
* Update VS Code to 1.33.0

* Fix slow file tree

* Fix WindowsService fill

* Provide `off` on event listeners

* Fix webview

* Fix double title bar and missing preferences on Mac

* Bump VS Code version in Travis config

* Fix black dialog text (again)

* Fix shared process not starting
2019-04-05 18:49:29 -05:00
Anmol Sethi
4dd74b31b9 Merge pull request #435 from codercom/docker-fixes
Reduce layers in Dockerfile
2019-04-05 10:18:50 -05:00
Anmol Sethi
bc0f6eb65d Reduce layers in Dockerfile 2019-04-04 20:00:42 -05:00
Anmol Sethi
6737d3da18 Merge pull request #433 from codercom/docker-fixes
Significantly improve the Dockerfile
2019-04-04 18:42:14 -05:00
Anmol Sethi
eb0f773146 Update README.md 2019-04-04 18:41:45 -05:00
Anmol Sethi
ebac84899e Significantly improve the Dockerfile
- Adds dumb-init so closes #403, closes #361, closes #383
- User mode docker so closes #192, closes #65
- Uses latest docker ubuntu instead of 18.10 which is the rolling tag so closes #404

Thanks to @RichardMcSorley and @sr229
2019-04-04 18:41:45 -05:00
Richard McSorley
0b7a090a73 Use an init system 2019-04-04 18:41:45 -05:00
Hikari Kibo
a95019f38d fix: slap adduser into another RUN
im abusing the word slap today please help me
2019-04-04 18:41:45 -05:00
Hikari
15948c1af6 feat: user-mode docker
Signed-off-by: Hikari <enra@sayonika.moe>
2019-04-04 18:41:45 -05:00
Asher
e73eb74208 Fix sending dates through the protocol
Fixes #253.
2019-04-04 18:24:53 -05:00
Kyle Carberry
278c59b920 Remove non-working menu items 2019-04-04 18:04:34 -04:00
Kyle Carberry
5a1eb858a9 Merge branch 'master' of github.com:codercom/code-server 2019-04-04 17:13:58 -04:00
Kyle Carberry
3c1dfb1170 Update @coder/nbin. Fixes UI state saves 2019-04-04 17:13:54 -04:00
Asher
09a02eb9e9 Build for production in Docker 2019-04-04 13:33:49 -05:00
Kyle Carberry
2bd7281fa0 Update @coder/nbin 2019-04-04 09:59:50 -04:00
Asher
e12fcd3a0d Fix error when shared process exits with null 2019-04-03 17:32:20 -05:00
Kyle Carberry
4af84fcaf6 Add flags for customizing user data dir and extensions dir (#420)
* Add flags for customizing extensions directory

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

* Add bold function for text/numbers

* remove unused dependency:

* remove unused line break

* Change logger message

* Use NodeJS.ErrnoException type

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

* Cache lib directory for speedy builds

* Fix path linking for default extensions

* Update reset

* Reset to head

* Improve caching

* Still run yarn in CI

* Update yarn before install

* Increase cache timeout

* Install vscode from vstar

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

* Remove additional flags added

* Remove unused dependency

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

* Handle all floating promises

* Remove stringified proto from trace logging

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

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

* Connect to a new terminal instance on disconnect

* Use our retry for the watcher

* Specify method when proxy doesn't exist

* Don't error when closing/killing disconnected proxy

* Specify proxy ID when a method doesn't exist

* Use our retry for the searcher

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

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

* Use our retry for the extension host

* Emit error in parent proxy class

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

* Reconnect spdlog

* Add error message when shared process disconnects

* Pass method resolve to parse

* Don't pass method to getProxy

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

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

* Remove unecessary space

* Add smaller gif

* Add even smaller gif

* Trim time in new gif

* Cropped a tad more

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

to license.

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

Fixes #247.

* Batch stat and readdir calls

* Fix fs.exists

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

* Batch lstat

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

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

* Return proxies synchronously

Otherwise events can be lost.

* Ensure events cannot be missed

* Refactor remaining fills

* Use more up-to-date version of util

For callbackify.

* Wait for dispose to come back before removing

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

* Remove old node-pty tests

* Fix emitting events twice on duplex streams

* Preserve environment when spawning processes

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

* Remove rimraf dependency from ide

* Update net.Server.listening

* Use exit event instead of killed

Doesn't look like killed is even a thing.

* Add response timeout to server

* Fix trash

* Require node-pty & spdlog after they get unpackaged

This fixes an error when running in the binary.

* Fix errors in down emitter preventing reconnecting

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

* Refactor event tests to use jest.fn()

* Reject proxy call when disconnected

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

* Use nbin for binary packaging

* Remove additional module requires

* Attempt to remove require for local bootstrap-fork

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

* Update README.md

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

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

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

* doc: change occurences of index to install guide

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

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

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

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

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

* doc: remove citations for dev mode requirement

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

* doc: clarify more wording

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

* doc: fix typo in Crostini section

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

* Fixed google-cloud documentation steps order

* Edited docs based on the latest release versions

* Make docs more dynamic based on Releases

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

Signed-off-by: Steve Sloka <slokas@vmware.com>
2019-03-18 10:44:08 -05:00
Jim Tittsler
6749f25bbf Doc: fix typo (#277) 2019-03-17 22:35:29 -05:00
Sean Smith
f6b96e3778 Fix security links in cloud setup guides (#260) 2019-03-15 15:32:24 -04:00
Kyle Carberry
3b8cd0a3cd Fix docker cmd in readme 2019-03-15 01:24:44 -04:00
Kyle Carberry
2f27b5df8c Fix #251 2019-03-15 00:51:05 -04:00
Kyle Carberry
400fba7f6f Fix type import not resolving properly 2019-03-15 00:48:39 -04:00
Kyle Carberry
bfaadd4e51 Merge branch 'master' of github.com:codercom/code-server 2019-03-14 22:58:21 +00:00
Kyle Carberry
d16b35ed0b Remove sourcemaps for production 2019-03-14 22:58:12 +00:00
Dafydd
633f8dcd72 Fixing broken syntax in the run command (#238) 2019-03-14 18:32:40 -04:00
Mike Hatch
98cad8ae69 Fixed a couple of typos (#243) 2019-03-14 18:32:18 -04:00
Kyle Carberry
2e53bb6690 Automatically target production for client-side builds 2019-03-14 22:29:19 +00:00
Anmol Sethi
e3d9716607 Merge pull request #231 from nhooyr/docker
Dockerfile: include git
2019-03-13 15:38:55 -04:00
Anmol Sethi
862c94401a Dockerfile: include git
Closes #221
Closes #230
Closes #203
2019-03-13 15:19:38 -04:00
Sandro Jäckel
3a6e27bc87 Combine apt-get in final docker image and remove cache 2019-03-13 15:13:24 -04:00
pokemonlover1234
ec2d01ab40 Add git to docker container
Install git in the docker container, proposed by #203. While it is possible that issue requires further discussion, this is just a proposal pull request. If that proposal is accepted, here's the pull request.
2019-03-13 15:12:18 -04:00
Asher
e4ff723895 Fix race with watcher & stat
There is a window between when the stat is made and the result makes it
back to the client where a file is created or deleted and it won't be a
part of that stat.

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

/doc/assets/chrome_confirm.png -- 34.10kb -> 18.95kb (44.43%)
/doc/assets/server-password-modal.png -- 96.67kb -> 58.14kb (39.86%)
/doc/assets/ide.png -- 984.53kb -> 603.38kb (38.71%)
/doc/assets/chrome_warning.png -- 44.25kb -> 30.03kb (32.14%)
/doc/assets/logo-horizontal.png -- 22.00kb -> 17.48kb (20.55%)
/doc/assets/aws_ubuntu.png -- 51.62kb -> 43.93kb (14.91%)
/doc/assets/cli.png -- 160.70kb -> 136.78kb (14.89%)
2019-03-13 14:45:52 -04:00
Michael Desantis
0efae1fcb6 Add error message for incorrect password. Fix issue #55 (#201) 2019-03-12 19:19:32 -04:00
Kyle Carberry
7cc7aa51aa Add additional ide-api events 2019-03-12 17:43:53 -04:00
Asher
6c8e513e71 Fill remainder of graceful-fs
It's just fs. Used by fs-extra.
2019-03-12 15:50:41 -05:00
Asher
f7c1ebf667 Fix moving data directory across devices 2019-03-12 15:45:44 -05:00
Asher
ba37a34fa2 Create data directory path before moving old directory there 2019-03-12 15:05:47 -05:00
Asher
e1dc6967ed Fix trash 2019-03-12 14:04:17 -05:00
Asher
3155eb76f5 Allow webview iframe to load styles 2019-03-12 12:55:43 -05:00
Asher
e597d49912 Fix issues with configuration directories
- Move the old data directory if possible.
- Fix extension path to not use a hard-coded path and instead use the
  data directory.
- Create every part of the path during startup.
- Create each path when a connection is made as well in case they are
  deleted while the server is running.
- Create every part of the path before saving settings or writing a file
  using the resource endpoint.
2019-03-12 11:44:56 -05:00
Asher
0a9f5d8eee Pass env as actual env instead of as a flag 2019-03-11 17:50:35 -05:00
Asher
736feaba51 Add proper version to --version 2019-03-11 17:38:11 -05:00
Asher
307aa4ae7f Fix READMEs not loading 2019-03-11 16:20:13 -05:00
Andrey Esin
1df352fe26 Adding minus to flag '-i' (#198) 2019-03-11 13:59:07 -05:00
Asher
8aff206538 Fix some dialog styling issues
- Fix black text caused by 1.32.0 upgrade.
- Fix various alignment and padding issues (a few elements with more
  space below than above).
2019-03-11 11:29:24 -05:00
Anmol Sethi
03c0bde3a9 Another fix for the Dockerfile and build (#188)
* Dockerfile: fix

ENTRYPOINT must be provided in exec form for args
from docker run to be passed correctly.

See https://docs.docker.com/engine/reference/builder/#cmd

* build: fix reuse of vscode repository regression

See #167
2019-03-11 09:47:41 -05:00
Anmol Sethi
a36476df21 Merge pull request #193 from netchampfaris/patch-1
fix: missing word
2019-03-11 01:29:07 -04:00
Faris Ansari
25c46bea32 fix: missing word 2019-03-11 10:27:42 +05:30
Anmol Sethi
60937c604e Merge pull request #179 from codercom/anmol
Update docker oneliner and fix clone task
2019-03-10 11:39:27 -04:00
Anmol Sethi
449d51d24d Dockerfile: use relative path instead of $PWD for CMD
Thanks @ay-b
2019-03-10 11:39:18 -04:00
Anmol Sethi
26edea5098 Update docker oneliner and fix clone task 2019-03-10 11:39:17 -04:00
Anmol Sethi
8527d10033 Merge pull request #172 from korzq/patch-1
Add nginx reverse proxy guide for selfhosted
2019-03-10 11:36:56 -04:00
赵吉彤
4387fdfb9e Clone exact vscode release branch when build task (#167)
* chore: clone exactly release branch when build

* fix: clone params
2019-03-10 10:02:08 -05:00
Khải
41d1be9205 Add -t flag to docker example (#181)
It is quite annoying when Ctrl-C does not stop the container
2019-03-10 08:28:25 -05:00
Anmol Sethi
76e0338d7f Reverts parts of my last PR (#177)
Cannot mount data directory at the moment, we need to make code-server
override all binaries in there instead of reusing and will wait
until the new Dockerfile is pushed before removing the default
entrypoint from README.md
2019-03-09 16:01:53 -07:00
Anmol Sethi
f37533579d Fix build and Dockerfile issues (#176)
* build: fix yarn corruption issues

Closes #100

Thanks @zerdos

* Dockerfile: add default Entrypoint and mount data dir

Closes #170
Closes #78

Thanks @ay-b
2019-03-09 14:51:51 -08:00
Eric Zhu
438808573d Add nginx reverse proxy guide for selfhosted
#80
2019-03-09 14:17:21 -05:00
Michael
c471babc69 Use a timing-safe equality check for passwords (#133) 2019-03-09 09:24:36 -08:00
David Bushong
d7a66e4f15 Open websocket on same path as page (#149)
This is the critical piece to let you serve code-server proxied under a
path.  Otherwise if you proxy e.g. `/editor/` thru to
`http://localhost:8000`, everything works fine except the websocket
connection is still opened to `/`
2019-03-09 09:23:51 -08:00
Forest Hoffman
30d14eeab4 Adhere to XDG base directory spec for dataDir and logDir (#156) 2019-03-09 09:11:30 -08:00
Avelino
e22e2c8b67 Improve github issue template (#162)
- Simplify the user by opening an issue (giving direction)
- Mark issue label
- Markdown lint

Signed-off-by: Avelino <t@avelino.xxx>
2019-03-09 09:07:43 -08:00
Aidan Fitzgerald
ffb75b6801 Fix typo: environemnt -> environment (#159)
Signed-off-by: Aidan Fitzgerald <acf67@cornell.edu>
2019-03-09 09:06:47 -08:00
Asher
cded51f650 Add back web class
Forgot to move this to the new file when upgrading to 1.32.0.
2019-03-08 12:45:20 -06:00
Asher
8bab787804 Move upx compression behind an env flag 2019-03-08 12:01:27 -06:00
Anmol Sethi
3e9d86ee91 ci: enable travis npm cache (#110)
* ci: enable travis npm cache

* ci: upgrade travis version

Thanks @zerdos

* ci: switch to using yarn cache
2019-03-08 09:21:11 -08:00
Philipp Molitor
e418ecf653 Improve .dockerignore (#111)
* impove .dockerignore

* impove .dockerignore
2019-03-08 09:13:25 -08:00
Asher
36c05ed335 Feature/1.32.0 update (#117)
* Update VS Code to 1.32.0

* Update patch

Most changes are moved files, most notably shell.contribution.ts which
is now main.contribution.ts.

Also:
- repl.ts no longer uses isMacintosh
- shell.ts doesn't exist
- added back the commented-out CSP headers

* Use es6 target for bootstrap-fork

* Directly reference cross-env binary

yarn and npm find the binary just fine when running the tasks from the
root but it doesn't work if you run one of those tasks directly from
within those directories.

* Update import paths and bootstrap-fork ignores

* Increase memory limit for building default extensions

* Fix invalid regex in Firefox

* Update startup function

* Fix global.require error

* Update zip extract arguments

* Update travis to minimum required Node version

* Always chmod executable dependencies

Fixes EACCESS errors for users that had the files unpacked before we
added the chmod call.

* Remove unused var declaration
2019-03-08 08:37:03 -08:00
Jeremy Pinnix
5c435a3b6c Update grammar on README (#139) 2019-03-08 08:34:31 -08:00
Semen Zhydenko
9f4212eace inital -> initial (#135) 2019-03-08 08:10:50 -08:00
Anmol Sethi
96175d36ea Handle arch in dockerfile and add PR template (#109)
* repo: add a pull request template

* Dockerfile: include arch when copying

Thanks @yush1ga and @zerdos
2019-03-07 13:48:11 -08:00
Kyle Carberry
c8afb7908e Merge branch 'master' of github.com:codercom/code-server 2019-03-07 21:23:25 +00:00
Kyle Carberry
231cdec7fb Update known issues in readme with better wording 2019-03-07 21:23:16 +00:00
Elizabeth
8e5f288459 Updated current args available for passing in docs (#105) 2019-03-07 13:19:00 -08:00
Anmol Sethi
14f1230bed Merge pull request #98 from nhooyr/docker
Dockerfile: cleanup and add en_US.UTF-8 locale
2019-03-07 14:08:09 -05:00
Anmol Sethi
fba3fe5609 Dockerfile: cleanup and add en_US.UTF-8 locale
Closes #84
Closes #83

Thanks @xMrWhite, @frol and @sr229.
2019-03-07 13:55:13 -05:00
Elizabeth
8e68411174 Update example docker command port bind options (#88)
must use 127.0.0.1 and not `localhost`
2019-03-07 09:31:01 -08:00
Asher
3d654a8df7 Resolve paths
Fixes #19.
2019-03-07 11:25:26 -06:00
Takayosi Amagi
87d2e22a6b Open localhost URL with --open (#28)
* Open localhost URL with --open

* Catch errors on opn

Co-Authored-By: fand <fand@users.noreply.github.com>
2019-03-07 09:23:54 -08:00
Anmol Sethi
17267bd801 Add Dockerfile and some cleanup (#57) 2019-03-06 18:59:43 -08:00
Michael Desantis
ac56fcaafc Remove unused markup from password page (#49) 2019-03-06 18:47:05 -08:00
Michael Desantis
e20b79b5cc Generate secure passwords, fixes issue #26 (#51) 2019-03-06 16:25:44 -08:00
Philipp Gillé
e99f8abc3c Fix spelling error (#46) 2019-03-06 16:21:33 -08:00
Asher
994531d8bb Add task for packaging release (#6)
* Add task for packaging release

* Modify package task to package a single binary

This is so it can be used as part of the build/release script.

* Package release as part of Travis deploy

* Set platform env var

* Add arch env var

* Make version available to the code

* Use tar for Linux and zip for Mac & Windows
2019-03-06 18:15:52 -06:00
Kyle Carberry
8916cb9bb2 Update to vscode 1.31.1 (#45) 2019-03-06 11:10:23 -08:00
Ammar Bandukwala
d4867ca430 Cleanup English in README (#21) 2019-03-06 10:11:27 -08:00
Icebob
3fbdb2e46c Implement #4 - fix password via CLI (#5) 2019-03-06 00:08:43 -08:00
Ammar Bandukwala
1d8da2161f Add value props to readme (#20) 2019-03-05 17:45:21 -08:00
Kyle Carberry
414eb7076f Update element fill to replace file URIs in image tags 2019-03-06 00:06:44 +00:00
Asher
2b3d2933eb Updated binary name in issue templates 2019-03-05 17:23:13 -06:00
Andrew Grekov
14ead1a62f Fixes calculating number of threads for HappyPack (#10) 2019-03-05 15:02:49 -08:00
John McCambridge
3b48c57861 Fix port number in docs (#12) 2019-03-05 13:53:35 -08:00
Colin
c772e920cd Add Discord widget to README (#16) 2019-03-05 13:53:05 -08:00
Ammar Bandukwala
e6aa74c412 Merge pull request #7 from codercom/fix-doc
Clarify that cloud offering is seperate from the open source
2019-03-05 10:52:43 -08:00
Ammar Bandukwala
838c8a6f2c Clarify that the cloud offering is seperate from the open source 2019-03-05 12:51:34 -06:00
166 changed files with 16055 additions and 9324 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
Dockerfile
# Docs
doc/
# GitHub stuff
.github
.gitignore
.travis.yml
LICENSE
README.md
node_modules

3
.github/CODEOWNERS vendored
View File

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

View File

@@ -2,17 +2,21 @@
name: Bug Report
about: Report problems and unexpected behavior.
title: ''
labels: ''
labels: 'bug'
assignees: ''
---
<!-- Please search existing issues to avoid creating duplicates. -->
<!-- All extension-specific issues should be created with the `Extension Bug` template. -->
- `vscode-remote` version: <!-- The version of vscode-remote -->
- `code-server` version: <!-- The version of code-server -->
- OS Version: <!-- OS version, cloud provider, -->
#### Steps to Reproduce
## Description
1.
2.
<!-- Describes the problem here -->
## Steps to Reproduce
1. <!-- step 1: click ... -->
1. <!-- step 2: ... -->

View File

@@ -8,11 +8,15 @@ assignees: ''
<!-- Please search existing issues to avoid creating duplicates. -->
- `vscode-remote` version: <!-- The version of vscode-remote -->
- `code-server` version: <!-- The version of code-server -->
- OS Version: <!-- OS version, cloud provider, -->
- Extension: <!-- Link to extension -->
#### Steps to Reproduce
## Description
1.
2.
<!-- Describes the problem here -->
## Steps to Reproduce
1. <!-- step 1: click ... -->
1. <!-- step 2: ... -->

View File

@@ -2,7 +2,7 @@
name: Feature Request
about: Suggest an idea for this project.
title: ''
labels: ''
labels: 'feature'
assignees: ''
---

View File

@@ -2,16 +2,16 @@
name: Question
about: Ask a question.
title: ''
labels: ''
labels: 'question'
assignees: ''
---
<!-- Please search existing issues to avoid creating duplicates. -->
#### Description
## Description
<!-- A description of the the question. -->
#### Related Issues
## Related Issues
<!-- Any issues related to your question. -->

6
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,6 @@
<!-- Please answer these questions before submitting your PR. Thanks! -->
### Describe in detail the problem you had and how this PR fixes it
### Is there an open issue you can link to?

5
.gitignore vendored
View File

@@ -1,5 +1,8 @@
lib
/lib
node_modules
dist
out
.DS_Store
release
.vscode
.cache

1
.node-version Normal file
View File

@@ -0,0 +1 @@
10.15.1

View File

@@ -1,33 +1,50 @@
language: node_js
node_js:
- 8.9.3
- 10.15.1
services:
- docker
matrix:
include:
- os: linux
dist: ubuntu
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
- npm install -g yarn@1.12.3
script:
- scripts/build.sh
before_deploy:
- export TRAVIS_TAG="1.31.0-$TRAVIS_BUILD_NUMBER"
- echo "$TRAVIS_TAG" "$TRAVIS_COMMIT"
- echo "$VERSION" "$TRAVIS_COMMIT"
- git config --local user.name "$USER_NAME"
- git config --local user.email "$USER_EMAIL"
- git tag "$TRAVIS_TAG" "$TRAVIS_COMMIT"
- git tag "$VERSION" "$TRAVIS_COMMIT"
deploy:
provider: releases
file_glob: true
draft: true
tag_name: $TRAVIS_TAG
target_commitish: $TRAVIS_COMMIT
name: $TRAVIS_TAG
tag_name: "$VERSION"
target_commitish: "$TRAVIS_COMMIT"
name: "$VERSION"
skip_cleanup: true
api_key:
secure: YL/x24KjYjgYXPcJWk3FV7FGxI79Mh6gBECQEcdlf3fkLEoKFVgzHBoUNWrFPzyR4tgLyWNAgcpD9Lkme1TRWTom7UPjXcwMNyLcLa+uec7ciSAnYD9ntLTpiCuPDD1u0LtRGclSi/EHQ+F8YVq+HZJpXTsJeAmOmihma3GVbGKSZr+BRum+0YZSG4w+o4TOlYzw/4bLWS52MogZcwpjd+hemBbgXLuGU2ziKv2vEKCZFbEeA16II4x1WLI4mutDdCeh7+3aLzGLwDa49NxtsVYNjyNFF75JhCTCNA55e2YMiLz9Uq69IXe/mi5F7xUaFfhIqqLNyKBnKeEOzu3dYnc+8n3LjnQ+00PmkF05nx9kBn3UfV1kwQGh6QbyDmTtBP07rtUMyI14aeQqHjxsaVRdMnwj9Q2DjXRr8UDqESZF0rmK3pHCXS2fBhIzLE8tLVW5Heiba2pQRFMHMZW+KBE97FzcFh7is90Ait3T8enfcd/PWFPYoBejDAdjwxwOkezh5N5ZkYquEfDYuWrFi6zRFCktsruaAcA+xGtTf9oilBBzUqu8Ie+YFWH5me83xakcblJWdaW/D2rLJAJH3m6LFm8lBqyUgDX5t/etob6CpDuYHu5D1J3XINOj/+aLAcadq6qlh70PMZS3zYffUu3JlzaD2amlSHIT8b5YXFc=
file: packages/server/cli-*
file:
- release/*.tar.gz
- release/*.zip
on:
repo: codercom/code-server
repo: cdr/code-server
branch: master
cache:
yarn: true
timeout: 1000
directories:
- .cache

53
Dockerfile Normal file
View File

@@ -0,0 +1,53 @@
FROM node:10.15.1
# Install VS Code's deps. These are the only two it seems we need.
RUN apt-get update && apt-get install -y \
libxkbfile-dev \
libsecret-1-dev
# Ensure latest yarn.
RUN npm install -g yarn@1.13
WORKDIR /src
COPY . .
# In the future, we can use https://github.com/yarnpkg/rfcs/pull/53 to make yarn use the node_modules
# directly which should be fast as it is slow because it populates its own cache every time.
RUN yarn && NODE_ENV=production yarn task build:server:binary
# We deploy with ubuntu so that devs have a familiar environment.
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y \
openssl \
net-tools \
git \
locales \
sudo \
dumb-init \
vim \
curl \
wget
RUN locale-gen en_US.UTF-8
# We unfortunately cannot use update-locale because docker will not use the env variables
# configured in /etc/default/locale so we need to set it manually.
ENV LC_ALL=en_US.UTF-8
RUN adduser --gecos '' --disabled-password coder && \
echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd
USER coder
# We create first instead of just using WORKDIR as when WORKDIR creates, the user is root.
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.
# 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
EXPOSE 8443
ENTRYPOINT ["dumb-init", "code-server"]

View File

@@ -1,22 +1,43 @@
# 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)](#)
[!["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 running on a remote server, accessible through the browser.
`code-server` is [VS Code](https://github.com/Microsoft/vscode) running on a remote server, accessible through the browser.
Try it out:
```bash
docker run -it -p 127.0.0.1:8443:8443 -v "${PWD}:/home/coder/project" codercom/code-server --allow-http --no-auth
```
- Code on your Chromebook, tablet, and laptop with a consistent dev environment.
- If you have a Windows or Mac workstation, more easily develop for Linux.
- Take advantage of large cloud servers to speed up tests, compilations, downloads, and more.
- Preserve battery life when you're on the go.
- All intensive computation runs on your server.
- You're no longer running excess instances of Chrome.
![Screenshot](/doc/assets/ide.png)
## Getting Started
[Try `code-server` now](https://coder.com/signup) for free at coder.com.
### Run over SSH
1. [Download a binary](https://github.com/codercom/code-server/releases) (Linux and OSX supported. Windows coming soon)
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/cdr/code-server/releases) (Linux and OS X supported. Windows coming soon)
2. Start the binary with the project directory as the first argument
```
code-server <inital directory to open>
code-server <initial directory to open>
```
> You will be prompted to enter the password shown in the CLI
`code-server` should now be running at https://localhost:8443.
@@ -25,7 +46,7 @@
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).
@@ -33,17 +54,28 @@ How to [secure your setup](/doc/security/ssl.md).
### Known Issues
- Debugging extensions doesnt work.
- Creating custom VS Code extensions and debugging them doesn't work.
### Future
- **Stay up to date!** Get notified about new releases of code-server.
![Screenshot](/doc/assets/release.gif)
- Windows support.
- Electron and ChromeOS applications to bridge the gap between local<->remote.
- Electron and Chrome OS applications to bridge the gap between local<->remote.
- Run VS Code unit tests against our builds to ensure features work as expected.
### Extensions
At the moment we can't use the official VSCode Marketplace. We've created a custom extension marketplace focused around open-sourced extensions. However, if you have access to the `.vsix` file, you can manually install the extension.
## Telemetry
Use the `--disable-telemetry` flag or set `DISABLE_TELEMETRY=true` to disable tracking ENTIRELY.
We use data collected to improve code-server.
## Contributing
Guides on setup for development will be coming soon. :)
Development guides are coming soon.
## License
@@ -51,7 +83,7 @@ Guides on setup for development will be coming soon. :)
## Enterprise
Visit [our enterprise page](https://coder.com/enterprise) for more information on our enterprise offering.
Visit [our enterprise page](https://coder.com/enterprise) for more information about our enterprise offering.
## Commercialization

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,23 +1,34 @@
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";
import * as tar from "tar";
const isWin = os.platform() === "win32";
const libPath = path.join(__dirname, "../lib");
const vscodePath = path.join(libPath, "vscode");
const defaultExtensionsPath = path.join(libPath, "extensions");
const pkgsPath = path.join(__dirname, "../packages");
const defaultExtensionsPath = path.join(libPath, "VSCode-linux-x64/resources/app/extensions");
const vscodeVersion = 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 copyForDefaultExtensions();
await Promise.all([
buildBootstrapFork(),
buildWeb(),
buildDefaultExtensions(),
buildServerBundle(),
buildAppBrowser(),
]);
@@ -32,48 +43,12 @@ const buildServerBinaryPackage = register("build:server:binary:package", async (
throw new Error("Cannot build binary without server bundle built");
}
await buildServerBinaryCopy();
await dependencyNexeBinary();
const resp = await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build:nexe"]);
const resp = await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build:binary"]);
if (resp.exitCode !== 0) {
throw new Error(`Failed to package binary: ${resp.stderr}`);
}
});
const dependencyNexeBinary = register("dependency:nexe", async (runner) => {
if (os.platform() === "linux") {
const nexeDir = path.join(os.homedir(), ".nexe");
const targetBinaryName = `${os.platform()}-${os.arch()}-${process.version.substr(1)}`;
const targetBinaryPath = path.join(nexeDir, targetBinaryName);
if (!fs.existsSync(targetBinaryPath)) {
/**
* We create a binary with nexe
* so we can compress it
*/
fse.mkdirpSync(nexeDir);
runner.cwd = nexeDir;
await runner.execute("wget", [`https://github.com/nexe/nexe/releases/download/v3.0.0-beta.15/${targetBinaryName}`]);
await runner.execute("chmod", ["+x", targetBinaryPath]);
}
if (fs.statSync(targetBinaryPath).size >= 20000000) {
// Compress w/ upx
const upxFolder = path.join(os.tmpdir(), "upx");
const upxBinary = path.join(upxFolder, "upx");
if (!fs.existsSync(upxBinary)) {
fse.mkdirpSync(upxFolder);
runner.cwd = upxFolder;
const upxExtract = await runner.execute("bash", ["-c", "curl -L https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz | tar xJ --strip-components=1"]);
if (upxExtract.exitCode !== 0) {
throw new Error(`Failed to extract upx: ${upxExtract.stderr}`);
}
}
if (!fs.existsSync(upxBinary)) {
throw new Error("Not sure how, but the UPX binary does not exist");
}
await runner.execute(upxBinary, [targetBinaryPath]);
}
}
});
const buildServerBinaryCopy = register("build:server:binary:copy", async (runner) => {
const cliPath = path.join(pkgsPath, "server");
const cliBuildPath = path.join(cliPath, "build");
@@ -82,19 +57,11 @@ const buildServerBinaryCopy = register("build:server:binary:copy", async (runner
const bootstrapForkPath = path.join(pkgsPath, "vscode", "out", "bootstrap-fork.js");
const webOutputPath = path.join(pkgsPath, "web", "out");
const browserAppOutputPath = path.join(pkgsPath, "app", "browser", "out");
const nodePtyModule = path.join(pkgsPath, "protocol", "node_modules", "node-pty-prebuilt", "build", "Release", "pty.node");
const spdlogModule = path.join(pkgsPath, "protocol", "node_modules", "spdlog", "build", "Release", "spdlog.node");
let ripgrepPath = path.join(pkgsPath, "..", "lib", "vscode", "node_modules", "vscode-ripgrep", "bin", "rg");
if (isWin) {
ripgrepPath += ".exe";
}
if (!fs.existsSync(nodePtyModule)) {
throw new Error("Could not find pty.node. Ensure all packages have been installed");
}
if (!fs.existsSync(spdlogModule)) {
throw new Error("Could not find spdlog.node. Ensure all packages have been installed");
}
if (!fs.existsSync(webOutputPath)) {
throw new Error("Web bundle must be built");
}
@@ -109,24 +76,22 @@ const buildServerBinaryCopy = register("build:server:binary:copy", async (runner
}
fse.copySync(defaultExtensionsPath, path.join(cliBuildPath, "extensions"));
fs.writeFileSync(path.join(cliBuildPath, "bootstrap-fork.js.gz"), zlib.gzipSync(fs.readFileSync(bootstrapForkPath)));
const cpDir = (dir: string, subdir: "auth" | "unauth", rootPath: string): void => {
const cpDir = (dir: string, rootPath: string, subdir?: "login"): void => {
const stat = fs.statSync(dir);
if (stat.isDirectory()) {
const paths = fs.readdirSync(dir);
paths.forEach((p) => cpDir(path.join(dir, p), subdir, rootPath));
paths.forEach((p) => cpDir(path.join(dir, p), rootPath, subdir));
} else if (stat.isFile()) {
const newPath = path.join(cliBuildPath, "web", subdir, path.relative(rootPath, dir));
const newPath = path.join(cliBuildPath, "web", subdir || "", path.relative(rootPath, dir));
fse.mkdirpSync(path.dirname(newPath));
fs.writeFileSync(newPath + ".gz", zlib.gzipSync(fs.readFileSync(dir)));
} else {
// Nothing
}
};
cpDir(webOutputPath, "auth", webOutputPath);
cpDir(browserAppOutputPath, "unauth", browserAppOutputPath);
cpDir(webOutputPath, webOutputPath);
cpDir(browserAppOutputPath, browserAppOutputPath, "login");
fse.mkdirpSync(path.join(cliBuildPath, "dependencies"));
fse.copySync(nodePtyModule, path.join(cliBuildPath, "dependencies", "pty.node"));
fse.copySync(spdlogModule, path.join(cliBuildPath, "dependencies", "spdlog.node"));
fse.copySync(ripgrepPath, path.join(cliBuildPath, "dependencies", "rg"));
});
@@ -164,93 +129,50 @@ const buildWeb = register("build:web", async (runner) => {
await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build"]);
});
const extDirPath = path.join("lib", "vscode-default-extensions");
const copyForDefaultExtensions = register("build:copy-vscode", async (runner) => {
if (!fs.existsSync(defaultExtensionsPath)) {
await ensureClean();
await ensureInstalled();
await new Promise((resolve, reject): void => {
fse.remove(extDirPath, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
await new Promise((resolve, reject): void => {
fse.copy(vscodePath, extDirPath, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
}
});
const buildDefaultExtensions = register("build:default-extensions", async (runner) => {
if (!fs.existsSync(defaultExtensionsPath)) {
await copyForDefaultExtensions();
runner.cwd = extDirPath;
const resp = await runner.execute(isWin ? "npx.cmd" : "npx", [isWin ? "gulp.cmd" : "gulp", "vscode-linux-x64"]);
if (resp.exitCode !== 0) {
throw new Error(`Failed to build default extensions: ${resp.stderr}`);
}
}
});
const ensureInstalled = register("vscode:install", async (runner) => {
await ensureCloned();
runner.cwd = libPath;
runner.cwd = vscodePath;
const install = await runner.execute(isWin ? "yarn.cmd" : "yarn", []);
if (install.exitCode !== 0) {
throw new Error(`Failed to install vscode dependencies: ${install.stderr}`);
}
});
if (fs.existsSync(vscodePath) && fs.existsSync(defaultExtensionsPath)) {
const pkgVersion = JSON.parse(fs.readFileSync(path.join(vscodePath, "package.json")).toString("utf8")).version;
if (pkgVersion === vscodeVersion) {
runner.cwd = vscodePath;
const ensureCloned = register("vscode:clone", async (runner) => {
if (fs.existsSync(vscodePath)) {
await ensureClean();
} else {
fse.mkdirpSync(libPath);
runner.cwd = libPath;
const clone = await runner.execute("git", ["clone", "https://github.com/microsoft/vscode"]);
if (clone.exitCode !== 0) {
throw new Error(`Failed to clone: ${clone.exitCode}`);
const reset = await runner.execute("git", ["reset", "--hard"]);
if (reset.exitCode !== 0) {
throw new Error(`Failed to clean git repository: ${reset.stderr}`);
}
return;
}
}
runner.cwd = vscodePath;
const checkout = await runner.execute("git", ["checkout", "tags/1.31.0"]);
if (checkout.exitCode !== 0) {
throw new Error(`Failed to checkout: ${checkout.stderr}`);
}
});
fse.removeSync(libPath);
fse.mkdirpSync(libPath);
const ensureClean = register("vscode:clean", async (runner) => {
runner.cwd = vscodePath;
await new Promise<void>((resolve, reject): void => {
https.get(vsSourceUrl, (res) => {
if (res.statusCode !== 200) {
return reject(res.statusMessage);
}
const status = await runner.execute("git", ["status", "--porcelain"]);
if (status.stdout.trim() !== "") {
const clean = await runner.execute("git", ["clean", "-f", "-d", "-X"]);
if (clean.exitCode !== 0) {
throw new Error(`Failed to clean git repository: ${clean.stderr}`);
}
const removeUnstaged = await runner.execute("git", ["checkout", "--", "."]);
if (removeUnstaged.exitCode !== 0) {
throw new Error(`Failed to remove unstaged files: ${removeUnstaged.stderr}`);
}
}
res.pipe(tar.x({
C: libPath,
}).on("finish", () => {
resolve();
}).on("error", (err: Error) => {
reject(err);
}));
}).on("error", (err) => {
reject(err);
});
});
});
const ensurePatched = register("vscode:patch", async (runner) => {
if (!fs.existsSync(vscodePath)) {
throw new Error("vscode must be cloned to patch");
}
await ensureClean();
await ensureInstalled();
runner.cwd = vscodePath;
const patchPath = path.join(__dirname, "../scripts/vscode.patch");
@@ -260,4 +182,30 @@ const ensurePatched = register("vscode:patch", async (runner) => {
}
});
register("package", async (runner, releaseTag) => {
if (!releaseTag) {
throw new Error("Please specify the release tag.");
}
const releasePath = path.resolve(__dirname, "../release");
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-${platform()}-${os.arch()}`);
const binaryDestination = path.join(archiveDir, "code-server");
fse.copySync(binaryPath, binaryDestination);
fs.chmodSync(binaryDestination, "755");
["README.md", "LICENSE"].forEach((fileName) => {
fse.copySync(path.resolve(__dirname, `../${fileName}`), path.join(archiveDir, fileName));
});
runner.cwd = releasePath;
await (os.platform() === "linux"
? runner.execute("tar", ["-cvzf", `${archiveName}.tar.gz`, `${archiveName}`])
: runner.execute("zip", ["-r", `${archiveName}.zip`, `${archiveName}`]));
});
run();

View File

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

View File

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

View File

@@ -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.
---
@@ -11,9 +11,9 @@ If you're just starting out, we recommend [installing code-server locally](../..
### Use the AWS wizard
- Click **Launch Instance** from your [EC2 dashboard](https://console.aws.amazon.com/ec2/v2/home).
- Select the Ubuntu Server 16.04 LTS (HVM), SSD Volume Type (`ami-0f9cf087c1f27d9b1)` at this time of writing)
- Select the Ubuntu Server 18.04 LTS (HVM), SSD Volume Type
- Select an appropriate instance size (we recommend t2.medium/large, depending on team size and number of repositories/languages enabled), then **Next: Configure Instance Details**
- Select **Next: ...** until you get to the **Configure Security Group** page, then add the default **HTTP** rule (port range "80", source "0.0.0.0/0, ::/0")
- Select **Next: ...** until you get to the **Configure Security Group** page, then add a **Custom TCP Rule** rule with port range set to `8443` and source set to "Anywhere"
> Rules with source of 0.0.0.0/0 allow all IP addresses to access your instance. We recommend setting [security group rules](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-network-security.html?icmpid=docs_ec2_console) to allow access from known IP addresses only.
- Click **Launch**
- You will be prompted to create a key pair
@@ -28,30 +28,39 @@ If you're just starting out, we recommend [installing code-server locally](../..
- In the description of your EC2 instance copy the public DNS (iPv4) address using the copy to clipboard button
- Open a terminal on your computer and use the following command to SSH into your EC2 instance
```
ssh i "path/to/your/keypair.pem" ubuntu@(paste the public DNS here)
ssh -i "path/to/your/keypair.pem" ubuntu@(paste the public DNS here)
```
>example: `ssh -i "/Users/John/Downloads/TestInstance.pem" ubuntu@ec2-3-45-678-910.compute-1.amazonaws.co`
- You should see a prompt for your EC2 instance like so<img src="../../assets/aws_ubuntu.png">
- At this point it is time to download the `code-server` binary. We will of course want the linux version. Make sure you copy the link for the latest linux version on our [releases page](https://github.com/codercom/code-server/releases)
- With the URL in the clipboard, run:
- At this point it is time to download the `code-server` binary. We will of course want the linux version.
- Find the latest Linux release from this URL:
```
wget https://github.com/codercom/code-server/releases/download/0.1.4/code-server-linux
https://github.com/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/cdr/code-server/releases/download/{version}/code-server-{version}-linux-x64.tar.gz
```
- Extract the downloaded tar.gz file with this command, for example:
```
tar -xvzf code-server-{version}-linux-x64.tar.gz
```
- Navigate to extracted directory with this command:
```
cd code-server-{version}-linux-x64
```
- If you run into any permission errors, make the binary executable by running:
```
chmod +x code-server-linux
chmod +x code-server
```
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md)
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../../security/ssl.md)
- Finally, run
```
sudo ./code-server-linux -p 80
./code-server
```
- When you visit the public IP for your AWS instance, you will be greeted with this page. Code-server is using a self-signed SSL certificate for easy setup. To proceed to the IDE, click **"Advanced"**<img src ="../../assets/chrome_warning.png">
- Then click **"proceed anyway"**<img src="../../assets/chrome_confirm.png">
- Open your browser and visit `https://$public_ip:8443/` (where `$public_ip` is your AWS instance's public IP address). You will be greeted with a page similar to the following screenshot. Code-server is using a self-signed SSL certificate for easy setup. In Chrome/Chromium, click **"Advanced"** then click **"proceed anyway"**. In Firefox, click **Advanced**, then **Add Exception**, then finally **Confirm Security Exception**.<img src ="../../assets/chrome_warning.png">
> 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
> 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,23 +15,34 @@ 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
- In the shell run the below command with the URL from your clipboard
- 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:
```
wget https://github.com/codercom/code-server/releases/download/0.1.4/code-server-linux
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/cdr/code-server/releases/download/{version}/code-server-{version}-linux-x64.tar.gz
```
- Extract the downloaded tar.gz file with this command, for example:
```
tar -xvzf code-server-{version}-linux-x64.tar.gz
```
- Navigate to extracted directory with this command:
```
cd code-server-{version}-linux-x64
```
- If you run into any permission errors when attempting to run the binary:
```
chmod +x code-server-linux
chmod +x code-server
```
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md)
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../../security/ssl.md)
- Finally start the code-server
```
sudo ./code-server-linux -p80
./code-server
```
> For instructions on how to keep the server running after you end your SSH session please checkout [how to use systemd](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) to start linux based services if they are killed
- When you visit the public IP for your Digital Ocean instance, you will be greeted with this page. Code-server is using a self-signed SSL certificate for easy setup. To proceed to the IDE, click **"Advanced"**<img src ="../../assets/chrome_warning.png">
- Then click **"proceed anyway"**<img src="../../assets/chrome_confirm.png">
- Open your browser and visit `https://$public_ip:8443/` (where `$public_ip` is your Digital Ocean instance's public IP address). You will be greeted with a page similar to the following screenshot. Code-server is using a self-signed SSL certificate for easy setup. In Chrome/Chromium, click **"Advanced"** then click **"proceed anyway"**. In Firefox, click **Advanced**, then **Add Exception**, then finally **Confirm Security Exception**.<img src ="../../assets/chrome_warning.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.
---
@@ -12,35 +12,55 @@ If you're just starting out, we recommend [installing code-server locally](../..
- [Open your Google Cloud console](https://console.cloud.google.com/compute/instances) to create a new VM instance and click **Create Instance**
- Choose an appropriate machine type (we recommend 2 vCPU and 7.5 GB RAM, more depending on team size and number of repositories/languages enabled)
- Choose Ubuntu 16.04 LTS as your boot disk
- Check the boxes for **Allow HTTP traffic** and **Allow HTTPS traffic** in the **Firewall** section
- Create your VM, and **take note** of it's public IP address.
- Copy the link to download the latest Linux binary from our [releases page](https://github.com/codercom/code-server/releases)
- Expand the "Management, security, disks, networking, sole tenancy" section, go to the "Networking" tab, then under network tags add "code-server"
- Create your VM, and **take note** of its public IP address.
- Visit "VPC network" in the console and go to "Firewall rules". Create a new firewall rule called "http-8443". Under "Target tags" add "code-server", and under "Protocols and ports" tick "Specified protocols and ports" and "tcp". Beside "tcp", add "8443", then create the rule.
- Copy the link to download the latest Linux binary from our [releases page](https://github.com/cdr/code-server/releases)
---
## Final Steps
1. SSH into your Google Cloud VM
- SSH into your Google Cloud VM
```
gcloud compute ssh --zone [region] [instance name]
```
2. Download the binary using the link we copied to clipboard
- Find the latest Linux release from this URL:
```
wget https://github.com/codercom/code-server/releases/download/0.1.4/code-server-linux
https://github.com/cdr/code-server/releases/latest
```
3. Make the binary executable if you run into any errors regarding permission:
- 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/cdr/code-server/releases/download/{version}/code-server-{version}-linux-x64.tar.gz
```
- Extract the downloaded tar.gz file with this command, for example:
```
tar -xvzf code-server-{version}-linux-x64.tar.gz
```
- Navigate to extracted directory with this command:
```
cd code-server-{version}-linux-x64
```
- Make the binary executable if you run into any errors regarding permission:
```
chmod +x code-server
```
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../../security/ssl.md)
- Start the code-server
```
chmod +x code-server-linux
./code-server
```
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md)
4. Start the code-server
```
sudo ./code-server-linux -p 80
```
- Open your browser and visit `https://$public_ip:8443/` (where `$public_ip` is your Compute Engine instance's public IP address). You will be greeted with a page similar to the following screenshot. Code-server is using a self-signed SSL certificate for easy setup. In Chrome/Chromium, click **"Advanced"** then click **"proceed anyway"**. In Firefox, click **Advanced**, then **Add Exception**, then finally **Confirm Security Exception**.<img src ="../../assets/chrome_warning.png">
> For instructions on how to keep the server running after you end your SSH session please checkout [how to use systemd](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) to start linux based services if they are killed
5. Access code-server from the public IP of your Google Cloud instance we noted earlier in your browser.
> example: 32.32.32.234
6. You will be greeted with this page. Code-server is using a self-signed SSL certificate for easy setup. To proceed to the IDE, click **"Advanced"**<img src ="../../assets/chrome_warning.png">
7. Then click **"proceed anyway"**<img src="../../assets/chrome_confirm.png">
---
> 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).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 137 KiB

BIN
doc/assets/cros.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 984 KiB

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 18 KiB

BIN
doc/assets/release.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -1,6 +1,6 @@
# Generate a self-signed certificate 🔒
code-server has the ability to secure your connection between client and server using SSL/TSL certificates. By default, the server will start with an unencrypted connection. We recommend Self-signed TLS/SSL certificates for personal of code-server or within an organization.
code-server has the ability to secure your connection between client and server using SSL/TSL certificates. By default, the server will start with an unencrypted connection. We recommend Self-signed TLS/SSL certificates for personal use of code-server or within an organization.
This guide will show you how to create a self-signed certificate and start code-server using your certificate/key.

View File

@@ -0,0 +1,53 @@
# Installng code-server in your ChromiumOS/ChromeOS/CloudReady machine
This guide will show you how to install code-server into your CrOS machine.
## Using Crostini
One of the easier ways to run code-server is via [Crostini](https://www.aboutchromebooks.com/tag/project-crostini/), the Linux apps support feature in CrOS. Make sure you have enough RAM, HDD space and your CPU has VT-x/ AMD-V support. If your chromebook has this, then you are qualified to use Crostini.
If you are running R69, you might want to enable this on [Chrome Flags](chrome://flags/#enable-experimental-crostini-ui). If you run R72, however, this is already enabled for you.
After checking your prerequisites, follow the steps in [the self-host install guide](index.md) on installing code-server. Once done, make sure code-server works by running it. After running it, simply go to `penguin.linux.test:8443` to access code-server. Now you should be greeted with this screen. If you did, congratulations, you have installed code-server in your Chromebook!
![code-server on Chromebook](../assets/cros.png)
Alternatively, if you ran code-server in another container and you need the IP for that specific container, simply go to Termina's shell via `crosh` and type `vsh termina`.
```bash
Loading extra module: /usr/share/crosh/dev.d/50-crosh.sh
Welcome to crosh, the Chrome OS developer shell.
If you got here by mistake, don't panic! Just close this tab and carry on.
Type 'help' for a list of commands.
If you want to customize the look/behavior, you can use the options page.
Load it by using the Ctrl+Shift+P keyboard shortcut.
crosh> vsh termina
(termina) chronos@localhost ~ $
```
While in termina, run `lxc list`. It should output the list of running containers.
```bash
(termina) chronos@localhost ~ $ lxc list
+---------+---------+-----------------------+------+------------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+---------+---------+-----------------------+------+------------+-----------+
| penguin | RUNNING | 100.115.92.199 (eth0) | | PERSISTENT | 0 |
+---------+---------+-----------------------+------+------------+-----------+
(termina) chronos@localhost ~ $
```
For this example, we show the default `penguin` container, which is exposed on `eth0` at 100.115.92.199. Simply enter the IP of the container where the code-server runs to Chrome.
## Using Crouton
[Crouton](https://github.com/dnschneid/crouton) is one of the old ways to get a running full Linux via `chroot` on a Chromebook. To use crouton, enable developer mode and go to `crosh`. This time, run `shell`, which should drop you to `bash`.
Make sure you downloaded `crouton`, if so, go ahead and run it under `~/Downloads`. After installing your chroot container via crouton, go ahead and enter `enter-chroot` to enter your container.
Follow the instructions set in [the self-host install guide](index.md) to install code-server. After that is done, run `code-server` and verify it works by going to `localhost:8443`.
> At this point in writing, `localhost` seems to work in this method. However, the author is not sure if it applies still to newer Chromebooks.

View File

@@ -2,13 +2,13 @@
[code-server](https://coder.com) is used by developers at Azure, Google, Reddit, and more to give them access to VS Code in the browser.
## Quickstart guide
## Quickstart Guide
> NOTE: If you get stuck or need help, [file an issue](https://github.com/codercom/code-server/issues/new?&title=Improve+self-hosted+quickstart+guide), [tweet (@coderhq)](https://twitter.com/coderhq) or [email](mailto:support@coder.com?subject=Self-hosted%20quickstart%20guide).
> 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)
This document pertains to Coder specific implementations of VS Code. For documentation on how to use VS Code itself, please refer to the official [documentation for VS Code](https://code.visualstudio.com/docs)
It takes just a few minutes to get your own self-hosted server running. If you've got a machine running macOS, Windows, or Linux, you're ready to start the binary which listens on port `8080` by default.
It takes just a few minutes to get your own self-hosted server running. If you've got a machine running macOS, Windows, or Linux, you're ready to start the binary which listens on port `8443` by default.
<!--
DO NOT CHANGE THIS TO A CODEBLOCK.
@@ -17,16 +17,15 @@ 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:8080`
4. In your browser navigate to `localhost:8443`
5. Paste the password from the cli into the login window<img src="../assets/server-password-modal.png">
> NOTE: Be careful with your password as sharing it will grant those users access to your server's file system
### Things to know
- When you visit the IP for your code-server, you will be greeted with this page. Code-server is using a self-signed SSL certificate for easy setup. To proceed to the IDE, click **"Advanced"**<img src ="../assets/chrome_warning.png">
- Then click **"proceed anyway"**<img src="../assets/chrome_confirm.png">
### Things To Know
- When you visit the IP for your code-server instance, you will be greeted with a page similar to the following screenshot. Code-server is using a self-signed SSL certificate for easy setup. In Chrome/Chromium, click **"Advanced"** then click **"proceed anyway"**. In Firefox, click **Advanced**, then **Add Exception**, then finally **Confirm Security Exception**.<img src ="../../assets/chrome_warning.png">
## Usage
<pre class="pre-wrap"><code>code-server<span class="virtual-br"></span> --help</code></pre>
@@ -34,42 +33,90 @@ It takes just a few minutes to get your own self-hosted server running. If you'v
code-server can be ran with a number of arguments to customize your working directory, host, port, and SSL certificate.
```
USAGE
$ code-server [WORKDIR]
Usage: code-server [options]
ARGUMENTS
WORKDIR [default: (directory to binary)] Specify working dir
Run VS Code on a remote server.
OPTIONS
-d, --data-dir=data-dir
-h, --host=host [default: 0.0.0.0]
-o, --open Open in browser on startup
-p, --port=port [default: 8080] Port to bind on
-v, --version show CLI version
--cert=cert
--cert-key=cert-key
--help show CLI help
Options:
-V, --version output the version number
--cert <value>
--cert-key <value>
-e, --extensions-dir <dir> Set the root path for extensions.
-d --user-data-dir <dir> Specifies the directory that user data is kept in, useful when running as root.
--data-dir <value> DEPRECATED: Use '--user-data-dir' instead. Customize where user-data is stored.
-h, --host <value> Customize the hostname. (default: "0.0.0.0")
-o, --open Open in the browser on startup.
-p, --port <number> Port to bind on. (default: 8443)
-N, --no-auth Start without requiring authentication.
-H, --allow-http Allow http connections.
-P, --password <value> Specify a password for authentication.
--disable-telemetry Disables ALL telemetry.
--help output usage information
```
### 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
### Data Directory
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 it's address. This can be changed by using `code-server -h` or `code-server --host=` followed by the address you want to use.
By default, code-server will use `0.0.0.0` as its address. This can be changed by using `code-server -h` or `code-server --host=` followed by the address you want to use.
> Example: `code-server -h 127.0.0.1`
### Open
You can have the server automatically open the VS Code in your browser on startup by using the `code server -o` or `code-server --open` flags
You can have the server automatically open the VS Code in your browser on startup by using the `code-server -o` or `code-server --open` flags
### Port
By default, code-server will use `8080` as it's port. This can be changed by using `code-server -p` or `code-server --port=` followed by the port you want to use.
### Port
By default, code-server will use `8443` as its port. This can be changed by using `code-server -p` or `code-server --port=` followed by the port you want to use.
> Example: `code-server -p 9000`
### Telemetry
Disable all telemetry with `code-server --disable-telemetry`.
### Cert and Cert Key
To encrypt the traffic between the browser and server use `code-server --cert=` followed by the path to your `.cer` file. Additionally, you can use certificate keys with `code-server --cert-key` followed by the path to your `.key` file.
> Example (certificate and key): `code-server --cert /etc/letsencrypt/live/example.com/fullchain.cer --cert-key /etc/letsencrypt/live/example.com/fullchain.key`
> Example (if you are using Letsencrypt or similar): `code-server --cert /etc/letsencrypt/live/example.com/fullchain.pem --cert-key /etc/letsencrypt/live/example.com/privkey.key`
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md)
### Nginx Reverse Proxy
Nginx is for reverse proxy. Below is a virtual host example that works with code-server. Please also pass --allow-http. You can also use certbot by EFF to get a ssl certificates for free.
```
server {
listen 80;
listen [::]:80;
server_name code.example.com code.example.org;
location / {
proxy_pass http://localhost:8443/;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
proxy_set_header Accept-Encoding gzip;
}
}
```
### Apache Reverse Proxy
Example of https virtualhost configuration for Apache as a reverse proxy. Please also pass --allow-http on code-server startup to allow the proxy to connect.
```
<VirtualHost *:80>
ServerName code.example.com
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) ws://localhost:8443/$1 [P,L]
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /(.*) http://localhost:8443/$1 [P,L]
ProxyRequests off
RequestHeader set X-Forwarded-Proto https
RequestHeader set X-Forwarded-Port 443
ProxyPass / http://localhost:8443/ nocanon
ProxyPassReverse / http://localhost:8443/
</VirtualHost>
```
*Important:* For more details about Apache reverse proxy configuration checkout the [documentation](https://httpd.apache.org/docs/current/mod/mod_proxy.html) - especially the [Securing your Server](https://httpd.apache.org/docs/current/mod/mod_proxy.html#access) section
### Help
Use `code-server -h` or `code-server --help` to view the usage for the cli. This is also shown at the beginning of this section.
Use `code-server --help` to view the usage for the CLI. This is also shown at the beginning of this section.

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.",
@@ -15,7 +15,9 @@
"devDependencies": {
"@types/fs-extra": "^5.0.4",
"@types/node": "^10.12.18",
"@types/tar": "^4.0.0",
"@types/trash": "^4.3.1",
"cache-loader": "^2.0.1",
"cross-env": "^5.2.0",
"crypto-browserify": "^3.12.0",
"css-loader": "^2.1.0",
@@ -34,24 +36,35 @@
"sass-loader": "^7.1.0",
"string-replace-loader": "^2.1.1",
"style-loader": "^0.23.1",
"tar": "^4.4.8",
"terser-webpack-plugin": "^1.2.3",
"ts-loader": "^5.3.3",
"ts-node": "^7.0.1",
"tsconfig-paths": "^3.8.0",
"tslib": "^1.9.3",
"tslint": "^5.12.1",
"typescript": "^3.2.2",
"typescript-tslint-plugin": "^0.2.1",
"uglifyjs-webpack-plugin": "^2.1.1",
"url-loader": "^1.1.2",
"util": "^0.11.1",
"webpack": "^4.28.4",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.2.1",
"webpack-dev-middleware": "^3.5.0",
"webpack-dev-server": "^3.1.14",
"webpack-hot-middleware": "^2.24.3",
"webpack-pwa-manifest": "^4.0.0",
"workbox-webpack-plugin": "^4.1.0",
"write-file-webpack-plugin": "^4.5.0"
},
"resolutions": {
"bindings": "1.3.0"
},
"dependencies": {
"node-loader": "^0.6.0",
"trash": "^4.3.0",
"node-pty": "0.8.1",
"spdlog": "0.8.1",
"webpack-merge": "^4.2.1"
}
}

View File

@@ -7,38 +7,24 @@
</head>
<body>
<div class="login">
<div class="back">
<- Back </div>
<h4 class="title">code-server</h4>
<h2 class="subtitle">
Enter server password
</h2>
<div class="mdc-text-field">
<input type="password" id="password" class="mdc-text-field__input" required>
<label class="mdc-floating-label" for="password">Password</label>
<div class="mdc-line-ripple"></div>
</div>
<div class="mdc-text-field-helper-line">
<div class="mdc-text-field-helper-text">helper text</div>
</div>
<div class="mdc-form-field">
<div class="mdc-checkbox">
<input type="checkbox" class="mdc-checkbox__native-control" id="remember" />
<div class="mdc-checkbox__background">
<svg class="mdc-checkbox__checkmark" viewBox="0 0 24 24">
<path class="mdc-checkbox__checkmark-path" fill="none" d="M1.73,12.91 8.1,19.28 22.79,4.59" />
</svg>
<div class="mdc-checkbox__mixedmark"></div>
<form id="login-form">
<div class="login">
<div class="back">
<- Back </div> <h4 class="title">code-server</h4>
<h2 class="subtitle">
Enter server password
</h2>
<div class="mdc-text-field">
<input type="password" id="password" class="mdc-text-field__input" required>
<label class="mdc-floating-label" for="password">Password</label>
<div class="mdc-line-ripple"></div>
</div>
</div>
<label for="remember">Remember Me</label>
</div>
<button id="submit" class="mdc-button mdc-button--unelevated">
<span class="mdc-button__label">Enter IDE</span>
</button>
</div>
</div>
<button id="submit" class="mdc-button mdc-button--unelevated">
<span class="mdc-button__label">Enter IDE</span>
</button>
<div id="error-display"></div>
</div>
</form>
</body>
</html>

View File

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

View File

@@ -20,11 +20,27 @@ window.addEventListener("message", (event) => {
});
const password = document.getElementById("password") as HTMLInputElement;
const submit = document.getElementById("submit") as HTMLButtonElement;
if (!submit) {
throw new Error("No submit button found");
const form = document.getElementById("login-form") as HTMLFormElement;
if (!form) {
throw new Error("No password form found");
}
submit.addEventListener("click", () => {
document.cookie = `password=${password.value}`;
form.addEventListener("submit", (e) => {
e.preventDefault();
document.cookie = `password=${password.value}; `
+ `path=${location.pathname.replace(/\/login\/?$/, "/")}; `
+ `domain=${location.hostname}`;
location.reload();
});
/**
* Notify user on load of page if previous password was unsuccessful
*/
const reg = new RegExp(`password=(\\w+);?`);
const matches = document.cookie.match(reg);
const errorDisplay = document.getElementById("error-display") as HTMLDivElement;
if (document.referrer === document.location.href && matches) {
errorDisplay.innerText = "Password is incorrect!";
}

View File

@@ -7,11 +7,10 @@ const root = path.resolve(__dirname, "../../..");
module.exports = merge(
require(path.join(root, "scripts/webpack.client.config.js"))({
entry: path.join(root, "packages/app/browser/src/app.ts"),
template: path.join(root, "packages/app/browser/src/app.html"),
dirname: __dirname,
entry: path.join(__dirname, "src/app.ts"),
name: "login",
template: path.join(__dirname, "src/app.html"),
}), {
output: {
path: path.join(__dirname, "out"),
},
},
);

View File

@@ -258,7 +258,7 @@ export class Server extends React.Component<ServerProps, {
if (xhr.status === 200) {
this.setState({
status: "Online",
version: "v1.31.0",
version: process.env.VERSION,
});
} else {
this.setState({

View File

@@ -5,15 +5,12 @@ const root = path.resolve(__dirname, "../..");
module.exports = merge(
require(path.join(root, "scripts/webpack.node.config.js"))({
// Options.
dirname: __dirname,
name: "dns",
}), {
externals: {
"node-named": "commonjs node-named",
},
output: {
path: path.join(__dirname, "out"),
filename: "main.js",
},
entry: [
"./packages/dns/src/dns.ts"
],

View File

@@ -1,28 +1,51 @@
import { IDisposable } from "@coder/disposable";
export interface Event<T> {
(listener: (e: T) => void): IDisposable;
(listener: (value: T) => void): IDisposable;
(id: number | string, listener: (value: T) => void): IDisposable;
}
/**
* Emitter typecasts for a single event type.
* Emitter typecasts for a single event type. You can optionally use IDs, but
* using undefined with IDs will not work. If you emit without an ID, *all*
* listeners regardless of their ID (or lack thereof) will receive the event.
* Similarly, if you listen without an ID you will get *all* events for any or
* no ID.
*/
export class Emitter<T> {
private listeners = <Array<(e: T) => void>>[];
private listeners = <Array<(value: T) => void>>[];
private readonly idListeners = new Map<number | string, Array<(value: T) => void>>();
public get event(): Event<T> {
return (cb: (e: T) => void): IDisposable => {
if (this.listeners) {
this.listeners.push(cb);
return (id: number | string | ((value: T) => void), cb?: (value: T) => void): IDisposable => {
if (typeof id !== "function") {
if (this.idListeners.has(id)) {
this.idListeners.get(id)!.push(cb!);
} else {
this.idListeners.set(id, [cb!]);
}
return {
dispose: (): void => {
if (this.idListeners.has(id)) {
const cbs = this.idListeners.get(id)!;
const i = cbs.indexOf(cb!);
if (i !== -1) {
cbs.splice(i, 1);
}
}
},
};
}
cb = id;
this.listeners.push(cb);
return {
dispose: (): void => {
if (this.listeners) {
const i = this.listeners.indexOf(cb);
if (i !== -1) {
this.listeners.splice(i, 1);
}
const i = this.listeners.indexOf(cb!);
if (i !== -1) {
this.listeners.splice(i, 1);
}
},
};
@@ -32,16 +55,45 @@ export class Emitter<T> {
/**
* Emit an event with a value.
*/
public emit(value: T): void {
if (this.listeners) {
this.listeners.forEach((t) => t(value));
public emit(value: T): void;
public emit(id: number | string, value: T): void;
public emit(id: number | string | T, value?: T): void {
if ((typeof id === "number" || typeof id === "string") && typeof value !== "undefined") {
if (this.idListeners.has(id)) {
this.idListeners.get(id)!.forEach((cb) => cb(value!));
}
this.listeners.forEach((cb) => cb(value!));
} else {
this.idListeners.forEach((cbs) => cbs.forEach((cb) => cb((id as T)!)));
this.listeners.forEach((cb) => cb((id as T)!));
}
}
/**
* Dispose the current events.
*/
public dispose(): void {
this.listeners = [];
public dispose(): void;
public dispose(id: number | string): void;
public dispose(id?: number | string): void {
if (typeof id !== "undefined") {
this.idListeners.delete(id);
} else {
this.listeners = [];
this.idListeners.clear();
}
}
public get counts(): { [key: string]: number } {
const counts = <{ [key: string]: number }>{};
if (this.listeners.length > 0) {
counts["n/a"] = this.listeners.length;
}
this.idListeners.forEach((cbs, id) => {
if (cbs.length > 0) {
counts[`${id}`] = cbs.length;
}
});
return counts;
}
}

View File

@@ -0,0 +1,122 @@
import { Emitter } from "../src/events";
describe("Event", () => {
const emitter = new Emitter<number>();
it("should listen to global event", () => {
const fn = jest.fn();
const d = emitter.event(fn);
emitter.emit(10);
expect(fn).toHaveBeenCalledWith(10);
d.dispose();
});
it("should listen to id event", () => {
const fn = jest.fn();
const d = emitter.event(0, fn);
emitter.emit(0, 5);
expect(fn).toHaveBeenCalledWith(5);
d.dispose();
});
it("should listen to string id event", () => {
const fn = jest.fn();
const d = emitter.event("string", fn);
emitter.emit("string", 55);
expect(fn).toHaveBeenCalledWith(55);
d.dispose();
});
it("should not listen wrong id event", () => {
const fn = jest.fn();
const d = emitter.event(1, fn);
emitter.emit(0, 5);
emitter.emit(1, 6);
expect(fn).toHaveBeenCalledWith(6);
expect(fn).toHaveBeenCalledTimes(1);
d.dispose();
});
it("should listen to id event globally", () => {
const fn = jest.fn();
const d = emitter.event(fn);
emitter.emit(1, 11);
expect(fn).toHaveBeenCalledWith(11);
d.dispose();
});
it("should listen to global event", () => {
const fn = jest.fn();
const d = emitter.event(3, fn);
emitter.emit(14);
expect(fn).toHaveBeenCalledWith(14);
d.dispose();
});
it("should listen to id event multiple times", () => {
const fn = jest.fn();
const disposers = [
emitter.event(934, fn),
emitter.event(934, fn),
emitter.event(934, fn),
emitter.event(934, fn),
];
emitter.emit(934, 324);
expect(fn).toHaveBeenCalledTimes(4);
expect(fn).toHaveBeenCalledWith(324);
disposers.forEach((d) => d.dispose());
});
it("should dispose individually", () => {
const fn = jest.fn();
const d = emitter.event(fn);
const fn2 = jest.fn();
const d2 = emitter.event(1, fn2);
d.dispose();
emitter.emit(12);
emitter.emit(1, 12);
expect(fn).not.toBeCalled();
expect(fn2).toBeCalledTimes(2);
d2.dispose();
emitter.emit(12);
emitter.emit(1, 12);
expect(fn).not.toBeCalled();
expect(fn2).toBeCalledTimes(2);
});
it("should dispose by id", () => {
const fn = jest.fn();
emitter.event(fn);
const fn2 = jest.fn();
emitter.event(1, fn2);
emitter.dispose(1);
emitter.emit(12);
emitter.emit(1, 12);
expect(fn).toBeCalledTimes(2);
expect(fn2).not.toBeCalled();
});
it("should dispose all", () => {
const fn = jest.fn();
emitter.event(fn);
emitter.event(1, fn);
emitter.dispose();
emitter.emit(12);
emitter.emit(1, 12);
expect(fn).not.toBeCalled();
});
});

View File

@@ -1,3 +1,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;
@@ -106,7 +113,7 @@ interface IMenuItem {
command: ICommandAction;
alt?: ICommandAction;
// when?: ContextKeyExpr;
group?: 'navigation' | string;
group?: "navigation" | string;
order?: number;
}
@@ -134,38 +141,44 @@ interface ICommandRegistry {
registerCommand(command: ICommand): IDisposable;
}
declare namespace ide {
export const client: {
run(func: (helper: ActiveEvalEmitter) => Disposer): ActiveEvalEmitter;
run<T1>(func: (helper: ActiveEvalEmitter, a1: T1) => Disposer, a1: T1): ActiveEvalEmitter;
run<T1, T2>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2) => Disposer, a1: T1, a2: T2): ActiveEvalEmitter;
run<T1, T2, T3>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3) => Disposer, a1: T1, a2: T2, a3: T3): ActiveEvalEmitter;
run<T1, T2, T3, T4>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3, a4: T4) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4): ActiveEvalEmitter;
run<T1, T2, T3, T4, T5>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): ActiveEvalEmitter;
run<T1, T2, T3, T4, T5, T6>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): ActiveEvalEmitter;
interface IStorageService {
save(): Promise<void>;
}
evaluate<R>(func: (helper: EvalHelper) => R | Promise<R>): Promise<R>;
evaluate<R, T1>(func: (helper: EvalHelper, a1: T1) => R | Promise<R>, a1: T1): Promise<R>;
evaluate<R, T1, T2>(func: (helper: EvalHelper, a1: T1, a2: T2) => R | Promise<R>, a1: T1, a2: T2): Promise<R>;
evaluate<R, T1, T2, T3>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3) => R | Promise<R>, a1: T1, a2: T2, a3: T3): Promise<R>;
evaluate<R, T1, T2, T3, T4>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4): Promise<R>;
evaluate<R, T1, T2, T3, T4, T5>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): Promise<R>;
evaluate<R, T1, T2, T3, T4, T5, T6>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): Promise<R>;
};
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;
onFileDelete(cb: (path: string) => void): void;
onFileSaved(cb: (path: string) => void): void;
onFileCopy(cb: (path: string, target: string) => void): void;
onModelAdded(cb: (path: string, languageId: string) => void): void;
onModelRemoved(cb: (path: string, languageId: string) => void): void;
onModelLanguageChange(cb: (path: string, languageId: string, oldLanguageId: string) => void): void;
onTerminalAdded(cb: () => void): void;
onTerminalRemoved(cb: () => void): void;
};
export enum Severity {
Ignore = 0,
Info = 1,
Warning = 2,
Error = 3
}
Error = 3,
}
export enum StatusbarAlignment {
LEFT = 0,
@@ -216,7 +229,7 @@ declare namespace ide {
declare global {
interface Window {
ide?: typeof ide;
addEventListener(event: "ide-ready", callback: (ide: CustomEvent & { readonly ide: typeof ide }) => void): void;
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@coder/ide-api",
"version": "1.0.2",
"version": "1.0.4",
"typings": "api.d.ts",
"author": "Coder",
"license": "MIT",

View File

@@ -1,10 +1,5 @@
{
"name": "@coder/ide",
"description": "Browser-based IDE client abstraction.",
"main": "src/index.ts",
"dependencies": {},
"devDependencies": {
"@types/rimraf": "^2.0.2",
"rimraf": "^2.6.3"
}
"main": "src/index.ts"
}

View File

@@ -34,6 +34,12 @@ export abstract class IdeClient {
this.loadTime = time(2500);
let appWindow: Window | undefined;
window.addEventListener("beforeunload", (e) => {
e.preventDefault(); // FireFox
e.returnValue = ""; // Chrome
});
window.addEventListener("message", (event) => {
if (event.data === "app") {
appWindow = event.source as Window;
@@ -41,7 +47,16 @@ export abstract class IdeClient {
});
this.sharedProcessData = new Promise((resolve): void => {
client.onSharedProcessActive(resolve);
let d = client.onSharedProcessActive((data) => {
d.dispose();
d = client.onSharedProcessActive(() => {
d.dispose();
this.retry.notificationService.error(
new Error("Disconnected from shared process. Searching, installing, enabling, and disabling extensions will not work until the page is refreshed."),
);
});
resolve(data);
});
});
window.addEventListener("contextmenu", (event) => {
@@ -65,10 +80,6 @@ export abstract class IdeClient {
});
}
/**
* Wrap a task in some logging, timing, and progress updates. Can optionally
* wait on other tasks which won't count towards this task's time.
*/
public async task<T>(description: string, duration: number, task: () => Promise<T>): Promise<T>;
public async task<T, V>(description: string, duration: number, task: (v: V) => Promise<T>, t: Promise<V>): Promise<T>;
public async task<T, V1, V2>(description: string, duration: number, task: (v1: V1, v2: V2) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>): Promise<T>;
@@ -76,6 +87,10 @@ export abstract class IdeClient {
public async task<T, V1, V2, V3, V4>(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>, t3: Promise<V3>, t4: Promise<V4>): Promise<T>;
public async task<T, V1, V2, V3, V4, V5>(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>, t3: Promise<V3>, t4: Promise<V4>, t5: Promise<V5>): Promise<T>;
public async task<T, V1, V2, V3, V4, V5, V6>(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>, t3: Promise<V3>, t4: Promise<V4>, t5: Promise<V5>, t6: Promise<V6>): Promise<T>;
/**
* Wrap a task in some logging, timing, and progress updates. Can optionally
* wait on other tasks which won't count towards this task's time.
*/
public async task<T>(
description: string, duration: number = 100, task: (...args: any[]) => Promise<T>, ...after: Array<Promise<any>> // tslint:disable-line no-any
): Promise<T> {

View File

@@ -1,195 +1,4 @@
import * as cp from "child_process";
import * as net from "net";
import * as stream from "stream";
import { CallbackEmitter, ActiveEvalReadable, ActiveEvalWritable } from "@coder/protocol";
import { Module } from "@coder/protocol";
import { client } from "./client";
import { promisify } from "util";
declare var __non_webpack_require__: typeof require;
class ChildProcess extends CallbackEmitter implements cp.ChildProcess {
private _connected: boolean = false;
private _killed: boolean = false;
private _pid = -1;
public readonly stdin: stream.Writable;
public readonly stdout: stream.Readable;
public readonly stderr: stream.Readable;
// We need the explicit type otherwise TypeScript thinks it is (Writable | Readable)[].
public readonly stdio: [stream.Writable, stream.Readable, stream.Readable] = [this.stdin, this.stdout, this.stderr];
// tslint:disable no-any
public constructor(method: "exec", command: string, options?: { encoding?: string | null } & cp.ExecOptions | null, callback?: (...args: any[]) => void);
public constructor(method: "fork", modulePath: string, options?: cp.ForkOptions, args?: string[]);
public constructor(method: "spawn", command: string, options?: cp.SpawnOptions, args?: string[]);
public constructor(method: "exec" | "spawn" | "fork", command: string, options: object = {}, callback?: string[] | ((...args: any[]) => void)) {
// tslint:enable no-any
super();
let args: string[] = [];
if (Array.isArray(callback)) {
args = callback;
callback = undefined;
}
this.ae = client.run((ae, command, method, args, options, callbackId) => {
const cp = __non_webpack_require__("child_process") as typeof import("child_process");
ae.preserveEnv(options);
let childProcess: cp.ChildProcess;
switch (method) {
case "exec":
childProcess = cp.exec(command, options, ae.maybeCallback(callbackId));
break;
case "spawn":
childProcess = cp.spawn(command, args, options);
break;
case "fork":
childProcess = ae.fork(command, args, options);
break;
default:
throw new Error(`invalid method ${method}`);
}
ae.on("disconnect", () => childProcess.disconnect());
ae.on("kill", (signal: string) => childProcess.kill(signal));
ae.on("ref", () => childProcess.ref());
ae.on("send", (message: string, callbackId: number) => childProcess.send(message, ae.maybeCallback(callbackId)));
ae.on("unref", () => childProcess.unref());
ae.emit("pid", childProcess.pid);
childProcess.on("close", (code, signal) => ae.emit("close", code, signal));
childProcess.on("disconnect", () => ae.emit("disconnect"));
childProcess.on("error", (error) => ae.emit("error", error));
childProcess.on("exit", (code, signal) => ae.emit("exit", code, signal));
childProcess.on("message", (message) => ae.emit("message", message));
if (childProcess.stdin) {
const stdinAe = ae.createUnique("stdin");
stdinAe.bindWritable(childProcess.stdin);
}
if (childProcess.stdout) {
const stdoutAe = ae.createUnique("stdout");
stdoutAe.bindReadable(childProcess.stdout);
}
if (childProcess.stderr) {
const stderrAe = ae.createUnique("stderr");
stderrAe.bindReadable(childProcess.stderr);
}
return {
onDidDispose: (cb): cp.ChildProcess => childProcess.on("close", cb),
dispose: (): void => {
childProcess.kill();
setTimeout(() => childProcess.kill("SIGKILL"), 5000); // Double tap.
},
};
}, command, method, args, options, this.storeCallback(callback));
this.ae.on("pid", (pid) => {
this._pid = pid;
this._connected = true;
});
this.stdin = new ActiveEvalWritable(this.ae.createUnique("stdin"));
this.stdout = new ActiveEvalReadable(this.ae.createUnique("stdout"));
this.stderr = new ActiveEvalReadable(this.ae.createUnique("stderr"));
this.ae.on("close", (code, signal) => this.emit("close", code, signal));
this.ae.on("disconnect", () => this.emit("disconnect"));
this.ae.on("error", (error) => this.emit("error", error));
this.ae.on("exit", (code, signal) => {
this._connected = false;
this._killed = true;
this.emit("exit", code, signal);
});
this.ae.on("message", (message) => this.emit("message", message));
}
public get pid(): number { return this._pid; }
public get connected(): boolean { return this._connected; }
public get killed(): boolean { return this._killed; }
public kill(): void { this.ae.emit("kill"); }
public disconnect(): void { this.ae.emit("disconnect"); }
public ref(): void { this.ae.emit("ref"); }
public unref(): void { this.ae.emit("unref"); }
public send(
message: any, // tslint:disable-line no-any to match spec
sendHandle?: net.Socket | net.Server | ((error: Error) => void),
options?: cp.MessageOptions | ((error: Error) => void),
callback?: (error: Error) => void): boolean {
if (typeof sendHandle === "function") {
callback = sendHandle;
sendHandle = undefined;
} else if (typeof options === "function") {
callback = options;
options = undefined;
}
if (sendHandle || options) {
throw new Error("sendHandle and options are not supported");
}
this.ae.emit("send", message, this.storeCallback(callback));
// Unfortunately this will always have to be true since we can't retrieve
// the actual response synchronously.
return true;
}
}
class CP {
public readonly ChildProcess = ChildProcess;
public exec = (
command: string,
options?: { encoding?: string | null } & cp.ExecOptions | null | ((error: cp.ExecException | null, stdout: string, stderr: string) => void) | ((error: cp.ExecException | null, stdout: Buffer, stderr: Buffer) => void),
callback?: ((error: cp.ExecException | null, stdout: string, stderr: string) => void) | ((error: cp.ExecException | null, stdout: Buffer, stderr: Buffer) => void),
): cp.ChildProcess => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
return new ChildProcess("exec", command, options, callback);
}
public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
if (args && !Array.isArray(args)) {
options = args;
args = undefined;
}
return new ChildProcess("fork", modulePath, options, args);
}
public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => {
if (args && !Array.isArray(args)) {
options = args;
args = undefined;
}
return new ChildProcess("spawn", command, options, args);
}
}
const fillCp = new CP();
// Methods that don't follow the standard callback pattern (an error followed
// by a single result) need to provide a custom promisify function.
Object.defineProperty(fillCp.exec, promisify.custom, {
value: (
command: string,
options?: { encoding?: string | null } & cp.ExecOptions | null,
): Promise<{ stdout: string | Buffer, stderr: string | Buffer }> => {
return new Promise((resolve, reject): void => {
fillCp.exec(command, options, (error: cp.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => {
if (error) {
reject(error);
} else {
resolve({ stdout, stderr });
}
});
});
},
});
export = fillCp;
export = client.modules[Module.ChildProcess];

View File

@@ -11,7 +11,7 @@ class WebsocketConnection implements ReadWriteConnection {
private activeSocket: WebSocket | undefined;
private readonly messageBuffer = <Uint8Array[]>[];
private readonly socketTimeoutDelay = 60 * 1000;
private readonly retryName = "Socket";
private readonly retry = retry.register("Socket", () => this.connect());
private isUp: boolean = false;
private closed: boolean = false;
@@ -26,11 +26,14 @@ class WebsocketConnection implements ReadWriteConnection {
public readonly onMessage = this.messageEmitter.event;
public constructor() {
retry.register(this.retryName, () => this.connect());
retry.block(this.retryName);
retry.run(this.retryName);
this.retry.block();
this.retry.run();
}
/**
* Send data across the socket. If closed, will error. If connecting, will
* queue.
*/
public send(data: Buffer | Uint8Array): void {
if (this.closed) {
throw new Error("web socket is closed");
@@ -42,6 +45,9 @@ class WebsocketConnection implements ReadWriteConnection {
}
}
/**
* Close socket connection.
*/
public close(): void {
this.closed = true;
this.dispose();
@@ -61,7 +67,12 @@ class WebsocketConnection implements ReadWriteConnection {
socket.addEventListener("close", (event) => {
if (this.isUp) {
this.isUp = false;
this.downEmitter.emit(undefined);
try {
this.downEmitter.emit(undefined);
} catch (error) {
// Don't let errors here prevent restarting.
logger.error(error.message);
}
}
logger.warn(
"Web socket closed",
@@ -70,8 +81,8 @@ class WebsocketConnection implements ReadWriteConnection {
field("wasClean", event.wasClean),
);
if (!this.closed) {
retry.block(this.retryName);
retry.run(this.retryName);
this.retry.block();
this.retry.run();
}
});
@@ -91,8 +102,9 @@ class WebsocketConnection implements ReadWriteConnection {
*/
private async openSocket(): Promise<WebSocket> {
this.dispose();
const wsProto = location.protocol === "https:" ? "wss" : "ws";
const socket = new WebSocket(
`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}`,
`${wsProto}://${location.host}${location.pathname}`,
);
socket.binaryType = "arraybuffer";
this.activeSocket = socket;
@@ -102,15 +114,19 @@ class WebsocketConnection implements ReadWriteConnection {
}, this.socketTimeoutDelay);
await new Promise((resolve, reject): void => {
const onClose = (): void => {
const doReject = (): void => {
clearTimeout(socketWaitTimeout);
socket.removeEventListener("close", onClose);
socket.removeEventListener("error", doReject);
socket.removeEventListener("close", doReject);
reject();
};
socket.addEventListener("close", onClose);
socket.addEventListener("error", doReject);
socket.addEventListener("close", doReject);
socket.addEventListener("open", async () => {
socket.addEventListener("open", () => {
clearTimeout(socketWaitTimeout);
socket.removeEventListener("error", doReject);
socket.removeEventListener("close", doReject);
resolve();
});
});

View File

@@ -19,7 +19,6 @@
background: #141414;
border: none;
box-sizing: border-box;
margin-bottom: 25px;
padding: 10px;
width: 100%;
}
@@ -31,11 +30,11 @@
.msgbox > .detail {
font-size: 14px;
margin-top: 5px;
margin: 5px 0;
}
.msgbox > .errors {
margin-bottom: 25px;
margin-top: 20px;
}
.msgbox > .errors {
@@ -46,6 +45,7 @@
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 20px;
}
.msgbox > .button-wrapper > button {

View File

@@ -31,30 +31,31 @@ export class Dialog {
private input: HTMLInputElement | undefined;
private errors: HTMLElement;
private buttons: HTMLElement[] | undefined;
private readonly msgBox: HTMLElement;
private actionEmitter = new Emitter<IDialogAction>();
public onAction = this.actionEmitter.event;
public constructor(private readonly options: IDialogOptions) {
const msgBox = document.createElement("div");
msgBox.classList.add("msgbox");
this.msgBox = document.createElement("div");
this.msgBox.classList.add("msgbox");
if (this.options.message) {
const messageDiv = document.createElement("div");
messageDiv.classList.add("msg");
messageDiv.innerText = this.options.message;
msgBox.appendChild(messageDiv);
this.msgBox.appendChild(messageDiv);
}
if (this.options.detail) {
const detailDiv = document.createElement("div");
detailDiv.classList.add("detail");
detailDiv.innerText = this.options.detail;
msgBox.appendChild(detailDiv);
this.msgBox.appendChild(detailDiv);
}
if (this.options.input) {
msgBox.classList.add("input");
this.msgBox.classList.add("input");
this.input = document.createElement("input");
this.input.classList.add("input");
this.input.value = this.options.input.value;
@@ -67,12 +68,11 @@ export class Dialog {
});
}
});
msgBox.appendChild(this.input);
this.msgBox.appendChild(this.input);
}
this.errors = document.createElement("div");
this.errors.classList.add("errors");
msgBox.appendChild(this.errors);
if (this.options.buttons && this.options.buttons.length > 0) {
this.buttons = this.options.buttons.map((buttonText, buttonIndex) => {
@@ -92,12 +92,12 @@ export class Dialog {
const buttonWrapper = document.createElement("div");
buttonWrapper.classList.add("button-wrapper");
this.buttons.forEach((b) => buttonWrapper.appendChild(b));
msgBox.appendChild(buttonWrapper);
this.msgBox.appendChild(buttonWrapper);
}
this.overlay = document.createElement("div");
this.overlay.className = "msgbox-overlay";
this.overlay.appendChild(msgBox);
this.overlay.appendChild(this.msgBox);
setTimeout(() => {
this.overlay.style.opacity = "1";
@@ -122,6 +122,7 @@ export class Dialog {
const errorDiv = document.createElement("error");
errorDiv.innerText = error;
this.errors.appendChild(errorDiv);
this.msgBox.appendChild(this.errors);
}
}
@@ -131,7 +132,7 @@ export class Dialog {
public show(): void {
if (!this.cachedActiveElement) {
this.cachedActiveElement = document.activeElement as HTMLElement;
document.body.appendChild(this.overlay);
(document.querySelector(".monaco-workbench") || document.body).appendChild(this.overlay);
document.addEventListener("keydown", this.onKeydown);
if (this.input) {
this.input.focus();

View File

@@ -1,12 +1,10 @@
/// <reference path="../../../../lib/vscode/src/typings/electron.d.ts" />
import { EventEmitter } from "events";
import * as fs from "fs";
import * as trash from "trash";
import { logger, field } from "@coder/logger";
import { IKey, Dialog as DialogBox } from "./dialog";
import { clipboard } from "./clipboard";
import { client } from "./client";
declare var __non_webpack_require__: typeof require;
// tslint:disable-next-line no-any
(global as any).getOpenUrls = (): string[] => {
@@ -25,18 +23,40 @@ const newCreateElement = <K extends keyof HTMLElementTagNameMap>(tagName: K): HT
// tslint:disable-next-line:no-any
return oldCreateElement.call(document, tagName as any);
};
// tslint:disable-next-line:no-any
const getPropertyDescriptor = (object: any, id: string): PropertyDescriptor | undefined => {
let op = Object.getPrototypeOf(object);
while (!Object.getOwnPropertyDescriptor(op, id)) {
op = Object.getPrototypeOf(op);
}
return Object.getOwnPropertyDescriptor(op, id);
};
if (tagName === "img") {
const img = createElement("img");
const oldSrc = getPropertyDescriptor(img, "src");
if (!oldSrc) {
throw new Error("Failed to find src property");
}
Object.defineProperty(img, "src", {
get: (): string => {
return oldSrc!.get!.call(img);
},
set: (value: string): void => {
if (value) {
const resourceBaseUrl = location.pathname.replace(/\/$/, "") + "/resource";
value = value.replace(/file:\/\//g, resourceBaseUrl);
}
oldSrc!.set!.call(img, value);
},
});
return img;
}
if (tagName === "style") {
const style = createElement("style");
// tslint:disable-next-line:no-any
const getPropertyDescriptor = (object: any, id: string): PropertyDescriptor | undefined => {
let op = Object.getPrototypeOf(object);
while (!Object.getOwnPropertyDescriptor(op, id)) {
op = Object.getPrototypeOf(op);
}
return Object.getOwnPropertyDescriptor(op, id);
};
const oldInnerHtml = getPropertyDescriptor(style, "innerHTML");
if (!oldInnerHtml) {
throw new Error("Failed to find innerHTML property");
@@ -46,7 +66,10 @@ const newCreateElement = <K extends keyof HTMLElementTagNameMap>(tagName: K): HT
return oldInnerHtml!.get!.call(style);
},
set: (value: string): void => {
value = value.replace(/file:\/\//g, "/resource");
if (value) {
const resourceBaseUrl = location.pathname.replace(/\/$/, "") + "/resource";
value = value.replace(/file:\/\//g, resourceBaseUrl);
}
oldInnerHtml!.set!.call(style, value);
},
});
@@ -59,7 +82,8 @@ const newCreateElement = <K extends keyof HTMLElementTagNameMap>(tagName: K): HT
if (sheet && !overridden) {
const oldInsertRule = sheet.insertRule;
sheet.insertRule = (rule: string, index?: number): void => {
rule = rule.replace(/file:\/\//g, "/resource");
const resourceBaseUrl = location.pathname.replace(/\/$/, "") + "/resource";
rule = rule.replace(/file:\/\//g, resourceBaseUrl);
oldInsertRule.call(sheet, rule, index);
};
overridden = true;
@@ -116,12 +140,18 @@ const newCreateElement = <K extends keyof HTMLElementTagNameMap>(tagName: K): HT
};
},
});
view.src = require("!!file-loader?name=[path][name].[ext]!./webview.html");
Object.defineProperty(view, "src", {
set: (): void => { /* Nope. */ },
});
(view as any).getWebContents = (): void => undefined; // tslint:disable-line no-any
(view as any).send = (channel: string, ...args: any[]): void => { // tslint:disable-line no-any
if (args[0] && typeof args[0] === "object" && args[0].contents) {
// TODO
args[0].contents = (args[0].contents as string).replace(/"(file:\/\/[^"]*)"/g, (m1) => `"/resource${m1}"`);
args[0].contents = (args[0].contents as string).replace(/"vscode-resource:([^"]*)"/g, (m, m1) => `"/resource${m1}"`);
const resourceBaseUrl = location.pathname.replace(/\/$/, "") + "/resource";
args[0].contents = (args[0].contents as string).replace(/"(file:\/\/[^"]*)"/g, (m1) => `"${resourceBaseUrl}${m1}"`);
args[0].contents = (args[0].contents as string).replace(/"vscode-resource:([^"]*)"/g, (m, m1) => `"${resourceBaseUrl}${m1}"`);
args[0].contents = (args[0].contents as string).replace(/style-src vscode-core-resource:/g, "style-src 'self'");
}
if (view.contentWindow) {
view.contentWindow.postMessage({
@@ -141,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 {
@@ -156,15 +188,23 @@ class Clipboard {
public writeText(value: string): Promise<void> {
return clipboard.writeText(value);
}
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 {
public async moveItemToTrash(path: string): Promise<void> {
await client.evaluate((_helper, path) => {
const trash = __non_webpack_require__("trash") as typeof import("trash");
return trash(path);
}, path);
await trash(path);
}
}
@@ -338,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

@@ -1,763 +1,4 @@
import { EventEmitter } from "events";
import * as fs from "fs";
import * as stream from "stream";
import { Client, IEncodingOptions, IEncodingOptionsCallback } from "@coder/protocol";
import { Module } from "@coder/protocol";
import { client } from "./client";
import { promisify } from "util";
declare var __non_webpack_require__: typeof require;
declare var _Buffer: typeof Buffer;
/**
* Implements the native fs module
* Doesn't use `implements typeof import("fs")` to remove need for __promisify__ impls
*
* TODO: For now we can't use async in the evaluate calls because they get
* transpiled to TypeScript's helpers. tslib is included but we also need to set
* _this somehow which the __awaiter helper uses.
*/
class FS {
public constructor(
private readonly client: Client,
) { }
public access = (path: fs.PathLike, mode: number | undefined | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
this.client.evaluate((_helper, path, mode) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.access)(path, mode);
}, path, mode).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
// tslint:disable-next-line no-any
public appendFile = (file: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
this.client.evaluate((_helper, path, data, options) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.appendFile)(path, data, options);
}, file, data, options).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
public chmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, path, mode) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.chmod)(path, mode);
}, path, mode).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public chown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, path, uid, gid) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.chown)(path, uid, gid);
}, path, uid, gid).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public close = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, fd) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.close)(fd);
}, fd).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public copyFile = (src: fs.PathLike, dest: fs.PathLike, flags: number | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof flags === "function") {
callback = flags;
}
this.client.evaluate((_helper, src, dest, flags) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.copyFile)(src, dest, flags);
}, src, dest, typeof flags !== "function" ? flags : undefined).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
// tslint:disable-next-line no-any
public createWriteStream = (path: fs.PathLike, options?: any): fs.WriteStream => {
const ae = this.client.run((ae, path, options) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const str = fs.createWriteStream(path, options);
ae.on("write", (d: string) => str.write(_Buffer.from(d, "utf8")));
ae.on("close", () => str.close());
ae.on("destroy", () => str.destroy());
str.on("close", () => ae.emit("close"));
str.on("open", (fd) => ae.emit("open", fd));
str.on("error", (err) => ae.emit(err));
return {
onDidDispose: (cb): fs.WriteStream => str.on("close", cb),
dispose: (): void => str.close(),
};
}, path, options);
return new (class WriteStream extends stream.Writable implements fs.WriteStream {
private _bytesWritten: number = 0;
public constructor() {
super({
write: (data, encoding, cb): void => {
this._bytesWritten += data.length;
ae.emit("write", Buffer.from(data, encoding), encoding);
cb();
},
});
ae.on("open", (fd: number) => this.emit("open", fd));
ae.on("close", () => this.emit("close"));
}
public get bytesWritten(): number {
return this._bytesWritten;
}
public get path(): string | Buffer {
return "";
}
public close(): void {
ae.emit("close");
}
public destroy(): void {
ae.emit("destroy");
}
}) as fs.WriteStream;
}
public exists = (path: fs.PathLike, callback: (exists: boolean) => void): void => {
this.client.evaluate((_helper, path) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.exists)(path);
}, path).then((r) => {
callback(r);
}).catch(() => {
callback(false);
});
}
public fchmod = (fd: number, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, fd, mode) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.fchmod)(fd, mode);
}, fd, mode).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public fchown = (fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, fd, uid, gid) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.fchown)(fd, uid, gid);
}, fd, uid, gid).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public fdatasync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, fd) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.fdatasync)(fd);
}, fd).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public fstat = (fd: number, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
this.client.evaluate((_helper, fd) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
const tslib = __non_webpack_require__("tslib") as typeof import("tslib");
return util.promisify(fs.fstat)(fd).then((stats) => {
return tslib.__assign(stats, {
_isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false,
_isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false,
_isDirectory: stats.isDirectory(),
_isFIFO: stats.isFIFO ? stats.isFIFO() : false,
_isFile: stats.isFile(),
_isSocket: stats.isSocket ? stats.isSocket() : false,
_isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false,
});
});
}, fd).then((stats) => {
callback(undefined!, new Stats(stats));
}).catch((ex) => {
callback(ex, undefined!);
});
}
public fsync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, fd) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.fsync)(fd);
}, fd).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public ftruncate = (fd: number, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof len === "function") {
callback = len;
len = undefined;
}
this.client.evaluate((_helper, fd, len) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.ftruncate)(fd, len);
}, fd, len).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
public futimes = (fd: number, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, fd, atime, mtime) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.futimes)(fd, atime, mtime);
}, fd, atime, mtime).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public lchmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, path, mode) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.lchmod)(path, mode);
}, path, mode).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public lchown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, path, uid, gid) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.lchown)(path, uid, gid);
}, path, uid, gid).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public link = (existingPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, existingPath, newPath) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.link)(existingPath, newPath);
}, existingPath, newPath).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public lstat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
this.client.evaluate((_helper, path) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
const tslib = __non_webpack_require__("tslib") as typeof import("tslib");
return util.promisify(fs.lstat)(path).then((stats) => {
return tslib.__assign(stats, {
_isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false,
_isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false,
_isDirectory: stats.isDirectory(),
_isFIFO: stats.isFIFO ? stats.isFIFO() : false,
_isFile: stats.isFile(),
_isSocket: stats.isSocket ? stats.isSocket() : false,
_isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false,
});
});
}, path).then((stats) => {
callback(undefined!, new Stats(stats));
}).catch((ex) => {
callback(ex, undefined!);
});
}
public mkdir = (path: fs.PathLike, mode: number | string | fs.MakeDirectoryOptions | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
this.client.evaluate((_helper, path, mode) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.mkdir)(path, mode);
}, path, mode).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
public mkdtemp = (prefix: string, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, folder: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
this.client.evaluate((_helper, prefix, options) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.mkdtemp)(prefix, options);
}, prefix, options).then((folder) => {
callback!(undefined!, folder);
}).catch((ex) => {
callback!(ex, undefined!);
});
}
public open = (path: fs.PathLike, flags: string | number, mode: string | number | undefined | null | ((err: NodeJS.ErrnoException, fd: number) => void), callback?: (err: NodeJS.ErrnoException, fd: number) => void): void => {
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
this.client.evaluate((_helper, path, flags, mode) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.open)(path, flags, mode);
}, path, flags, mode).then((fd) => {
callback!(undefined!, fd);
}).catch((ex) => {
callback!(ex, undefined!);
});
}
public read = <TBuffer extends Buffer | Uint8Array>(fd: number, buffer: TBuffer, offset: number, length: number, position: number | null, callback: (err: NodeJS.ErrnoException, bytesRead: number, buffer: TBuffer) => void): void => {
this.client.evaluate((_helper, fd, length, position) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
const buffer = new _Buffer(length);
return util.promisify(fs.read)(fd, buffer, 0, length, position).then((resp) => {
return {
bytesRead: resp.bytesRead,
content: resp.bytesRead < buffer.length ? buffer.slice(0, resp.bytesRead) : buffer,
};
});
}, fd, length, position).then((resp) => {
buffer.set(resp.content, offset);
callback(undefined!, resp.bytesRead, resp.content as TBuffer);
}).catch((ex) => {
callback(ex, undefined!, undefined!);
});
}
public readFile = (path: fs.PathLike | number, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, data: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
this.client.evaluate((_helper, path, options) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.readFile)(path, options).then((value) => value.toString());
}, path, options).then((buffer) => {
callback!(undefined!, buffer);
}).catch((ex) => {
callback!(ex, undefined!);
});
}
public readdir = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, files: Buffer[] | fs.Dirent[] | string[]) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
// TODO: options can also take `withFileTypes` but the types aren't working.
this.client.evaluate((_helper, path, options) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.readdir)(path, options);
}, path, options).then((files) => {
callback!(undefined!, files);
}).catch((ex) => {
callback!(ex, undefined!);
});
}
public readlink = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, linkString: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
this.client.evaluate((_helper, path, options) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.readlink)(path, options);
}, path, options).then((linkString) => {
callback!(undefined!, linkString);
}).catch((ex) => {
callback!(ex, undefined!);
});
}
public realpath = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, resolvedPath: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
this.client.evaluate((_helper, path, options) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.realpath)(path, options);
}, path, options).then((resolvedPath) => {
callback!(undefined!, resolvedPath);
}).catch((ex) => {
callback!(ex, undefined!);
});
}
public rename = (oldPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, oldPath, newPath) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.rename)(oldPath, newPath);
}, oldPath, newPath).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public rmdir = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, path) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.rmdir)(path);
}, path).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public stat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
this.client.evaluate((_helper, path) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
const tslib = __non_webpack_require__("tslib") as typeof import("tslib");
return util.promisify(fs.stat)(path).then((stats) => {
return tslib.__assign(stats, {
/**
* We need to check if functions exist because nexe's implemented FS
* lib doesnt implement fs.stats properly
*/
_isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false,
_isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false,
_isDirectory: stats.isDirectory(),
_isFIFO: stats.isFIFO ? stats.isFIFO() : false,
_isFile: stats.isFile(),
_isSocket: stats.isSocket ? stats.isSocket() : false,
_isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false,
});
});
}, path).then((stats) => {
callback(undefined!, new Stats(stats));
}).catch((ex) => {
callback(ex, undefined!);
});
}
public symlink = (target: fs.PathLike, path: fs.PathLike, type: fs.symlink.Type | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof type === "function") {
callback = type;
type = undefined;
}
this.client.evaluate((_helper, target, path, type) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.symlink)(target, path, type);
}, target, path, type).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
public truncate = (path: fs.PathLike, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof len === "function") {
callback = len;
len = undefined;
}
this.client.evaluate((_helper, path, len) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.truncate)(path, len);
}, path, len).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
public unlink = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, path) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.unlink)(path);
}, path).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public utimes = (path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((_helper, path, atime, mtime) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.utimes)(path, atime, mtime);
}, path, atime, mtime).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public write = <TBuffer extends Buffer | Uint8Array>(fd: number, buffer: TBuffer, offset: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), length: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), position: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), callback?: (err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void): void => {
if (typeof offset === "function") {
callback = offset;
offset = undefined;
}
if (typeof length === "function") {
callback = length;
length = undefined;
}
if (typeof position === "function") {
callback = position;
position = undefined;
}
this.client.evaluate((_helper, fd, buffer, offset, length, position) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.write)(fd, _Buffer.from(buffer, "utf8"), offset, length, position).then((resp) => {
return {
bytesWritten: resp.bytesWritten,
content: resp.buffer.toString("utf8"),
};
});
}, fd, buffer.toString(), offset, length, position).then((r) => {
callback!(undefined!, r.bytesWritten, Buffer.from(r.content, "utf8") as TBuffer);
}).catch((ex) => {
callback!(ex, undefined!, undefined!);
});
}
// tslint:disable-next-line no-any
public writeFile = (path: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
this.client.evaluate((_helper, path, data, options) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const util = __non_webpack_require__("util") as typeof import("util");
return util.promisify(fs.writeFile)(path, data, options);
}, path, data, options).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
public watch = (filename: fs.PathLike, options?: IEncodingOptions | ((event: string, filename: string | Buffer) => void), listener?: ((event: string, filename: string | Buffer) => void)): fs.FSWatcher => {
if (typeof options === "function") {
listener = options;
options = undefined;
}
const ae = this.client.run((ae, filename, hasListener, options) => {
const fs = __non_webpack_require__("fs") as typeof import ("fs");
// tslint:disable-next-line no-any
const watcher = fs.watch(filename, options as any, hasListener ? (event, filename): void => {
ae.emit("listener", event, filename);
} : undefined);
watcher.on("change", (event, filename) => ae.emit("change", event, filename));
watcher.on("error", (error) => ae.emit("error", error));
ae.on("close", () => watcher.close());
return {
onDidDispose: (cb): void => ae.on("close", cb),
dispose: (): void => watcher.close(),
};
}, filename.toString(), !!listener, options);
return new class Watcher extends EventEmitter implements fs.FSWatcher {
public constructor() {
super();
ae.on("change", (event: string, filename: string) => this.emit("change", event, filename));
ae.on("error", (error: Error) => this.emit("error", error));
ae.on("listener", (event: string, filename: string) => listener && listener(event, filename));
}
public close(): void {
ae.emit("close");
}
};
}
}
interface IStats {
dev: number;
ino: number;
mode: number;
nlink: number;
uid: number;
gid: number;
rdev: number;
size: number;
blksize: number;
blocks: number;
atimeMs: number;
mtimeMs: number;
ctimeMs: number;
birthtimeMs: number;
atime: Date | string;
mtime: Date | string;
ctime: Date | string;
birthtime: Date | string;
_isFile: boolean;
_isDirectory: boolean;
_isBlockDevice: boolean;
_isCharacterDevice: boolean;
_isSymbolicLink: boolean;
_isFIFO: boolean;
_isSocket: boolean;
}
class Stats implements fs.Stats {
public readonly atime: Date;
public readonly mtime: Date;
public readonly ctime: Date;
public readonly birthtime: Date;
public constructor(private readonly stats: IStats) {
this.atime = new Date(stats.atime);
this.mtime = new Date(stats.mtime);
this.ctime = new Date(stats.ctime);
this.birthtime = new Date(stats.birthtime);
}
public get dev(): number { return this.stats.dev; }
public get ino(): number { return this.stats.ino; }
public get mode(): number { return this.stats.mode; }
public get nlink(): number { return this.stats.nlink; }
public get uid(): number { return this.stats.uid; }
public get gid(): number { return this.stats.gid; }
public get rdev(): number { return this.stats.rdev; }
public get size(): number { return this.stats.size; }
public get blksize(): number { return this.stats.blksize; }
public get blocks(): number { return this.stats.blocks; }
public get atimeMs(): number { return this.stats.atimeMs; }
public get mtimeMs(): number { return this.stats.mtimeMs; }
public get ctimeMs(): number { return this.stats.ctimeMs; }
public get birthtimeMs(): number { return this.stats.birthtimeMs; }
public isFile(): boolean { return this.stats._isFile; }
public isDirectory(): boolean { return this.stats._isDirectory; }
public isBlockDevice(): boolean { return this.stats._isBlockDevice; }
public isCharacterDevice(): boolean { return this.stats._isCharacterDevice; }
public isSymbolicLink(): boolean { return this.stats._isSymbolicLink; }
public isFIFO(): boolean { return this.stats._isFIFO; }
public isSocket(): boolean { return this.stats._isSocket; }
public toObject(): object {
return JSON.parse(JSON.stringify(this));
}
}
const fillFs = new FS(client);
// Methods that don't follow the standard callback pattern (an error followed
// by a single result) need to provide a custom promisify function.
Object.defineProperty(fillFs.exists, promisify.custom, {
value: (path: fs.PathLike): Promise<boolean> => new Promise((resolve): void => fillFs.exists(path, resolve)),
});
export = fillFs;
export = client.modules[Module.Fs];

View File

@@ -1,258 +1,4 @@
import * as net from "net";
import { CallbackEmitter, ActiveEvalDuplex, ActiveEvalHelper } from "@coder/protocol";
import { Module } from "@coder/protocol";
import { client } from "./client";
declare var __non_webpack_require__: typeof require;
class Socket extends ActiveEvalDuplex implements net.Socket {
private _connecting: boolean = false;
private _destroyed: boolean = false;
public constructor(options?: net.SocketConstructorOpts, ae?: ActiveEvalHelper) {
super(ae || client.run((ae, options) => {
const net = __non_webpack_require__("net") as typeof import("net");
return ae.bindSocket(new net.Socket(options));
}, options));
this.ae.on("connect", () => {
this._connecting = false;
this.emit("connect");
});
this.ae.on("error", () => {
this._connecting = false;
this._destroyed = true;
});
this.ae.on("lookup", (error, address, family, host) => this.emit("lookup", error, address, family, host));
this.ae.on("timeout", () => this.emit("timeout"));
}
public connect(options: net.SocketConnectOpts | number | string, host?: string | Function, connectionListener?: Function): this {
// This is to get around type issues with socket.connect as well as extract
// the function wherever it might be.
switch (typeof options) {
case "string": options = { path: options }; break;
case "number": options = { port: options }; break;
}
switch (typeof host) {
case "function": connectionListener = host; break;
case "string": (options as net.TcpSocketConnectOpts).host = host; break;
}
this._connecting = true;
this.ae.emit("connect", options, this.storeCallback(connectionListener));
return this;
}
// tslint:disable-next-line no-any
public write(data: any, encoding?: string | Function, fd?: string | Function): boolean {
let callback: Function | undefined;
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
if (typeof fd === "function") {
callback = fd;
fd = undefined;
}
this.ae.emit("write", data, encoding, fd, this.storeCallback(callback));
return true; // Always true since we can't get this synchronously.
}
public get connecting(): boolean { return this._connecting; }
public get destroyed(): boolean { return this._destroyed; }
public get bufferSize(): number { throw new Error("not implemented"); }
public get bytesRead(): number { throw new Error("not implemented"); }
public get bytesWritten(): number { throw new Error("not implemented"); }
public get localAddress(): string { throw new Error("not implemented"); }
public get localPort(): number { throw new Error("not implemented"); }
public address(): net.AddressInfo | string { throw new Error("not implemented"); }
public setTimeout(timeout: number, callback?: Function): this { return this.emitReturnThis("setTimeout", timeout, this.storeCallback(callback)); }
public setNoDelay(noDelay?: boolean): this { return this.emitReturnThis("setNoDelay", noDelay); }
public setKeepAlive(enable?: boolean, initialDelay?: number): this { return this.emitReturnThis("setKeepAlive", enable, initialDelay); }
public unref(): void { this.ae.emit("unref"); }
public ref(): void { this.ae.emit("ref"); }
}
class Server extends CallbackEmitter implements net.Server {
private readonly sockets = new Map<number, Socket>();
private _listening: boolean = false;
public constructor(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: Socket) => void), connectionListener?: (socket: Socket) => void) {
super();
if (typeof options === "function") {
connectionListener = options;
options = undefined;
}
this.ae = client.run((ae, options, callbackId) => {
const net = __non_webpack_require__("net") as typeof import("net");
let connectionId = 0;
const sockets = new Map<number, net.Socket>();
const storeSocket = (socket: net.Socket): number => {
const socketId = connectionId++;
sockets.set(socketId, socket);
const socketAe = ae.createUnique(socketId);
const disposer = socketAe.bindSocket(socket);
socket.on("close", () => {
disposer.dispose();
sockets.delete(socketId);
});
return socketId;
};
const callback = ae.maybeCallback(callbackId);
let server = new net.Server(options, typeof callback !== "undefined" ? (socket): void => {
callback(storeSocket(socket));
} : undefined);
server.on("close", () => ae.emit("close"));
server.on("connection", (socket) => ae.emit("connection", storeSocket(socket)));
server.on("error", (error) => ae.emit("error", error));
server.on("listening", () => ae.emit("listening"));
ae.on("close", (callbackId: number) => server.close(ae.maybeCallback(callbackId)));
ae.on("listen", (handle?: net.ListenOptions | number | string) => server.listen(handle));
ae.on("ref", () => server.ref());
ae.on("unref", () => server.unref());
return {
onDidDispose: (cb): net.Server => server.on("close", cb),
dispose: (): void => {
server.removeAllListeners();
server.close();
sockets.forEach((socket) => {
socket.removeAllListeners();
socket.end();
socket.destroy();
socket.unref();
});
sockets.clear();
},
};
}, options || {}, this.storeCallback(connectionListener));
this.ae.on("close", () => {
this._listening = false;
this.emit("close");
});
this.ae.on("connection", (socketId) => {
const socketAe = this.ae.createUnique(socketId);
const socket = new Socket(undefined, socketAe);
this.sockets.set(socketId, socket);
socket.on("close", () => this.sockets.delete(socketId));
if (connectionListener) {
connectionListener(socket);
}
this.emit("connection", socket);
});
this.ae.on("error", (error) => {
this._listening = false;
this.emit("error", error);
});
this.ae.on("listening", () => {
this._listening = true;
this.emit("listening");
});
}
public listen(handle?: net.ListenOptions | number | string, hostname?: string | number | Function, backlog?: number | Function, listeningListener?: Function): this {
if (typeof handle === "undefined") {
throw new Error("no handle");
}
switch (typeof handle) {
case "number": handle = { port: handle }; break;
case "string": handle = { path: handle }; break;
}
switch (typeof hostname) {
case "function": listeningListener = hostname; break;
case "string": handle.host = hostname; break;
case "number": handle.backlog = hostname; break;
}
switch (typeof backlog) {
case "function": listeningListener = backlog; break;
case "number": handle.backlog = backlog; break;
}
if (listeningListener) {
this.ae.on("listening", () => {
listeningListener!();
});
}
this.ae.emit("listen", handle);
return this;
}
public close(callback?: Function): this {
// close() doesn't fire the close event until all connections are also
// closed, but it does prevent new connections.
this._listening = false;
this.ae.emit("close", this.storeCallback(callback));
return this;
}
public get connections(): number { return this.sockets.size; }
public get listening(): boolean { return this._listening; }
public get maxConnections(): number { throw new Error("not implemented"); }
public address(): net.AddressInfo | string { throw new Error("not implemented"); }
public ref(): this { return this.emitReturnThis("ref"); }
public unref(): this { return this.emitReturnThis("unref"); }
public getConnections(cb: (error: Error | null, count: number) => void): void { cb(null, this.sockets.size); }
// tslint:disable-next-line no-any
private emitReturnThis(event: string, ...args: any[]): this {
this.ae.emit(event, ...args);
return this;
}
}
type NodeNet = typeof net;
/**
* Implementation of net for the browser.
*/
class Net implements NodeNet {
// @ts-ignore this is because Socket is missing things from the Stream
// namespace but I'm unsure how best to provide them (finished,
// finished.__promisify__, pipeline, and some others) or if it even matters.
public readonly Socket = Socket;
public readonly Server = Server;
public createConnection(target: string | number | net.NetConnectOpts, host?: string | Function, callback?: Function): net.Socket {
const socket = new Socket();
socket.connect(target, host, callback);
return socket;
}
public createServer(
options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: net.Socket) => void),
connectionListener?: (socket: net.Socket) => void,
): net.Server {
return new Server(options, connectionListener);
}
public connect(): net.Socket { throw new Error("not implemented"); }
public isIP(_input: string): number { throw new Error("not implemented"); }
public isIPv4(_input: string): boolean { throw new Error("not implemented"); }
public isIPv6(_input: string): boolean { throw new Error("not implemented"); }
}
export = new Net();
export = client.modules[Module.Net];

View File

@@ -35,6 +35,10 @@ class OS {
default: this._platform = "linux"; break;
}
process.platform = this._platform;
process.env = {};
data.env.forEach((v, k) => {
process.env[k] = v;
});
}
public release(): string {

View File

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

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="en" style="width: 100%; height: 100%">
<head>
<title>Virtual Document</title>
</head>
<body style="margin: 0; overflow: hidden; width: 100%; height: 100%">
</body>
</html>

View File

@@ -1,14 +1,64 @@
import { logger, field } from "@coder/logger";
import { NotificationService, INotificationHandle, INotificationService, Severity } from "./fill/notification";
// tslint:disable no-any can have different return values
interface IRetryItem {
/**
* How many times this item has been retried.
*/
count?: number;
delay?: number; // In seconds.
end?: number; // In ms.
fn(): any | Promise<any>; // tslint:disable-line no-any can have different return values
/**
* In seconds.
*/
delay?: number;
/**
* In milliseconds.
*/
end?: number;
/**
* Function to run when retrying.
*/
fn(): any;
/**
* Timer for running this item.
*/
timeout?: number | NodeJS.Timer;
/**
* Whether the item is retrying or waiting to retry.
*/
running?: boolean;
showInNotification: boolean;
}
/**
* An retry-able instance.
*/
export interface RetryInstance {
/**
* Run this retry.
*/
run(error?: Error): void;
/**
* Block on this instance.
*/
block(): void;
}
/**
* A retry-able instance that doesn't use a promise so it must be manually
* ran again on failure and recovered on success.
*/
export interface ManualRetryInstance extends RetryInstance {
/**
* Mark this item as recovered.
*/
recover(): void;
}
/**
@@ -21,11 +71,11 @@ interface IRetryItem {
* to the user explaining what is happening with an option to immediately retry.
*/
export class Retry {
private items = new Map<string, IRetryItem>();
private readonly items = new Map<string, IRetryItem>();
// Times are in seconds.
private readonly retryMinDelay = 1;
private readonly retryMaxDelay = 10;
private readonly retryMaxDelay = 3;
private readonly maxImmediateRetries = 5;
private readonly retryExponent = 1.5;
private blocked: string | boolean | undefined;
@@ -50,8 +100,49 @@ export class Retry {
}
/**
* Block retries when we know they will fail (for example when starting Wush
* back up). If a name is passed, that service will still be allowed to retry
* Register a function to retry that starts/connects to a service.
*
* The service is automatically retried or recovered when the promise resolves
* or rejects. If the service dies after starting, it must be manually
* retried.
*/
public register(name: string, fn: () => Promise<any>): RetryInstance;
/**
* Register a function to retry that starts/connects to a service.
*
* Must manually retry if it fails to start again or dies after restarting and
* manually recover if it succeeds in starting again.
*/
public register(name: string, fn: () => any): ManualRetryInstance;
/**
* Register a function to retry that starts/connects to a service.
*/
public register(name: string, fn: () => any): RetryInstance | ManualRetryInstance {
if (this.items.has(name)) {
throw new Error(`"${name}" is already registered`);
}
this.items.set(name, { fn });
return {
block: (): void => this.block(name),
run: (error?: Error): void => this.run(name, error),
recover: (): void => this.recover(name),
};
}
/**
* Un-register a function to retry.
*/
public unregister(name: string): void {
if (!this.items.has(name)) {
throw new Error(`"${name}" is not registered`);
}
this.items.delete(name);
}
/**
* Block retries when we know they will fail (for example when the socket is
* down ). If a name is passed, that service will still be allowed to retry
* (unless we have already blocked).
*
* Blocking without a name will override a block with a name.
@@ -68,7 +159,7 @@ export class Retry {
/**
* Unblock retries and run any that are pending.
*/
public unblock(): void {
private unblock(): void {
this.blocked = false;
this.items.forEach((item, name) => {
if (item.running) {
@@ -77,35 +168,10 @@ export class Retry {
});
}
/**
* Register a function to retry that starts/connects to a service.
*
* If the function returns a promise, it will automatically be retried,
* recover, & unblock after calling `run` once (otherwise they need to be
* called manually).
*/
// tslint:disable-next-line no-any can have different return values
public register(name: string, fn: () => any | Promise<any>, showInNotification: boolean = true): void {
if (this.items.has(name)) {
throw new Error(`"${name}" is already registered`);
}
this.items.set(name, { fn, showInNotification });
}
/**
* Unregister a function to retry.
*/
public unregister(name: string): void {
if (!this.items.has(name)) {
throw new Error(`"${name}" is not registered`);
}
this.items.delete(name);
}
/**
* Retry a service.
*/
public run(name: string, error?: Error): void {
private run(name: string, error?: Error): void {
if (!this.items.has(name)) {
throw new Error(`"${name}" is not registered`);
}
@@ -149,7 +215,7 @@ export class Retry {
/**
* Reset a service after a successfully recovering.
*/
public recover(name: string): void {
private recover(name: string): void {
if (!this.items.has(name)) {
throw new Error(`"${name}" is not registered`);
}
@@ -191,9 +257,9 @@ export class Retry {
if (this.blocked === name) {
this.unblock();
}
}).catch(() => {
}).catch((error) => {
endItem();
this.run(name);
this.run(name, error);
});
} else {
endItem();
@@ -214,8 +280,7 @@ export class Retry {
const now = Date.now();
const items = Array.from(this.items.entries()).filter(([_, item]) => {
return item.showInNotification
&& typeof item.end !== "undefined"
return typeof item.end !== "undefined"
&& item.end > now
&& item.delay && item.delay >= this.notificationThreshold;
}).sort((a, b) => {

View File

@@ -2,113 +2,3 @@
# yarn lockfile v1
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
"@types/glob@*":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
dependencies:
"@types/events" "*"
"@types/minimatch" "*"
"@types/node" "*"
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/node@*":
version "11.9.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.4.tgz#ceb0048a546db453f6248f2d1d95e937a6f00a14"
integrity sha512-Zl8dGvAcEmadgs1tmSPcvwzO1YRsz38bVJQvH1RvRqSR9/5n61Q1ktcDL0ht3FXWR+ZpVmXVwN1LuH4Ax23NsA==
"@types/rimraf@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e"
integrity sha512-Hm/bnWq0TCy7jmjeN5bKYij9vw5GrDFWME4IuxV08278NtU/VdGbzsBohcCUJ7+QMqmUq5hpRKB39HeQWJjztQ==
dependencies:
"@types/glob" "*"
"@types/node" "*"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
glob@^7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
rimraf@^2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
dependencies:
glob "^7.1.3"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=

View File

@@ -5,9 +5,12 @@
"build": "tsc -p tsconfig.build.json && cp ./out/packages/logger/src/* ./out && rm -rf out/packages && ../../node_modules/.bin/webpack --config ./webpack.config.js",
"postinstall": "if [ ! -d out ];then npm run build; fi"
},
"version": "1.0.3",
"version": "1.1.3",
"main": "out/main.js",
"types": "out/index.d.ts",
"author": "Coder",
"license": "MIT"
"license": "MIT",
"dependencies": {
"@google-cloud/logging": "^4.5.2"
}
}

View File

@@ -0,0 +1,12 @@
import { field, logger } from "./logger";
import { createStackdriverExtender } from "./extender";
describe("Extender", () => {
it("should add stackdriver extender", () => {
logger.extend(createStackdriverExtender("coder-dev-1", "logging-package-tests"));
});
it("should log", async () => {
logger.debug("Bananas!", field("frog", { hi: "wow" }));
});
});

View File

@@ -0,0 +1,63 @@
import * as gcl from "@google-cloud/logging";
import { Extender, logger, field } from "./logger";
export const createStackdriverExtender = (projectId: string, logId: string): Extender => {
enum GcpLogSeverity {
DEFAULT = 0,
DEBUG = 100,
INFO = 200,
NOTICE = 300,
WARNING = 400,
ERROR = 500,
CRITICAL = 600,
ALERT = 700,
EMERGENCY = 800,
}
const logging = new gcl.Logging({
autoRetry: true,
projectId,
});
const log = logging.log(logId);
const convertSeverity = (severity: "trace" | "info" | "warn" | "debug" | "error"): GcpLogSeverity => {
switch (severity) {
case "trace":
case "debug":
return GcpLogSeverity.DEBUG;
case "info":
return GcpLogSeverity.INFO;
case "error":
return GcpLogSeverity.ERROR;
case "warn":
return GcpLogSeverity.WARNING;
}
};
return (options): void => {
const severity = convertSeverity(options.type);
// tslint:disable-next-line:no-any
const metadata = {} as any;
if (options.fields) {
options.fields.forEach((f) => {
if (!f) {
return;
}
metadata[f.identifier] = f.value;
});
}
const entry = log.entry({
// tslint:disable-next-line:no-any
severity: severity as any,
}, {
...metadata,
message: options.message,
});
log.write(entry).catch((ex) => {
logger.named("GCP").error("Failed to log", field("error", ex));
});
};
};

View File

@@ -59,6 +59,14 @@ export const field = <T>(name: string, value: T): Field<T> => {
return new Field(name, value);
};
export type Extender = (msg: {
message: string,
level: Level,
type: "trace" | "info" | "warn" | "debug" | "error",
fields?: FieldArray,
section?: string,
}) => void;
/**
* This formats & builds text for logging.
* It should only be used to build one log item at a time since it stores the
@@ -221,6 +229,7 @@ export class Logger {
private _formatter: Formatter,
private readonly name?: string,
private readonly defaultFields?: FieldArray,
private readonly extenders: Extender[] = [],
) {
if (name) {
this.nameColor = this.hashStringToColor(name);
@@ -248,6 +257,10 @@ export class Logger {
this.muted = true;
}
public extend(extender: Extender): void {
this.extenders.push(extender);
}
/**
* Outputs information.
*/
@@ -328,7 +341,7 @@ export class Logger {
* Each name is deterministically generated a color.
*/
public named(name: string, ...fields: FieldArray): Logger {
const l = new Logger(this._formatter, name, fields);
const l = new Logger(this._formatter, name, fields, this.extenders);
if (this.muted) {
l.mute();
}
@@ -393,6 +406,16 @@ export class Logger {
console.log(...this._formatter.flush());
}
// tslint:enable no-console
this.extenders.forEach((extender) => {
extender({
section: this.name,
fields: options.fields,
level: options.level,
message: options.message as string,
type: options.type,
});
});
}
/**

View File

@@ -1,16 +1,34 @@
const path = require("path");
const merge = require("webpack-merge");
module.exports = merge(require(path.join(__dirname, "../../scripts", "webpack.general.config.js"))(), {
devtool: "none",
mode: "production",
target: "node",
output: {
path: path.join(__dirname, "out"),
filename: "main.js",
libraryTarget: "commonjs",
},
entry: [
"./packages/logger/src/index.ts"
],
});
module.exports = [
merge(require(path.join(__dirname, "../../scripts", "webpack.general.config.js"))(), {
devtool: "none",
mode: "production",
target: "node",
output: {
path: path.join(__dirname, "out"),
filename: "main.js",
libraryTarget: "commonjs",
},
entry: [
"./packages/logger/src/index.ts"
],
}),
merge(require(path.join(__dirname, "../../scripts", "webpack.general.config.js"))(), {
devtool: "none",
mode: "production",
target: "node",
output: {
path: path.join(__dirname, "out"),
filename: "extender.js",
libraryTarget: "commonjs",
},
externals: {
"@google-cloud/logging": "commonjs @google-cloud/logging",
},
entry: [
"./packages/logger/src/extender.ts"
],
}),
];

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,11 @@
"xmlhttprequest": "1.8.0"
},
"jest": {
"globals": {
"ts-jest": {
"diagnostics": false
}
},
"moduleFileExtensions": [
"ts",
"tsx",
@@ -26,7 +31,9 @@
"@coder/ide/src/fill/evaluation": "<rootDir>/ide/src/fill/evaluation",
"@coder/ide/src/fill/client": "<rootDir>/ide/src/fill/client",
"@coder/(.*)/test": "<rootDir>/$1/test",
"@coder/(.*)": "<rootDir>/$1/src"
"@coder/(.*)": "<rootDir>/$1/src",
"vs/(.*)": "<rootDir>/../lib/vscode/src/vs/$1",
"vszip": "<rootDir>/../lib/vscode/src/vs/base/node/zip.ts"
},
"transform": {
"^.+\\.tsx?$": "ts-jest"

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,15 +4,18 @@
"dependencies": {
"express": "^4.16.4",
"google-protobuf": "^3.6.1",
"node-pty-prebuilt": "^0.7.6",
"spdlog": "^0.7.2",
"tslib": "^1.9.3",
"trash": "^4.3.0",
"ws": "^6.1.2"
},
"devDependencies": {
"@types/google-protobuf": "^3.2.7",
"@types/rimraf": "^2.0.2",
"@types/text-encoding": "^0.0.35",
"rimraf": "^2.6.3",
"text-encoding": "^0.7.0",
"ts-protoc-gen": "^0.8.0"
},
"scripts": {
"gen": "./scripts/generate_proto.sh"
}
}

View File

@@ -1,3 +1,5 @@
#!/bin/bash
protoc --plugin="protoc-gen-ts=./node_modules/.bin/protoc-gen-ts" --js_out="import_style=commonjs,binary:./src/proto" --ts_out="./src/proto" ./src/proto/*.proto --proto_path="./src/proto"
cd "$(dirname "$0")/.."
protoc --plugin="protoc-gen-ts=./node_modules/.bin/protoc-gen-ts" --js_out="import_style=commonjs,binary:./src/proto" --ts_out="./src/proto" ./src/proto/*.proto --proto_path="./src/proto"

View File

@@ -1,19 +1,32 @@
import { EventEmitter } from "events";
import { PathLike } from "fs";
import { ExecException, ExecOptions } from "child_process";
import { promisify } from "util";
import { Emitter } from "@coder/events";
import { logger, field } from "@coder/logger";
import { Ping, NewEvalMessage, ServerMessage, EvalDoneMessage, EvalFailedMessage, ClientMessage, WorkingInitMessage, EvalEventMessage } from "../proto";
import { ReadWriteConnection, InitData, OperatingSystem, SharedProcessData } from "../common/connection";
import { ActiveEvalHelper, EvalHelper, Disposer, ServerActiveEvalHelper } from "../common/helpers";
import { stringify, parse } from "../common/util";
import { ReadWriteConnection, InitData, SharedProcessData } from "../common/connection";
import { 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";
// tslint:disable no-any
interface ProxyData {
promise: Promise<void>;
instance: any;
callbacks: Map<number, (...args: any[]) => void>;
}
/**
* Client accepts an arbitrary connection intended to communicate with the Server.
* Client accepts a connection to communicate with the server.
*/
export class Client {
private evalId = 0;
private readonly evalDoneEmitter = new Emitter<EvalDoneMessage>();
private readonly evalFailedEmitter = new Emitter<EvalFailedMessage>();
private readonly evalEventEmitter = new Emitter<EvalEventMessage>();
private messageId = 0;
private callbackId = 0;
private readonly proxies = new Map<number | Module, ProxyData>();
private readonly successEmitter = new Emitter<Method.Success>();
private readonly failEmitter = new Emitter<Method.Fail>();
private readonly eventEmitter = new Emitter<{ event: string; args: any[]; }>();
private _initData: InitData | undefined;
private readonly initDataEmitter = new Emitter<InitData>();
@@ -22,37 +35,123 @@ export class Client {
private readonly sharedProcessActiveEmitter = new Emitter<SharedProcessData>();
public readonly onSharedProcessActive = this.sharedProcessActiveEmitter.event;
private disconnected: boolean = false;
// The socket timeout is 60s, so we need to send a ping periodically to
// prevent it from closing.
private pingTimeout: NodeJS.Timer | number | undefined;
private readonly pingTimeoutDelay = 30000;
private readonly responseTimeout = 10000;
public readonly modules: {
[Module.ChildProcess]: ChildProcessModule,
[Module.Fs]: FsModule,
[Module.Net]: NetModule,
[Module.NodePty]: NodePtyModule,
[Module.Spdlog]: SpdlogModule,
[Module.Trash]: TrashModule,
};
/**
* @param connection Established connection to the server
*/
public constructor(
private readonly connection: ReadWriteConnection,
) {
connection.onMessage((data) => {
public constructor(private readonly connection: ReadWriteConnection) {
connection.onMessage(async (data) => {
let message: ServerMessage | undefined;
try {
message = ServerMessage.deserializeBinary(data);
this.handleMessage(message);
await this.handleMessage(message);
} catch (error) {
logger.error(
"Failed to handle server message",
field("id", message && message.hasEvalEvent() ? message.getEvalEvent()!.getId() : undefined),
field("id", message && this.getMessageId(message)),
field("length", data.byteLength),
field("error", error.message),
);
}
});
connection.onClose(() => {
clearTimeout(this.pingTimeout as any); // tslint:disable-line no-any
this.pingTimeout = undefined;
this.createProxy(Module.ChildProcess);
this.createProxy(Module.Fs);
this.createProxy(Module.Net);
this.createProxy(Module.NodePty);
this.createProxy(Module.Spdlog);
this.createProxy(Module.Trash);
this.modules = {
[Module.ChildProcess]: new ChildProcessModule(this.getProxy(Module.ChildProcess).instance),
[Module.Fs]: new FsModule(this.getProxy(Module.Fs).instance),
[Module.Net]: new NetModule(this.getProxy(Module.Net).instance),
[Module.NodePty]: new NodePtyModule(this.getProxy(Module.NodePty).instance),
[Module.Spdlog]: new SpdlogModule(this.getProxy(Module.Spdlog).instance),
[Module.Trash]: new TrashModule(this.getProxy(Module.Trash).instance),
};
// Methods that don't follow the standard callback pattern (an error
// followed by a single result) need to provide a custom promisify function.
Object.defineProperty(this.modules[Module.Fs].exists, promisify.custom, {
value: (path: PathLike): Promise<boolean> => {
return new Promise((resolve): void => this.modules[Module.Fs].exists(path, resolve));
},
});
Object.defineProperty(this.modules[Module.ChildProcess].exec, promisify.custom, {
value: (
command: string,
options?: { encoding?: string | null } & ExecOptions | null,
): Promise<{ stdout: string | Buffer, stderr: string | Buffer }> => {
return new Promise((resolve, reject): void => {
this.modules[Module.ChildProcess].exec(command, options, (error: ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => {
if (error) {
reject(error);
} else {
resolve({ stdout, stderr });
}
});
});
},
});
/**
* If the connection is interrupted, the calls will neither succeed nor fail
* nor exit so we need to send a failure on all of them as well as trigger
* events so things like child processes can clean up and possibly restart.
*/
const handleDisconnect = (): void => {
this.disconnected = true;
logger.trace(() => [
"disconnected from server",
field("proxies", this.proxies.size),
field("callbacks", Array.from(this.proxies.values()).reduce((count, p) => count + p.callbacks.size, 0)),
field("success listeners", this.successEmitter.counts),
field("fail listeners", this.failEmitter.counts),
field("event listeners", this.eventEmitter.counts),
]);
const message = new Method.Fail();
const error = new Error("disconnected");
message.setResponse(argumentToProto(error));
this.failEmitter.emit(message);
this.eventEmitter.emit({ event: "disconnected", args: [error] });
this.eventEmitter.emit({ event: "done", args: [] });
};
connection.onDown(() => handleDisconnect());
connection.onClose(() => {
clearTimeout(this.pingTimeout as any);
this.pingTimeout = undefined;
handleDisconnect();
this.proxies.clear();
this.successEmitter.dispose();
this.failEmitter.dispose();
this.eventEmitter.dispose();
this.initDataEmitter.dispose();
this.sharedProcessActiveEmitter.dispose();
});
connection.onUp(() => this.disconnected = false);
this.initDataPromise = new Promise((resolve): void => {
this.initDataEmitter.event(resolve);
});
@@ -60,6 +159,9 @@ export class Client {
this.startPinging();
}
/**
* Close the connection.
*/
public dispose(): void {
this.connection.close();
}
@@ -68,173 +170,225 @@ export class Client {
return this.initDataPromise;
}
public run(func: (helper: ServerActiveEvalHelper) => Disposer): ActiveEvalHelper;
public run<T1>(func: (helper: ServerActiveEvalHelper, a1: T1) => Disposer, a1: T1): ActiveEvalHelper;
public run<T1, T2>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2) => Disposer, a1: T1, a2: T2): ActiveEvalHelper;
public run<T1, T2, T3>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3) => Disposer, a1: T1, a2: T2, a3: T3): ActiveEvalHelper;
public run<T1, T2, T3, T4>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3, a4: T4) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4): ActiveEvalHelper;
public run<T1, T2, T3, T4, T5>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): ActiveEvalHelper;
public run<T1, T2, T3, T4, T5, T6>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): ActiveEvalHelper;
/**
* Run a function on the server and provide an event emitter which allows
* listening and emitting to the emitter provided to that function. The
* function should return a disposer for cleaning up when the client
* disconnects and for notifying when disposal has happened outside manual
* activation.
* Make a remote call for a proxy's method using proto.
*/
public run<T1, T2, T3, T4, T5, T6>(func: (helper: ServerActiveEvalHelper, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6) => Disposer, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6): ActiveEvalHelper {
const doEval = this.doEvaluate(func, a1, a2, a3, a4, a5, a6, true);
// This takes server events and emits them to the client's emitter.
const eventEmitter = new EventEmitter();
const d1 = this.evalEventEmitter.event((msg) => {
if (msg.getId() === doEval.id) {
eventEmitter.emit(msg.getEvent(), ...msg.getArgsList().map(parse));
private remoteCall(proxyId: number | Module, method: string, args: any[]): Promise<any> {
if (typeof proxyId === "number" && (this.disconnected || !this.proxies.has(proxyId))) {
// Can assume killing or closing works because a disconnected proxy is
// disposed on the server's side, and a non-existent proxy has already
// been disposed.
switch (method) {
case "close":
case "kill":
return Promise.resolve();
}
});
doEval.completed.then(() => {
d1.dispose();
}).catch((ex) => {
d1.dispose();
// This error event is only received by the client.
eventEmitter.emit("error", ex);
});
return Promise.reject(
new Error(`Unable to call "${method}" on proxy ${proxyId}: disconnected`),
);
}
return new ActiveEvalHelper({
// This takes client events and emits them to the server's emitter and
// listens to events received from the server (via the event hook above).
// tslint:disable no-any
on: (event: string, cb: (...args: any[]) => void): EventEmitter => eventEmitter.on(event, cb),
emit: (event: string, ...args: any[]): void => {
const eventsMsg = new EvalEventMessage();
eventsMsg.setId(doEval.id);
eventsMsg.setEvent(event);
eventsMsg.setArgsList(args.map((a) => stringify(a)));
const clientMsg = new ClientMessage();
clientMsg.setEvalEvent(eventsMsg);
this.connection.send(clientMsg.serializeBinary());
},
removeAllListeners: (event: string): EventEmitter => eventEmitter.removeAllListeners(event),
// tslint:enable no-any
});
}
const message = new Method();
const id = this.messageId++;
let proxyMessage: Method.Named | Method.Numbered;
if (typeof proxyId === "string") {
proxyMessage = new Method.Named();
proxyMessage.setModule(moduleToProto(proxyId));
message.setNamedProxy(proxyMessage);
} else {
proxyMessage = new Method.Numbered();
proxyMessage.setProxyId(proxyId);
message.setNumberedProxy(proxyMessage);
}
proxyMessage.setId(id);
proxyMessage.setMethod(method);
public evaluate<R>(func: (helper: EvalHelper) => R | Promise<R>): Promise<R>;
public evaluate<R, T1>(func: (helper: EvalHelper, a1: T1) => R | Promise<R>, a1: T1): Promise<R>;
public evaluate<R, T1, T2>(func: (helper: EvalHelper, a1: T1, a2: T2) => R | Promise<R>, a1: T1, a2: T2): Promise<R>;
public evaluate<R, T1, T2, T3>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3) => R | Promise<R>, a1: T1, a2: T2, a3: T3): Promise<R>;
public evaluate<R, T1, T2, T3, T4>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4): Promise<R>;
public evaluate<R, T1, T2, T3, T4, T5>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): Promise<R>;
public evaluate<R, T1, T2, T3, T4, T5, T6>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): Promise<R>;
/**
* Evaluates a function on the server.
* To pass variables, ensure they are serializable and passed through the included function.
* @example
* const returned = await this.client.evaluate((helper, value) => {
* return value;
* }, "hi");
* console.log(returned);
* // output: "hi"
* @param func Function to evaluate
* @returns Promise rejected or resolved from the evaluated function
*/
public evaluate<R, T1, T2, T3, T4, T5, T6>(func: (helper: EvalHelper, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6) => R | Promise<R>, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6): Promise<R> {
return this.doEvaluate(func, a1, a2, a3, a4, a5, a6, false).completed;
}
const storeCallback = (cb: (...args: any[]) => void): number => {
const callbackId = this.callbackId++;
logger.trace(() => [
"storing callback",
field("proxyId", proxyId),
field("callbackId", callbackId),
]);
// tslint:disable-next-line no-any
private doEvaluate<R, T1, T2, T3, T4, T5, T6>(func: (...args: any[]) => void | Promise<void> | R | Promise<R>, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6, active: boolean = false): {
readonly completed: Promise<R>;
readonly id: number;
} {
const newEval = new NewEvalMessage();
const id = this.evalId++;
newEval.setId(id);
newEval.setActive(active);
newEval.setArgsList([a1, a2, a3, a4, a5, a6].map((a) => stringify(a)));
newEval.setFunction(func.toString());
this.getProxy(proxyId).callbacks.set(callbackId, cb);
const clientMsg = new ClientMessage();
clientMsg.setNewEval(newEval);
this.connection.send(clientMsg.serializeBinary());
return callbackId;
};
const completed = new Promise<R>((resolve, reject): void => {
logger.trace(() => [
"sending",
field("id", id),
field("proxyId", proxyId),
field("method", method),
]);
proxyMessage.setArgsList(args.map((a) => argumentToProto<ClientServerProxy>(
a,
storeCallback,
(p) => p.proxyId,
)));
const clientMessage = new ClientMessage();
clientMessage.setMethod(message);
this.connection.send(clientMessage.serializeBinary());
// The server will send back a fail or success message when the method
// has completed, so we listen for that based on the message's unique ID.
const promise = new Promise((resolve, reject): void => {
const dispose = (): void => {
d1.dispose();
d2.dispose();
clearTimeout(timeout as any);
};
const d1 = this.evalDoneEmitter.event((doneMsg) => {
if (doneMsg.getId() === id) {
dispose();
resolve(parse(doneMsg.getResponse()));
}
const timeout = setTimeout(() => {
dispose();
reject(new Error("timed out"));
}, this.responseTimeout);
const d1 = this.successEmitter.event(id, (message) => {
dispose();
resolve(this.protoToArgument(message.getResponse(), promise));
});
const d2 = this.evalFailedEmitter.event((failedMsg) => {
if (failedMsg.getId() === id) {
dispose();
reject(parse(failedMsg.getResponse()));
}
const d2 = this.failEmitter.event(id, (message) => {
dispose();
reject(protoToArgument(message.getResponse()));
});
});
return { completed, id };
return promise;
}
/**
* Handles a message from the server. All incoming server messages should be
* routed through here.
* Handle all messages from the server.
*/
private handleMessage(message: ServerMessage): void {
if (message.hasInit()) {
const init = message.getInit()!;
let opSys: OperatingSystem;
switch (init.getOperatingSystem()) {
case WorkingInitMessage.OperatingSystem.WINDOWS:
opSys = OperatingSystem.Windows;
break;
case WorkingInitMessage.OperatingSystem.LINUX:
opSys = OperatingSystem.Linux;
break;
case WorkingInitMessage.OperatingSystem.MAC:
opSys = OperatingSystem.Mac;
break;
default:
throw new Error(`unsupported operating system ${init.getOperatingSystem()}`);
}
this._initData = {
dataDirectory: init.getDataDirectory(),
homeDirectory: init.getHomeDirectory(),
tmpDirectory: init.getTmpDirectory(),
workingDirectory: init.getWorkingDirectory(),
os: opSys,
shell: init.getShell(),
builtInExtensionsDirectory: init.getBuiltinExtensionsDir(),
};
this.initDataEmitter.emit(this._initData);
} else if (message.hasEvalDone()) {
this.evalDoneEmitter.emit(message.getEvalDone()!);
} else if (message.hasEvalFailed()) {
this.evalFailedEmitter.emit(message.getEvalFailed()!);
} else if (message.hasEvalEvent()) {
this.evalEventEmitter.emit(message.getEvalEvent()!);
} else if (message.hasSharedProcessActive()) {
const sharedProcessActiveMessage = message.getSharedProcessActive()!;
this.sharedProcessActiveEmitter.emit({
socketPath: sharedProcessActiveMessage.getSocketPath(),
logPath: sharedProcessActiveMessage.getLogPath(),
});
} else if (message.hasPong()) {
// Nothing to do since we run the pings on a timer, in case either message
// is dropped which would break the ping cycle.
} else {
throw new Error("unknown message type");
private async handleMessage(message: ServerMessage): Promise<void> {
switch (message.getMsgCase()) {
case ServerMessage.MsgCase.INIT:
const init = message.getInit()!;
this._initData = {
dataDirectory: init.getDataDirectory(),
homeDirectory: init.getHomeDirectory(),
tmpDirectory: init.getTmpDirectory(),
workingDirectory: init.getWorkingDirectory(),
os: protoToOperatingSystem(init.getOperatingSystem()),
shell: init.getShell(),
extensionsDirectory: init.getExtensionsDirectory(),
builtInExtensionsDirectory: init.getBuiltinExtensionsDir(),
extraExtensionDirectories: init.getExtraExtensionDirectoriesList(),
extraBuiltinExtensionDirectories: init.getExtraBuiltinExtensionDirectoriesList(),
env: init.getEnvMap(),
};
this.initDataEmitter.emit(this._initData);
break;
case ServerMessage.MsgCase.SUCCESS:
this.emitSuccess(message.getSuccess()!);
break;
case ServerMessage.MsgCase.FAIL:
this.emitFail(message.getFail()!);
break;
case ServerMessage.MsgCase.EVENT:
await this.emitEvent(message.getEvent()!);
break;
case ServerMessage.MsgCase.CALLBACK:
await this.runCallback(message.getCallback()!);
break;
case ServerMessage.MsgCase.SHARED_PROCESS_ACTIVE:
const sharedProcessActiveMessage = message.getSharedProcessActive()!;
this.sharedProcessActiveEmitter.emit({
socketPath: sharedProcessActiveMessage.getSocketPath(),
logPath: sharedProcessActiveMessage.getLogPath(),
});
break;
case ServerMessage.MsgCase.PONG:
// Nothing to do since pings are on a timer rather than waiting for the
// next pong in case a message from either the client or server is dropped
// which would break the ping cycle.
break;
default:
throw new Error("unknown message type");
}
}
private startPinging = (): void => {
/**
* Convert message to a success event.
*/
private emitSuccess(message: Method.Success): void {
logger.trace(() => [
"received resolve",
field("id", message.getId()),
]);
this.successEmitter.emit(message.getId(), message);
}
/**
* Convert message to a fail event.
*/
private emitFail(message: Method.Fail): void {
logger.trace(() => [
"received reject",
field("id", message.getId()),
]);
this.failEmitter.emit(message.getId(), message);
}
/**
* Emit an event received from the server. We could send requests for "on" to
* the server and serialize functions using IDs, but doing it that way makes
* it possible to miss events depending on whether the server receives the
* request before it emits. Instead, emit all events from the server so all
* events are always caught on the client.
*/
private async emitEvent(message: Event): Promise<void> {
const eventMessage = message.getNamedEvent()! || message.getNumberedEvent()!;
const proxyId = message.getNamedEvent()
? protoToModule(message.getNamedEvent()!.getModule())
: message.getNumberedEvent()!.getProxyId();
const event = eventMessage.getEvent();
await this.ensureResolved(proxyId);
logger.trace(() => [
"received event",
field("proxyId", proxyId),
field("event", event),
]);
const args = eventMessage.getArgsList().map((a) => this.protoToArgument(a));
this.eventEmitter.emit(proxyId, { event, args });
}
/**
* Run a callback as requested by the server. Since we don't know when
* callbacks get garbage collected we dispose them only when the proxy
* disposes. That means they should only be used if they run for the lifetime
* of the proxy (like child_process.exec), otherwise we'll leak. They should
* also only be used when passed together with the method. If they are sent
* afterward, they may never be called due to timing issues.
*/
private async runCallback(message: Callback): Promise<void> {
const callbackMessage = message.getNamedCallback()! || message.getNumberedCallback()!;
const proxyId = message.getNamedCallback()
? protoToModule(message.getNamedCallback()!.getModule())
: message.getNumberedCallback()!.getProxyId();
const callbackId = callbackMessage.getCallbackId();
await this.ensureResolved(proxyId);
logger.trace(() => [
"running callback",
field("proxyId", proxyId),
field("callbackId", callbackId),
]);
const args = callbackMessage.getArgsList().map((a) => this.protoToArgument(a));
this.getProxy(proxyId).callbacks.get(callbackId)!(...args);
}
/**
* Start the ping loop. Does nothing if already pinging.
*/
private readonly startPinging = (): void => {
if (typeof this.pingTimeout !== "undefined") {
return;
}
@@ -250,4 +404,142 @@ export class Client {
schedulePing();
}
/**
* Return the message's ID if it has one or a string identifier. For logging
* errors with an ID to make the error more useful.
*/
private getMessageId(message: ServerMessage): number | string | undefined {
if (message.hasInit()) {
return "init";
} else if (message.hasSuccess()) {
return message.getSuccess()!.getId();
} else if (message.hasFail()) {
return message.getFail()!.getId();
} else if (message.hasEvent()) {
const eventMessage = message.getEvent()!.getNamedEvent()!
|| message.getEvent()!.getNumberedEvent()!;
return `event: ${eventMessage.getEvent()}`;
} else if (message.hasCallback()) {
const callbackMessage = message.getCallback()!.getNamedCallback()!
|| message.getCallback()!.getNumberedCallback()!;
return `callback: ${callbackMessage.getCallbackId()}`;
} else if (message.hasSharedProcessActive()) {
return "shared";
} else if (message.hasPong()) {
return "pong";
}
}
/**
* Return a proxy that makes remote calls.
*/
private createProxy<T extends ClientServerProxy>(proxyId: number | Module, promise: Promise<any> = Promise.resolve()): T {
logger.trace(() => [
"creating proxy",
field("proxyId", proxyId),
]);
const instance = new Proxy({
proxyId,
onDone: (cb: (...args: any[]) => void): void => {
this.eventEmitter.event(proxyId, (event) => {
if (event.event === "done") {
cb(...event.args);
}
});
},
onEvent: (cb: (event: string, ...args: any[]) => void): void => {
this.eventEmitter.event(proxyId, (event) => {
cb(event.event, ...event.args);
});
},
} as ClientServerProxy, {
get: (target: any, name: string): any => {
// When resolving a promise with a proxy, it will check for "then".
if (name === "then") {
return;
}
if (typeof target[name] === "undefined") {
target[name] = (...args: any[]): Promise<any> | ServerProxy => {
return this.remoteCall(proxyId, name, args);
};
}
return target[name];
},
});
this.proxies.set(proxyId, {
promise,
instance,
callbacks: new Map(),
});
instance.onDone(() => {
const log = (): void => {
logger.trace(() => [
typeof proxyId === "number" ? "disposed proxy" : "disposed proxy callbacks",
field("proxyId", proxyId),
field("disconnected", this.disconnected),
field("callbacks", Array.from(this.proxies.values()).reduce((count, proxy) => count + proxy.callbacks.size, 0)),
field("success listeners", this.successEmitter.counts),
field("fail listeners", this.failEmitter.counts),
field("event listeners", this.eventEmitter.counts),
]);
};
// Uniquely identified items (top-level module proxies) can continue to
// be used so we don't need to delete them.
if (typeof proxyId === "number") {
const dispose = (): void => {
this.proxies.delete(proxyId);
this.eventEmitter.dispose(proxyId);
log();
};
if (!this.disconnected) {
instance.dispose().then(dispose).catch(dispose);
} else {
dispose();
}
} else {
// The callbacks will still be unusable though.
this.getProxy(proxyId).callbacks.clear();
log();
}
});
return instance;
}
/**
* We aren't guaranteed the promise will call all the `then` callbacks
* synchronously once it resolves, so the event message can come in and fire
* before a caller has been able to attach an event. Waiting for the promise
* ensures it runs after everything else.
*/
private async ensureResolved(proxyId: number | Module): Promise<void> {
await this.getProxy(proxyId).promise;
}
/**
* Same as protoToArgument except provides createProxy.
*/
private protoToArgument(value?: Argument, promise?: Promise<any>): any {
return protoToArgument(value, undefined, (id) => this.createProxy(id, promise));
}
/**
* Get a proxy. Error if it doesn't exist.
*/
private getProxy(proxyId: number | Module): ProxyData {
if (!this.proxies.has(proxyId)) {
throw new Error(`proxy ${proxyId} disposed too early`);
}
return this.proxies.get(proxyId)!;
}
}

View File

@@ -0,0 +1,151 @@
import * as cp from "child_process";
import * as net from "net";
import * as stream from "stream";
import { callbackify } from "util";
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 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;
public readonly stdio: [stream.Writable, stream.Readable, stream.Readable];
private _connected: boolean = false;
private _killed: boolean = false;
private _pid = -1;
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!));
this.stderr = new Readable(proxyPromises.then((p) => p.stderr!));
this.stdio = [this.stdin, this.stdout, this.stderr];
this.catch(this.proxy.getPid().then((pid) => {
this._pid = pid;
this._connected = true;
}));
this.on("disconnect", () => this._connected = false);
this.on("exit", () => {
this._connected = false;
this._killed = true;
});
}
public get pid(): number {
return this._pid;
}
public get connected(): boolean {
return this._connected;
}
public get killed(): boolean {
return this._killed;
}
public kill(): void {
this._killed = true;
this.catch(this.proxy.kill());
}
public disconnect(): void {
this.catch(this.proxy.disconnect());
}
public ref(): void {
this.catch(this.proxy.ref());
}
public unref(): void {
this.catch(this.proxy.unref());
}
public send(
message: any, // tslint:disable-line no-any
sendHandle?: net.Socket | net.Server | ((error: Error) => void),
options?: cp.MessageOptions | ((error: Error) => void),
callback?: (error: Error) => void): boolean {
if (typeof sendHandle === "function") {
callback = sendHandle;
sendHandle = undefined;
} else if (typeof options === "function") {
callback = options;
options = undefined;
}
if (sendHandle || options) {
throw new Error("sendHandle and options are not supported");
}
callbackify(this.proxy.send)(message, (error) => {
if (callback) {
callback(error);
}
});
return true; // Always true since we can't get this synchronously.
}
/**
* Exit and close the process when disconnected.
*/
protected handleDisconnect(): void {
this.emit("exit", 1);
this.emit("close");
}
}
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: ClientChildProcessModuleProxy) {}
public exec = (
command: string,
options?: { encoding?: string | null } & cp.ExecOptions | null
| ((error: cp.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => void),
callback?: ((error: cp.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => void),
): cp.ChildProcess => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
return new ChildProcess(this.proxy.exec(command, options, callback));
}
public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
if (!Array.isArray(args)) {
options = args;
args = undefined;
}
return new ChildProcess(this.proxy.fork(modulePath, args, options));
}
public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => {
if (!Array.isArray(args)) {
options = args;
args = undefined;
}
return new ChildProcess(this.proxy.spawn(command, args, options));
}
}

View File

@@ -0,0 +1,380 @@
import * as fs from "fs";
import { callbackify } from "util";
import { Batch, ClientProxy, ClientServerProxy } from "../../common/proxy";
import { IEncodingOptions, IEncodingOptionsCallback } from "../../common/util";
import { FsModuleProxy, ReadStreamProxy, Stats as IStats, WatcherProxy, WriteStreamProxy } from "../../node/modules/fs";
import { Readable, Writable } from "./stream";
// tslint:disable completed-docs no-any
class StatBatch extends Batch<IStats, { path: fs.PathLike }> {
public constructor(private readonly proxy: FsModuleProxy) {
super();
}
protected remoteCall(batch: { path: fs.PathLike }[]): Promise<(IStats | Error)[]> {
return this.proxy.statBatch(batch);
}
}
class LstatBatch extends Batch<IStats, { path: fs.PathLike }> {
public constructor(private readonly proxy: FsModuleProxy) {
super();
}
protected remoteCall(batch: { path: fs.PathLike }[]): Promise<(IStats | Error)[]> {
return this.proxy.lstatBatch(batch);
}
}
class ReaddirBatch extends Batch<Buffer[] | fs.Dirent[] | string[], { path: fs.PathLike, options: IEncodingOptions }> {
public constructor(private readonly proxy: FsModuleProxy) {
super();
}
protected remoteCall(queue: { path: fs.PathLike, options: IEncodingOptions }[]): Promise<(Buffer[] | fs.Dirent[] | string[] | Error)[]> {
return this.proxy.readdirBatch(queue);
}
}
interface ClientWatcherProxy extends WatcherProxy, ClientServerProxy<fs.FSWatcher> {}
class Watcher extends ClientProxy<ClientWatcherProxy> implements fs.FSWatcher {
public close(): void {
this.catch(this.proxy.close());
}
protected handleDisconnect(): void {
this.emit("close");
}
}
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");
}
public get path(): string | Buffer {
throw new Error("not implemented");
}
public close(): void {
this.catch(this.proxy.close());
}
}
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: ClientFsModuleProxy) {
this.statBatch = new StatBatch(this.proxy);
this.lstatBatch = new LstatBatch(this.proxy);
this.readdirBatch = new ReaddirBatch(this.proxy);
}
public access = (path: fs.PathLike, mode: number | undefined | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
callbackify(this.proxy.access)(path, mode, callback!);
}
public appendFile = (path: fs.PathLike | number, data: any, options?: fs.WriteFileOptions | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.appendFile)(path, data, options, callback!);
}
public chmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.chmod)(path, mode, callback!);
}
public chown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.chown)(path, uid, gid, callback!);
}
public close = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.close)(fd, callback!);
}
public copyFile = (src: fs.PathLike, dest: fs.PathLike, flags: number | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof flags === "function") {
callback = flags;
}
callbackify(this.proxy.copyFile)(
src, dest, typeof flags !== "function" ? flags : undefined, callback!,
);
}
public 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));
}
public exists = (path: fs.PathLike, callback: (exists: boolean) => void): void => {
this.proxy.exists(path).then((exists) => callback(exists)).catch(() => callback(false));
}
public fchmod = (fd: number, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.fchmod)(fd, mode, callback!);
}
public fchown = (fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.fchown)(fd, uid, gid, callback!);
}
public fdatasync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.fdatasync)(fd, callback!);
}
public fstat = (fd: number, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
callbackify(this.proxy.fstat)(fd, (error, stats) => {
callback(error, stats && new Stats(stats));
});
}
public fsync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.fsync)(fd, callback!);
}
public ftruncate = (fd: number, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof len === "function") {
callback = len;
len = undefined;
}
callbackify(this.proxy.ftruncate)(fd, len, callback!);
}
public futimes = (fd: number, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.futimes)(fd, atime, mtime, callback!);
}
public lchmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.lchmod)(path, mode, callback!);
}
public lchown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.lchown)(path, uid, gid, callback!);
}
public link = (existingPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.link)(existingPath, newPath, callback!);
}
public lstat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
callbackify(this.lstatBatch.add)({ path }, (error, stats) => {
callback(error, stats && new Stats(stats));
});
}
public mkdir = (path: fs.PathLike, mode: number | string | fs.MakeDirectoryOptions | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
callbackify(this.proxy.mkdir)(path, mode, callback!);
}
public mkdtemp = (prefix: string, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, folder: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.mkdtemp)(prefix, options, callback!);
}
public open = (path: fs.PathLike, flags: string | number, mode: string | number | undefined | null | ((err: NodeJS.ErrnoException, fd: number) => void), callback?: (err: NodeJS.ErrnoException, fd: number) => void): void => {
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
callbackify(this.proxy.open)(path, flags, mode, callback!);
}
public read = (fd: number, buffer: Buffer, offset: number, length: number, position: number | null, callback: (err: NodeJS.ErrnoException, bytesRead: number, buffer: Buffer) => void): void => {
this.proxy.read(fd, length, position).then((response) => {
buffer.set(response.buffer, offset);
callback(undefined!, response.bytesRead, response.buffer);
}).catch((error) => {
callback(error, undefined!, undefined!);
});
}
public readFile = (path: fs.PathLike | number, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, data: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.readFile)(path, options, callback!);
}
public readdir = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, files: Buffer[] | fs.Dirent[] | string[]) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.readdirBatch.add)({ path, options }, callback!);
}
public readlink = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, linkString: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.readlink)(path, options, callback!);
}
public realpath = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, resolvedPath: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.realpath)(path, options, callback!);
}
public rename = (oldPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.rename)(oldPath, newPath, callback!);
}
public rmdir = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.rmdir)(path, callback!);
}
public stat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
callbackify(this.statBatch.add)({ path }, (error, stats) => {
callback(error, stats && new Stats(stats));
});
}
public symlink = (target: fs.PathLike, path: fs.PathLike, type: fs.symlink.Type | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof type === "function") {
callback = type;
type = undefined;
}
callbackify(this.proxy.symlink)(target, path, type, callback!);
}
public truncate = (path: fs.PathLike, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof len === "function") {
callback = len;
len = undefined;
}
callbackify(this.proxy.truncate)(path, len, callback!);
}
public unlink = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.unlink)(path, callback!);
}
public utimes = (path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.utimes)(path, atime, mtime, callback!);
}
public write = (fd: number, buffer: Buffer, offset: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void), length: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void), position: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void), callback?: (err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void): void => {
if (typeof offset === "function") {
callback = offset;
offset = undefined;
}
if (typeof length === "function") {
callback = length;
length = undefined;
}
if (typeof position === "function") {
callback = position;
position = undefined;
}
this.proxy.write(fd, buffer, offset, length, position).then((r) => {
callback!(undefined!, r.bytesWritten, r.buffer);
}).catch((error) => {
callback!(error, undefined!, undefined!);
});
}
public writeFile = (path: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.writeFile)(path, data, options, callback!);
}
public watch = (filename: fs.PathLike, options?: IEncodingOptions | ((event: string, filename: string | Buffer) => void), listener?: ((event: string, filename: string | Buffer) => void)): fs.FSWatcher => {
if (typeof options === "function") {
listener = options;
options = undefined;
}
const watcher = new Watcher(this.proxy.watch(filename, options));
if (listener) {
watcher.on("change", listener);
}
return watcher;
}
}
class Stats implements fs.Stats {
public constructor(private readonly stats: IStats) {}
public get dev(): number { return this.stats.dev; }
public get ino(): number { return this.stats.ino; }
public get mode(): number { return this.stats.mode; }
public get nlink(): number { return this.stats.nlink; }
public get uid(): number { return this.stats.uid; }
public get gid(): number { return this.stats.gid; }
public get rdev(): number { return this.stats.rdev; }
public get size(): number { return this.stats.size; }
public get blksize(): number { return this.stats.blksize; }
public get blocks(): number { return this.stats.blocks; }
public get atime(): Date { return this.stats.atime; }
public get mtime(): Date { return this.stats.mtime; }
public get ctime(): Date { return this.stats.ctime; }
public get birthtime(): Date { return this.stats.birthtime; }
public get atimeMs(): number { return this.stats.atimeMs; }
public get mtimeMs(): number { return this.stats.mtimeMs; }
public get ctimeMs(): number { return this.stats.ctimeMs; }
public get birthtimeMs(): number { return this.stats.birthtimeMs; }
public isFile(): boolean { return this.stats._isFile; }
public isDirectory(): boolean { return this.stats._isDirectory; }
public isBlockDevice(): boolean { return this.stats._isBlockDevice; }
public isCharacterDevice(): boolean { return this.stats._isCharacterDevice; }
public isSymbolicLink(): boolean { return this.stats._isSymbolicLink; }
public isFIFO(): boolean { return this.stats._isFIFO; }
public isSocket(): boolean { return this.stats._isSocket; }
public toObject(): object {
return JSON.parse(JSON.stringify(this));
}
}

View File

@@ -0,0 +1,6 @@
export * from "./child_process";
export * from "./fs";
export * from "./net";
export * from "./node-pty";
export * from "./spdlog";
export * from "./trash";

View File

@@ -0,0 +1,296 @@
import * as net from "net";
import { callbackify } from "util";
import { ClientProxy, ClientServerProxy } from "../../common/proxy";
import { NetModuleProxy, NetServerProxy, NetSocketProxy } from "../../node/modules/net";
import { Duplex } from "./stream";
// tslint:disable completed-docs
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<ClientNetSocketProxy> | ClientNetSocketProxy, connecting?: boolean) {
super(proxyPromise);
if (connecting) {
this._connecting = connecting;
}
this.on("close", () => {
this._destroyed = true;
this._connecting = false;
});
this.on("connect", () => this._connecting = false);
}
public connect(options: number | string | net.SocketConnectOpts, host?: string | Function, callback?: Function): this {
if (typeof host === "function") {
callback = host;
host = undefined;
}
this._connecting = true;
if (callback) {
this.on("connect", callback as () => void);
}
return this.catch(this.proxy.connect(options, host));
}
// tslint:disable-next-line no-any
public end(data?: any, encoding?: string | Function, callback?: Function): void {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
callbackify(this.proxy.end)(data, encoding, () => {
if (callback) {
callback();
}
});
}
// tslint:disable-next-line no-any
public write(data: any, encoding?: string | Function, fd?: string | Function): boolean {
let callback: undefined | Function;
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
if (typeof fd === "function") {
callback = fd;
fd = undefined;
}
if (typeof fd !== "undefined") {
throw new Error("fd argument not supported");
}
callbackify(this.proxy.write)(data, encoding, () => {
if (callback) {
callback();
}
});
return true; // Always true since we can't get this synchronously.
}
public get connecting(): boolean {
return this._connecting;
}
public get destroyed(): boolean {
return this._destroyed;
}
public get bufferSize(): number {
throw new Error("not implemented");
}
public get bytesRead(): number {
throw new Error("not implemented");
}
public get bytesWritten(): number {
throw new Error("not implemented");
}
public get localAddress(): string {
throw new Error("not implemented");
}
public get localPort(): number {
throw new Error("not implemented");
}
public address(): net.AddressInfo | string {
throw new Error("not implemented");
}
public setTimeout(): this {
throw new Error("not implemented");
}
public setNoDelay(): this {
throw new Error("not implemented");
}
public setKeepAlive(): this {
throw new Error("not implemented");
}
public unref(): void {
this.catch(this.proxy.unref());
}
public ref(): void {
this.catch(this.proxy.ref());
}
}
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<ClientNetServerProxy> | ClientNetServerProxy) {
super(proxyPromise);
this.catch(this.proxy.onConnection((socketProxy) => {
const socket = new Socket(socketProxy);
const socketId = this.socketId++;
this.sockets.set(socketId, socket);
socket.on("error", () => this.sockets.delete(socketId));
socket.on("close", () => this.sockets.delete(socketId));
this.emit("connection", socket);
}));
this.on("listening", () => this._listening = true);
this.on("error", () => this._listening = false);
this.on("close", () => this._listening = false);
}
public listen(handle?: net.ListenOptions | number | string, hostname?: string | number | Function, backlog?: number | Function, callback?: Function): this {
if (typeof hostname === "function") {
callback = hostname;
hostname = undefined;
}
if (typeof backlog === "function") {
callback = backlog;
backlog = undefined;
}
if (callback) {
this.on("listening", callback as () => void);
}
return this.catch(this.proxy.listen(handle, hostname, backlog));
}
public get connections(): number {
return this.sockets.size;
}
public get listening(): boolean {
return this._listening;
}
public get maxConnections(): number {
throw new Error("not implemented");
}
public address(): net.AddressInfo | string {
throw new Error("not implemented");
}
public close(callback?: () => void): this {
this._listening = false;
if (callback) {
this.on("close", callback);
}
return this.catch(this.proxy.close());
}
public ref(): this {
return this.catch(this.proxy.ref());
}
public unref(): this {
return this.catch(this.proxy.unref());
}
public getConnections(cb: (error: Error | null, count: number) => void): void {
cb(null, this.sockets.size);
}
protected handleDisconnect(): void {
this.emit("close");
}
}
type NodeNet = typeof net;
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: 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.
this.Socket = class extends Socket {
public constructor(options?: net.SocketConstructorOpts) {
super(proxy.createSocket(options));
}
};
this.Server = class extends Server {
public constructor(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: Socket) => void), listener?: (socket: Socket) => void) {
super(proxy.createServer(typeof options !== "function" ? options : undefined));
if (typeof options === "function") {
listener = options;
}
if (listener) {
this.on("connection", listener);
}
}
};
}
public createConnection = (target: string | number | net.NetConnectOpts, host?: string | Function, callback?: Function): net.Socket => {
if (typeof host === "function") {
callback = host;
host = undefined;
}
const socket = new Socket(this.proxy.createConnection(target, host), true);
if (callback) {
socket.on("connect", callback as () => void);
}
return socket;
}
public createServer = (
options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: net.Socket) => void),
callback?: (socket: net.Socket) => void,
): net.Server => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
const server = new Server(this.proxy.createServer(options));
if (callback) {
server.on("connection", callback);
}
return server;
}
public connect = (): net.Socket => {
throw new Error("not implemented");
}
public isIP = (_input: string): number => {
throw new Error("not implemented");
}
public isIPv4 = (_input: string): boolean => {
throw new Error("not implemented");
}
public isIPv6 = (_input: string): boolean => {
throw new Error("not implemented");
}
}

View File

@@ -0,0 +1,79 @@
import * as pty from "node-pty";
import { ClientProxy, ClientServerProxy } from "../../common/proxy";
import { NodePtyModuleProxy, NodePtyProcessProxy } from "../../node/modules/node-pty";
// tslint:disable completed-docs
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: ClientNodePtyModuleProxy,
private readonly file: string,
private readonly args: string[] | string,
private readonly options: pty.IPtyForkOptions,
) {
super(moduleProxy.spawn(file, args, options));
this.on("process", (process) => this._process = process);
}
protected initialize(proxyPromise: Promise<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 {
return this._pid;
}
public get process(): string {
return this._process;
}
public resize(columns: number, rows: number): void {
this.lastCols = columns;
this.lastRows = rows;
this.catch(this.proxy.resize(columns, rows));
}
public write(data: string): void {
this.catch(this.proxy.write(data));
}
public kill(signal?: string): void {
this.catch(this.proxy.kill(signal));
}
protected handleDisconnect(): void {
this._process += " (disconnected)";
this.emit("data", "\r\n\nLost connection...\r\n\n");
this.initialize(this.moduleProxy.spawn(this.file, this.args, {
...this.options,
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: ClientNodePtyModuleProxy) {}
public spawn = (file: string, args: string[] | string, options: pty.IPtyForkOptions): pty.IPty => {
return new NodePtyProcess(this.proxy, file, args, options);
}
}

View File

@@ -0,0 +1,62 @@
import * as spdlog from "spdlog";
import { ClientProxy, ClientServerProxy } from "../../common/proxy";
import { RotatingLoggerProxy, SpdlogModuleProxy } from "../../node/modules/spdlog";
// tslint:disable completed-docs
interface ClientRotatingLoggerProxy extends RotatingLoggerProxy, ClientServerProxy {}
class RotatingLogger extends ClientProxy<ClientRotatingLoggerProxy> implements spdlog.RotatingLogger {
public constructor(
private readonly moduleProxy: ClientSpdlogModuleProxy,
private readonly name: string,
private readonly filename: string,
private readonly filesize: number,
private readonly filecount: number,
) {
super(moduleProxy.createLogger(name, filename, filesize, filecount));
}
public trace (message: string): void { this.catch(this.proxy.trace(message)); }
public debug (message: string): void { this.catch(this.proxy.debug(message)); }
public info (message: string): void { this.catch(this.proxy.info(message)); }
public warn (message: string): void { this.catch(this.proxy.warn(message)); }
public error (message: string): void { this.catch(this.proxy.error(message)); }
public critical (message: string): void { this.catch(this.proxy.critical(message)); }
public setLevel (level: number): void { this.catch(this.proxy.setLevel(level)); }
public clearFormatters (): void { this.catch(this.proxy.clearFormatters()); }
public flush (): void { this.catch(this.proxy.flush()); }
public drop (): void { this.catch(this.proxy.drop()); }
protected handleDisconnect(): void {
this.initialize(this.moduleProxy.createLogger(this.name, this.filename, this.filesize, this.filecount));
}
}
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: ClientSpdlogModuleProxy) {
this.RotatingLogger = class extends RotatingLogger {
public constructor(name: string, filename: string, filesize: number, filecount: number) {
super(proxy, name, filename, filesize, filecount);
}
};
}
public setAsyncMode = (bufferSize: number, flushInterval: number): Promise<void> => {
return this.proxy.setAsyncMode(bufferSize, flushInterval);
}
public createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): RotatingLogger {
return new RotatingLogger(this.proxy, name, filename, filesize, filecount);
}
public createRotatingLoggerAsync(name: string, filename: string, filesize: number, filecount: number): Promise<RotatingLogger> {
return Promise.resolve(this.createRotatingLogger(name, filename, filesize, filecount));
}
}

View File

@@ -0,0 +1,257 @@
import * as stream from "stream";
import { callbackify } from "util";
import { ClientProxy, ClientServerProxy } from "../../common/proxy";
import { isPromise } from "../../common/util";
import { DuplexProxy, ReadableProxy, WritableProxy } from "../../node/modules/stream";
// tslint:disable completed-docs no-any
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");
}
public get writableHighWaterMark(): number {
throw new Error("not implemented");
}
public get writableLength(): number {
throw new Error("not implemented");
}
public _write(): void {
throw new Error("not implemented");
}
public _destroy(): void {
throw new Error("not implemented");
}
public _final(): void {
throw new Error("not implemented");
}
public pipe<T>(): T {
throw new Error("not implemented");
}
public cork(): void {
throw new Error("not implemented");
}
public uncork(): void {
throw new Error("not implemented");
}
public destroy(): void {
this.catch(this.proxy.destroy());
}
public setDefaultEncoding(encoding: string): this {
return this.catch(this.proxy.setDefaultEncoding(encoding));
}
public write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
callbackify(this.proxy.write)(chunk, encoding, (error) => {
if (callback) {
callback(error);
}
});
return true; // Always true since we can't get this synchronously.
}
public end(data?: any | (() => void), encoding?: string | (() => void), callback?: (() => void)): void {
if (typeof data === "function") {
callback = data;
data = undefined;
}
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
callbackify(this.proxy.end)(data, encoding, () => {
if (callback) {
callback();
}
});
}
protected handleDisconnect(): void {
this.emit("close");
this.emit("finish");
}
}
export 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");
}
public get readableHighWaterMark(): number {
throw new Error("not implemented");
}
public get readableLength(): number {
throw new Error("not implemented");
}
public _read(): void {
throw new Error("not implemented");
}
public read(): void {
throw new Error("not implemented");
}
public _destroy(): void {
throw new Error("not implemented");
}
public unpipe(): this {
throw new Error("not implemented");
}
public pause(): this {
throw new Error("not implemented");
}
public resume(): this {
throw new Error("not implemented");
}
public isPaused(): boolean {
throw new Error("not implemented");
}
public wrap(): this {
throw new Error("not implemented");
}
public push(): boolean {
throw new Error("not implemented");
}
public unshift(): void {
throw new Error("not implemented");
}
public pipe<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;
}
public [Symbol.asyncIterator](): AsyncIterableIterator<any> {
throw new Error("not implemented");
}
public destroy(): void {
this.catch(this.proxy.destroy());
}
public setEncoding(encoding: string): this {
return this.catch(this.proxy.setEncoding(encoding));
}
protected handleDisconnect(): void {
this.emit("close");
this.emit("end");
}
}
export 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) {
super(proxyPromise);
this._readable = new Readable(proxyPromise, false);
}
public get readable(): boolean {
return this._readable.readable;
}
public get readableHighWaterMark(): number {
return this._readable.readableHighWaterMark;
}
public get readableLength(): number {
return this._readable.readableLength;
}
public _read(): void {
this._readable._read();
}
public read(): void {
this._readable.read();
}
public unpipe(): this {
this._readable.unpipe();
return this;
}
public pause(): this {
this._readable.unpipe();
return this;
}
public resume(): this {
this._readable.resume();
return this;
}
public isPaused(): boolean {
return this._readable.isPaused();
}
public wrap(): this {
this._readable.wrap();
return this;
}
public push(): boolean {
return this._readable.push();
}
public unshift(): void {
this._readable.unshift();
}
public [Symbol.asyncIterator](): AsyncIterableIterator<any> {
return this._readable[Symbol.asyncIterator]();
}
public setEncoding(encoding: string): this {
return this.catch(this.proxy.setEncoding(encoding));
}
protected handleDisconnect(): void {
super.handleDisconnect();
this.emit("end");
}
}

View File

@@ -0,0 +1,15 @@
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: ClientTrashModuleProxy) {}
public trash = (path: string, options?: trash.Options): Promise<void> => {
return this.proxy.trash(path, options);
}
}

View File

@@ -1,3 +1,5 @@
import * as jspb from "google-protobuf";
export interface SendableConnection {
send(data: Buffer | Uint8Array): void;
}
@@ -5,6 +7,8 @@ export interface SendableConnection {
export interface ReadWriteConnection extends SendableConnection {
onMessage(cb: (data: Uint8Array | Buffer) => void): void;
onClose(cb: () => void): void;
onDown(cb: () => void): void;
onUp(cb: () => void): void;
close(): void;
}
@@ -21,7 +25,11 @@ export interface InitData {
readonly homeDirectory: string;
readonly tmpDirectory: string;
readonly shell: string;
readonly extensionsDirectory: string;
readonly builtInExtensionsDirectory: string;
readonly extraExtensionDirectories: string[];
readonly extraBuiltinExtensionDirectories: string[];
readonly env: jspb.Map<string, string>;
}
export interface SharedProcessData {

View File

@@ -1,418 +0,0 @@
/// <reference path="../../../../lib/vscode/src/typings/spdlog.d.ts" />
import { ChildProcess, SpawnOptions, ForkOptions } from "child_process";
import { EventEmitter } from "events";
import { Socket } from "net";
import { Duplex, Readable, Writable } from "stream";
import { IDisposable } from "@coder/disposable";
import { logger } from "@coder/logger";
// tslint:disable no-any
export type ForkProvider = (modulePath: string, args: string[], options: ForkOptions) => ChildProcess;
export interface Disposer extends IDisposable {
onDidDispose: (cb: () => void) => void;
}
interface ActiveEvalEmitter {
removeAllListeners(event?: string): void;
emit(event: string, ...args: any[]): void;
on(event: string, cb: (...args: any[]) => void): void;
}
/**
* Helper class for server-side evaluations.
*/
export class EvalHelper {
// For any non-external modules that are not built in, we need to require and
// access them here. A require on the client-side won't work since that code
// won't exist on the server (and bloat the client with an unused import), and
// we can't manually import on the server-side and then call
// `__webpack_require__` on the client-side because Webpack stores modules by
// their paths which would require us to hard-code the path. These aren't
// required immediately so we have a chance to unpack the .node files and set
// their locations.
public modules = {
spdlog: require("spdlog") as typeof import("spdlog"),
pty: require("node-pty-prebuilt") as typeof import("node-pty"),
};
/**
* Some spawn code tries to preserve the env (the debug adapter for instance)
* but the env is mostly blank (since we're in the browser), so we'll just
* always preserve the main process.env here, otherwise it won't have access
* to PATH, etc.
* TODO: An alternative solution would be to send the env to the browser?
*/
public preserveEnv(options: SpawnOptions | ForkOptions): void {
if (options && options.env) {
options.env = { ...process.env, ...options.env };
}
}
}
/**
* Helper class for client-side active evaluations.
*/
export class ActiveEvalHelper implements ActiveEvalEmitter {
public constructor(private readonly emitter: ActiveEvalEmitter) {}
public removeAllListeners(event?: string): void {
this.emitter.removeAllListeners(event);
}
public emit(event: string, ...args: any[]): void {
this.emitter.emit(event, ...args);
}
public on(event: string, cb: (...args: any[]) => void): void {
this.emitter.on(event, cb);
}
/**
* Create a new helper to make unique events for an item.
*/
public createUnique(id: number | "stdout" | "stderr" | "stdin"): ActiveEvalHelper {
return new ActiveEvalHelper(this.createUniqueEmitter(id));
}
/**
* Wrap the evaluation emitter to make unique events for an item to prevent
* conflicts when it shares that emitter with other items.
*/
protected createUniqueEmitter(id: number | "stdout" | "stderr" | "stdin"): ActiveEvalEmitter {
let events = <string[]>[];
return {
removeAllListeners: (event?: string): void => {
if (!event) {
events.forEach((e) => this.removeAllListeners(e));
events = [];
} else {
const index = events.indexOf(event);
if (index !== -1) {
events.splice(index, 1);
this.removeAllListeners(`${event}:${id}`);
}
}
},
emit: (event: string, ...args: any[]): void => {
this.emit(`${event}:${id}`, ...args);
},
on: (event: string, cb: (...args: any[]) => void): void => {
if (!events.includes(event)) {
events.push(event);
}
this.on(`${event}:${id}`, cb);
},
};
}
}
/**
* Helper class for server-side active evaluations.
*/
export class ServerActiveEvalHelper extends ActiveEvalHelper implements EvalHelper {
private readonly evalHelper = new EvalHelper();
public modules = this.evalHelper.modules;
public constructor(emitter: ActiveEvalEmitter, public readonly fork: ForkProvider) {
super(emitter);
}
public preserveEnv(options: SpawnOptions | ForkOptions): void {
this.evalHelper.preserveEnv(options);
}
/**
* If there is a callback ID, return a function that emits the callback event
* on the active evaluation with that ID and all arguments passed to it.
* Otherwise, return undefined.
*/
public maybeCallback(callbackId?: number): ((...args: any[]) => void) | undefined {
return typeof callbackId !== "undefined" ? (...args: any[]): void => {
this.emit("callback", callbackId, ...args);
} : undefined;
}
/**
* Bind a socket to an active evaluation and returns a disposer.
*/
public bindSocket(socket: Socket): Disposer {
socket.on("connect", () => this.emit("connect"));
socket.on("lookup", (error, address, family, host) => this.emit("lookup", error, address, family, host));
socket.on("timeout", () => this.emit("timeout"));
this.on("connect", (options, callbackId) => socket.connect(options, this.maybeCallback(callbackId)));
this.on("ref", () => socket.ref());
this.on("setKeepAlive", (enable, initialDelay) => socket.setKeepAlive(enable, initialDelay));
this.on("setNoDelay", (noDelay) => socket.setNoDelay(noDelay));
this.on("setTimeout", (timeout, callbackId) => socket.setTimeout(timeout, this.maybeCallback(callbackId)));
this.on("unref", () => socket.unref());
this.bindReadable(socket);
this.bindWritable(socket);
return {
onDidDispose: (cb): Socket => socket.on("close", cb),
dispose: (): void => {
socket.removeAllListeners();
socket.end();
socket.destroy();
socket.unref();
},
};
}
/**
* Bind a writable stream to the active evaluation.
*/
public bindWritable(writable: Writable | Duplex): void {
if (!((writable as Readable).read)) { // To avoid binding twice.
writable.on("close", () => this.emit("close"));
writable.on("error", (error) => this.emit("error", error));
this.on("destroy", () => writable.destroy());
}
writable.on("drain", () => this.emit("drain"));
writable.on("finish", () => this.emit("finish"));
writable.on("pipe", () => this.emit("pipe"));
writable.on("unpipe", () => this.emit("unpipe"));
this.on("cork", () => writable.cork());
this.on("end", (chunk, encoding, callbackId) => writable.end(chunk, encoding, this.maybeCallback(callbackId)));
this.on("setDefaultEncoding", (encoding) => writable.setDefaultEncoding(encoding));
this.on("uncork", () => writable.uncork());
// Sockets can pass an fd instead of a callback but streams cannot.
this.on("write", (chunk, encoding, fd, callbackId) => writable.write(chunk, encoding, this.maybeCallback(callbackId) || fd));
}
/**
* Bind a readable stream to the active evaluation.
*/
public bindReadable(readable: Readable): void {
// Streams don't have an argument on close but sockets do.
readable.on("close", (...args: any[]) => this.emit("close", ...args));
readable.on("data", (data) => this.emit("data", data));
readable.on("end", () => this.emit("end"));
readable.on("error", (error) => this.emit("error", error));
readable.on("readable", () => this.emit("readable"));
this.on("destroy", () => readable.destroy());
this.on("pause", () => readable.pause());
this.on("push", (chunk, encoding) => readable.push(chunk, encoding));
this.on("resume", () => readable.resume());
this.on("setEncoding", (encoding) => readable.setEncoding(encoding));
this.on("unshift", (chunk) => readable.unshift(chunk));
}
public createUnique(id: number | "stdout" | "stderr" | "stdin"): ServerActiveEvalHelper {
return new ServerActiveEvalHelper(this.createUniqueEmitter(id), this.fork);
}
}
/**
* An event emitter that can store callbacks with IDs in a map so we can pass
* them back and forth through an active evaluation using those IDs.
*/
export class CallbackEmitter extends EventEmitter {
private _ae: ActiveEvalHelper | undefined;
private callbackId = 0;
private readonly callbacks = new Map<number, Function>();
public constructor(ae?: ActiveEvalHelper) {
super();
if (ae) {
this.ae = ae;
}
}
protected get ae(): ActiveEvalHelper {
if (!this._ae) {
throw new Error("trying to access active evaluation before it has been set");
}
return this._ae;
}
protected set ae(ae: ActiveEvalHelper) {
if (this._ae) {
throw new Error("cannot override active evaluation");
}
this._ae = ae;
this.ae.on("callback", (callbackId, ...args: any[]) => this.runCallback(callbackId, ...args));
}
/**
* Store the callback and return and ID referencing its location in the map.
*/
protected storeCallback(callback?: Function): number | undefined {
if (!callback) {
return undefined;
}
const callbackId = this.callbackId++;
this.callbacks.set(callbackId, callback);
return callbackId;
}
/**
* Call the function with the specified ID and delete it from the map.
* If the ID is undefined or doesn't exist, nothing happens.
*/
private runCallback(callbackId?: number, ...args: any[]): void {
const callback = typeof callbackId !== "undefined" && this.callbacks.get(callbackId);
if (callback && typeof callbackId !== "undefined") {
this.callbacks.delete(callbackId);
callback(...args);
}
}
}
/**
* A writable stream over an active evaluation.
*/
export class ActiveEvalWritable extends CallbackEmitter implements Writable {
public constructor(ae: ActiveEvalHelper) {
super(ae);
// Streams don't have an argument on close but sockets do.
this.ae.on("close", (...args: any[]) => this.emit("close", ...args));
this.ae.on("drain", () => this.emit("drain"));
this.ae.on("error", (error) => this.emit("error", error));
this.ae.on("finish", () => this.emit("finish"));
this.ae.on("pipe", () => logger.warn("pipe is not supported"));
this.ae.on("unpipe", () => logger.warn("unpipe is not supported"));
}
public get writable(): boolean { throw new Error("not implemented"); }
public get writableHighWaterMark(): number { throw new Error("not implemented"); }
public get writableLength(): number { throw new Error("not implemented"); }
public _write(): void { throw new Error("not implemented"); }
public _destroy(): void { throw new Error("not implemented"); }
public _final(): void { throw new Error("not implemented"); }
public pipe<T>(): T { throw new Error("not implemented"); }
public cork(): void { this.ae.emit("cork"); }
public destroy(): void { this.ae.emit("destroy"); }
public setDefaultEncoding(encoding: string): this {
this.ae.emit("setDefaultEncoding", encoding);
return this;
}
public uncork(): void { this.ae.emit("uncork"); }
public write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
// Sockets can pass an fd instead of a callback but streams cannot..
this.ae.emit("write", chunk, encoding, undefined, this.storeCallback(callback));
// Always true since we can't get this synchronously.
return true;
}
public end(data?: any, encoding?: string | Function, callback?: Function): void {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
this.ae.emit("end", data, encoding, this.storeCallback(callback));
}
}
/**
* A readable stream over an active evaluation.
*/
export class ActiveEvalReadable extends CallbackEmitter implements Readable {
public constructor(ae: ActiveEvalHelper) {
super(ae);
this.ae.on("close", () => this.emit("close"));
this.ae.on("data", (data) => this.emit("data", data));
this.ae.on("end", () => this.emit("end"));
this.ae.on("error", (error) => this.emit("error", error));
this.ae.on("readable", () => this.emit("readable"));
}
public get readable(): boolean { throw new Error("not implemented"); }
public get readableHighWaterMark(): number { throw new Error("not implemented"); }
public get readableLength(): number { throw new Error("not implemented"); }
public _read(): void { throw new Error("not implemented"); }
public read(): any { throw new Error("not implemented"); }
public isPaused(): boolean { throw new Error("not implemented"); }
public pipe<T>(): T { throw new Error("not implemented"); }
public unpipe(): this { throw new Error("not implemented"); }
public unshift(): this { throw new Error("not implemented"); }
public wrap(): this { throw new Error("not implemented"); }
public push(): boolean { throw new Error("not implemented"); }
public _destroy(): void { throw new Error("not implemented"); }
public [Symbol.asyncIterator](): AsyncIterableIterator<any> { throw new Error("not implemented"); }
public destroy(): void { this.ae.emit("destroy"); }
public pause(): this { return this.emitReturnThis("pause"); }
public resume(): this { return this.emitReturnThis("resume"); }
public setEncoding(encoding?: string): this { return this.emitReturnThis("setEncoding", encoding); }
// tslint:disable-next-line no-any
protected emitReturnThis(event: string, ...args: any[]): this {
this.ae.emit(event, ...args);
return this;
}
}
/**
* An duplex stream over an active evaluation.
*/
export class ActiveEvalDuplex extends ActiveEvalReadable implements Duplex {
// Some unfortunate duplication here since we can't have multiple extends.
public constructor(ae: ActiveEvalHelper) {
super(ae);
this.ae.on("drain", () => this.emit("drain"));
this.ae.on("finish", () => this.emit("finish"));
this.ae.on("pipe", () => logger.warn("pipe is not supported"));
this.ae.on("unpipe", () => logger.warn("unpipe is not supported"));
}
public get writable(): boolean { throw new Error("not implemented"); }
public get writableHighWaterMark(): number { throw new Error("not implemented"); }
public get writableLength(): number { throw new Error("not implemented"); }
public _write(): void { throw new Error("not implemented"); }
public _destroy(): void { throw new Error("not implemented"); }
public _final(): void { throw new Error("not implemented"); }
public pipe<T>(): T { throw new Error("not implemented"); }
public cork(): void { this.ae.emit("cork"); }
public destroy(): void { this.ae.emit("destroy"); }
public setDefaultEncoding(encoding: string): this {
this.ae.emit("setDefaultEncoding", encoding);
return this;
}
public uncork(): void { this.ae.emit("uncork"); }
public write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
// Sockets can pass an fd instead of a callback but streams cannot..
this.ae.emit("write", chunk, encoding, undefined, this.storeCallback(callback));
// Always true since we can't get this synchronously.
return true;
}
public end(data?: any, encoding?: string | Function, callback?: Function): void {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
this.ae.emit("end", data, encoding, this.storeCallback(callback));
}
}

View File

@@ -0,0 +1,325 @@
import { EventEmitter } from "events";
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 must return promises.
*/
const unpromisify = <T extends ClientServerProxy>(proxyPromise: Promise<T>): T => {
return new Proxy({}, {
get: (target: any, name: string): any => {
if (typeof target[name] === "undefined") {
target[name] = async (...args: any[]): Promise<any> => {
const proxy = await proxyPromise;
return proxy ? (proxy as any)[name](...args) : undefined;
};
}
return target[name];
},
});
};
/**
* Client-side emitter that just forwards 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 ClientServerProxy> extends EventEmitter {
private _proxy: T;
/**
* You can specify not to bind events in order to avoid emitting twice for
* duplex streams.
*/
public constructor(
private _proxyPromise: Promise<T> | T,
private readonly bindEvents: boolean = true,
) {
super();
this._proxy = this.initialize(this._proxyPromise);
if (this.bindEvents) {
this.on("disconnected", (error) => {
try {
this.emit("error", error);
} catch (error) {
// If nothing is listening, EventEmitter will throw an error.
}
this.handleDisconnect();
});
}
}
/**
* Remove an event listener.
*/
public off(event: string, cb: (...args: any[]) => void): this {
// Fill it here because the fill we're using to provide EventEmitter for the
// browser doesn't appear to include `off`.
this.removeListener(event, cb);
return this;
}
/**
* 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;
}
/**
* Initialize the proxy by unpromisifying if necessary and binding to its
* events.
*/
protected initialize(proxyPromise: Promise<T> | T): T {
this._proxyPromise = proxyPromise;
this._proxy = isPromise(this._proxyPromise)
? unpromisify(this._proxyPromise)
: this._proxyPromise;
if (this.bindEvents) {
this.proxy.onEvent((event, ...args): void => {
this.emit(event, ...args);
});
}
return this._proxy;
}
/**
* Perform necessary cleanup on disconnect (or reconnect).
*/
protected abstract handleDisconnect(): void;
/**
* Emit an error event if the promise errors.
*/
protected catch(promise?: Promise<any>): this {
if (promise) {
promise.catch((e) => this.emit("error", e));
}
return this;
}
}
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;
}
/**
* 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 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.
*/
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.
*/
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.
*
* This cannot be async because then we can bind to the events too late.
*/
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;
}
/**
* Supported top-level module proxies.
*/
export enum Module {
Fs = "fs",
ChildProcess = "child_process",
Net = "net",
Spdlog = "spdlog",
NodePty = "node-pty",
Trash = "trash",
}
interface BatchItem<T, A> {
args: A;
resolve: (t: T) => void;
reject: (e: Error) => void;
}
/**
* Batch remote calls.
*/
export abstract class Batch<T, A> {
private idleTimeout: number | NodeJS.Timer | undefined;
private maxTimeout: number | NodeJS.Timer | undefined;
private batch = <BatchItem<T, A>[]>[];
public constructor(
/**
* Flush after reaching this amount of time.
*/
private readonly maxTime: number = 1000,
/**
* Flush after reaching this count.
*/
private readonly maxCount: number = 100,
/**
* Flush after not receiving more requests for this amount of time.
* This is pretty low by default so essentially we just end up batching
* requests that are all made at the same time.
*/
private readonly idleTime: number = 1,
) {}
public add = (args: A): Promise<T> => {
return new Promise((resolve, reject): void => {
this.batch.push({
args,
resolve,
reject,
});
if (this.batch.length >= this.maxCount) {
this.flush();
} else {
clearTimeout(this.idleTimeout as any);
this.idleTimeout = setTimeout(this.flush, this.idleTime);
if (typeof this.maxTimeout === "undefined") {
this.maxTimeout = setTimeout(this.flush, this.maxTime);
}
}
});
}
/**
* Perform remote call for a batch.
*/
protected abstract remoteCall(batch: A[]): Promise<(T | Error)[]>;
/**
* Flush out the current batch.
*/
private readonly flush = (): void => {
clearTimeout(this.idleTimeout as any);
clearTimeout(this.maxTimeout as any);
this.maxTimeout = undefined;
const batch = this.batch;
this.batch = [];
this.remoteCall(batch.map((q) => q.args)).then((results) => {
batch.forEach((item, i) => {
const result = results[i];
if (result && result instanceof Error) {
item.reject(result);
} else {
item.resolve(result);
}
});
}).catch((error) => batch.forEach((item) => item.reject(error)));
}
}

View File

@@ -1,3 +1,9 @@
import { Argument, Module as ProtoModule, WorkingInit } from "../proto";
import { OperatingSystem } from "../common/connection";
import { ClientServerProxy, Module, ServerProxy } from "./proxy";
// tslint:disable no-any
/**
* Return true if we're in a browser environment (including web workers).
*/
@@ -13,87 +19,228 @@ export const escapePath = (path: string): string => {
return `'${path.replace(/'/g, "'\\''")}'`;
};
export type EventCallback = (event: string, ...args: any[]) => void;
export type IEncodingOptions = {
encoding?: string | null;
encoding?: BufferEncoding | null;
flag?: string;
mode?: string;
persistent?: boolean;
recursive?: boolean;
} | string | undefined | null;
} | BufferEncoding | undefined | null;
// tslint:disable-next-line no-any
export type IEncodingOptionsCallback = IEncodingOptions | ((err: NodeJS.ErrnoException, ...args: any[]) => void);
/**
* Stringify an event argument. isError is because although methods like
* `fs.stat` are supposed to throw Error objects, they currently throw regular
* objects when running tests through Jest.
* Convert an argument to proto.
* If sending a function is possible, provide `storeFunction`.
* If sending a proxy is possible, provide `storeProxy`.
*/
export const stringify = (arg: any, isError?: boolean): string => { // tslint:disable-line no-any
if (arg instanceof Error || isError) {
// Errors don't stringify at all. They just become "{}".
return JSON.stringify({
type: "Error",
data: {
message: arg.message,
stack: arg.stack,
code: (arg as NodeJS.ErrnoException).code,
},
});
} else if (arg instanceof Uint8Array) {
// With stringify, these get turned into objects with each index becoming a
// key for some reason. Then trying to do something like write that data
// results in [object Object] being written. Stringify them like a Buffer
// instead.
return JSON.stringify({
type: "Buffer",
data: Array.from(arg),
});
}
export const argumentToProto = <P = ClientServerProxy | ServerProxy>(
value: any,
storeFunction?: (fn: () => void) => number,
storeProxy?: (proxy: P) => number | Module,
): Argument => {
const convert = (currentValue: any): Argument => {
const message = new Argument();
return JSON.stringify(arg);
};
/**
* Parse an event argument.
*/
export const parse = (arg: string): any => { // tslint:disable-line no-any
const convert = (value: any): any => { // tslint:disable-line no-any
if (value && value.data && value.type) {
switch (value.type) {
// JSON.stringify turns a Buffer into an object but JSON.parse doesn't
// turn it back, it just remains an object.
case "Buffer":
if (Array.isArray(value.data)) {
return Buffer.from(value);
}
if (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();
arg.setMessage(currentValue.message);
arg.setStack(currentValue.stack);
arg.setCode(currentValue.code);
message.setError(arg);
} else if (currentValue instanceof Uint8Array || currentValue instanceof Buffer) {
const arg = new Argument.BufferValue();
arg.setData(currentValue);
message.setBuffer(arg);
} else if (Array.isArray(currentValue)) {
const arg = new Argument.ArrayValue();
arg.setDataList(currentValue.map(convert));
message.setArray(arg);
} else if (currentValue instanceof Date
|| (currentValue && typeof currentValue.getTime === "function")) {
const arg = new Argument.DateValue();
arg.setDate(currentValue.toString());
message.setDate(arg);
} else if (currentValue !== null && typeof currentValue === "object") {
const arg = new Argument.ObjectValue();
const map = arg.getDataMap();
Object.keys(currentValue).forEach((key) => {
map.set(key, convert(currentValue[key]));
});
message.setObject(arg);
} else if (currentValue === null) {
message.setNull(new Argument.NullValue());
} else {
switch (typeof currentValue) {
case "undefined":
message.setUndefined(new Argument.UndefinedValue());
break;
// Errors apparently can't be stringified, so we do something similar to
// what happens to buffers and stringify them as regular objects.
case "Error":
if (value.data.message) {
const error = new Error(value.data.message);
// TODO: Can we set the stack? Doing so seems to make it into an
// "invalid object".
if (typeof value.data.code !== "undefined") {
(error as NodeJS.ErrnoException).code = value.data.code;
}
// tslint:disable-next-line no-any
(error as any).originalStack = value.data.stack;
return error;
case "function":
if (!storeFunction) {
throw new Error("no way to serialize function");
}
const arg = new Argument.FunctionValue();
arg.setId(storeFunction(currentValue));
message.setFunction(arg);
break;
case "number":
message.setNumber(currentValue);
break;
case "string":
message.setString(currentValue);
break;
case "boolean":
message.setBoolean(currentValue);
break;
default:
throw new Error(`cannot convert ${typeof currentValue} to proto`);
}
}
if (value && typeof value === "object") {
Object.keys(value).forEach((key) => {
value[key] = convert(value[key]);
});
}
return value;
return message;
};
return arg ? convert(JSON.parse(arg)) : arg;
return convert(value);
};
/**
* Convert proto to an argument.
* If running a remote callback is supported, provide `runCallback`.
* If using a remote proxy is supported, provide `createProxy`.
*/
export const protoToArgument = (
message?: Argument,
runCallback?: (id: number, args: any[]) => void,
createProxy?: (id: number) => ServerProxy,
): any => {
const convert = (currentMessage: Argument): any => {
switch (currentMessage.getMsgCase()) {
case Argument.MsgCase.ERROR:
const errorMessage = currentMessage.getError()!;
const error = new Error(errorMessage.getMessage());
(error as NodeJS.ErrnoException).code = errorMessage.getCode();
(error as any).originalStack = errorMessage.getStack();
return error;
case Argument.MsgCase.BUFFER:
return Buffer.from(currentMessage.getBuffer()!.getData() as Uint8Array);
case Argument.MsgCase.ARRAY:
return currentMessage.getArray()!.getDataList().map((a) => convert(a));
case Argument.MsgCase.PROXY:
if (!createProxy) {
throw new Error("no way to create proxy");
}
return createProxy(currentMessage.getProxy()!.getId());
case Argument.MsgCase.DATE:
return new Date(currentMessage.getDate()!.getDate());
case Argument.MsgCase.OBJECT:
const obj: { [Key: string]: any } = {};
currentMessage.getObject()!.getDataMap().forEach((argument, key) => {
obj[key] = convert(argument);
});
return obj;
case Argument.MsgCase.UNDEFINED:
return undefined;
case Argument.MsgCase.NULL:
return null;
case Argument.MsgCase.FUNCTION:
if (!runCallback) {
throw new Error("no way to run remote callback");
}
return (...args: any[]): void => {
return runCallback(currentMessage.getFunction()!.getId(), args);
};
case Argument.MsgCase.NUMBER:
return currentMessage.getNumber();
case Argument.MsgCase.STRING:
return currentMessage.getString();
case Argument.MsgCase.BOOLEAN:
return currentMessage.getBoolean();
default:
throw new Error("cannot convert unexpected proto to argument");
}
};
return message && convert(message);
};
export const protoToModule = (protoModule: ProtoModule): Module => {
switch (protoModule) {
case ProtoModule.CHILDPROCESS: return Module.ChildProcess;
case ProtoModule.FS: return Module.Fs;
case ProtoModule.NET: return Module.Net;
case ProtoModule.NODEPTY: return Module.NodePty;
case ProtoModule.SPDLOG: return Module.Spdlog;
case ProtoModule.TRASH: return Module.Trash;
default: throw new Error(`invalid module ${protoModule}`);
}
};
export const moduleToProto = (moduleName: Module): ProtoModule => {
switch (moduleName) {
case Module.ChildProcess: return ProtoModule.CHILDPROCESS;
case Module.Fs: return ProtoModule.FS;
case Module.Net: return ProtoModule.NET;
case Module.NodePty: return ProtoModule.NODEPTY;
case Module.Spdlog: return ProtoModule.SPDLOG;
case Module.Trash: return ProtoModule.TRASH;
default: throw new Error(`invalid module "${moduleName}"`);
}
};
export const protoToOperatingSystem = (protoOp: WorkingInit.OperatingSystem): OperatingSystem => {
switch (protoOp) {
case WorkingInit.OperatingSystem.WINDOWS: return OperatingSystem.Windows;
case WorkingInit.OperatingSystem.LINUX: return OperatingSystem.Linux;
case WorkingInit.OperatingSystem.MAC: return OperatingSystem.Mac;
default: throw new Error(`unsupported operating system ${protoOp}`);
}
};
export const platformToProto = (platform: NodeJS.Platform): WorkingInit.OperatingSystem => {
switch (platform) {
case "win32": return WorkingInit.OperatingSystem.WINDOWS;
case "linux": return WorkingInit.OperatingSystem.LINUX;
case "darwin": return WorkingInit.OperatingSystem.MAC;
default: throw new Error(`unrecognized platform "${platform}"`);
}
};
export const isProxy = <P = ClientServerProxy | ServerProxy>(value: any): value is P => {
return value && typeof value === "object" && typeof value.onEvent === "function";
};
export const isPromise = (value: any): value is Promise<any> => {
return typeof value.then === "function" && typeof value.catch === "function";
};
/**
* When spawning VS Code tries to preserve the environment but since it's in
* the browser, it doesn't work.
*/
export const withEnv = <T extends { env?: NodeJS.ProcessEnv }>(options?: T): T | undefined => {
return options && options.env ? {
...options,
env: {
...process.env, ...options.env,
},
} : options;
};

View File

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

View File

@@ -1,151 +0,0 @@
import { fork as cpFork } from "child_process";
import { EventEmitter } from "events";
import * as vm from "vm";
import { logger, field } from "@coder/logger";
import { NewEvalMessage, EvalFailedMessage, EvalDoneMessage, ServerMessage, EvalEventMessage } from "../proto";
import { SendableConnection } from "../common/connection";
import { ServerActiveEvalHelper, EvalHelper, ForkProvider } from "../common/helpers";
import { stringify, parse } from "../common/util";
export interface ActiveEvaluation {
onEvent(msg: EvalEventMessage): void;
dispose(): void;
}
declare var __non_webpack_require__: typeof require;
export const evaluate = (connection: SendableConnection, message: NewEvalMessage, onDispose: () => void, fork?: ForkProvider): ActiveEvaluation | void => {
/**
* Send the response and call onDispose.
*/
// tslint:disable-next-line no-any
const sendResp = (resp: any): void => {
logger.trace(() => [
"resolve",
field("id", message.getId()),
field("response", stringify(resp)),
]);
const evalDone = new EvalDoneMessage();
evalDone.setId(message.getId());
evalDone.setResponse(stringify(resp));
const serverMsg = new ServerMessage();
serverMsg.setEvalDone(evalDone);
connection.send(serverMsg.serializeBinary());
onDispose();
};
/**
* Send an exception and call onDispose.
*/
const sendException = (error: Error): void => {
logger.trace(() => [
"reject",
field("id", message.getId()),
field("response", stringify(error, true)),
]);
const evalFailed = new EvalFailedMessage();
evalFailed.setId(message.getId());
evalFailed.setResponse(stringify(error, true));
const serverMsg = new ServerMessage();
serverMsg.setEvalFailed(evalFailed);
connection.send(serverMsg.serializeBinary());
onDispose();
};
let eventEmitter = message.getActive() ? new EventEmitter(): undefined;
const sandbox = {
helper: eventEmitter ? new ServerActiveEvalHelper({
removeAllListeners: (event?: string): void => {
eventEmitter!.removeAllListeners(event);
},
// tslint:disable no-any
on: (event: string, cb: (...args: any[]) => void): void => {
eventEmitter!.on(event, (...args: any[]) => {
logger.trace(() => [
`${event}`,
field("id", message.getId()),
field("args", args.map((a) => stringify(a))),
]);
cb(...args);
});
},
emit: (event: string, ...args: any[]): void => {
logger.trace(() => [
`emit ${event}`,
field("id", message.getId()),
field("args", args.map((a) => stringify(a))),
]);
const eventMsg = new EvalEventMessage();
eventMsg.setEvent(event);
eventMsg.setArgsList(args.map((a) => stringify(a)));
eventMsg.setId(message.getId());
const serverMsg = new ServerMessage();
serverMsg.setEvalEvent(eventMsg);
connection.send(serverMsg.serializeBinary());
},
// tslint:enable no-any
}, fork || cpFork) : new EvalHelper(),
_Buffer: Buffer,
// When the client is ran from Webpack, it will replace
// __non_webpack_require__ with require, which we then need to provide to
// the sandbox. Since the server might also be using Webpack, we need to set
// it to the non-Webpack version when that's the case. Then we need to also
// provide __non_webpack_require__ for when the client doesn't run through
// Webpack meaning it doesn't get replaced with require (Jest for example).
require: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require,
__non_webpack_require__: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require,
setTimeout,
setInterval,
clearTimeout,
process: {
env: process.env,
},
args: message.getArgsList().map(parse),
};
let value: any; // tslint:disable-line no-any
try {
const code = `(${message.getFunction()})(helper, ...args);`;
value = vm.runInNewContext(code, sandbox, {
// If the code takes longer than this to return, it is killed and throws.
timeout: message.getTimeout() || 15000,
});
} catch (ex) {
sendException(ex);
}
// An evaluation completes when the value it returns resolves. An active
// evaluation completes when it is disposed. Active evaluations are required
// to return disposers so we can know both when it has ended (so we can clean
// up on our end) and how to force end it (for example when the client
// disconnects).
// tslint:disable-next-line no-any
const promise = !eventEmitter ? value as Promise<any> : new Promise((resolve): void => {
value.onDidDispose(resolve);
});
if (promise && promise.then) {
promise.then(sendResp).catch(sendException);
} else {
sendResp(value);
}
return eventEmitter ? {
onEvent: (eventMsg: EvalEventMessage): void => {
eventEmitter!.emit(eventMsg.getEvent(), ...eventMsg.getArgsList().map(parse));
},
dispose: (): void => {
if (eventEmitter) {
if (value && value.dispose) {
value.dispose();
}
eventEmitter.removeAllListeners();
eventEmitter = undefined;
}
},
} : undefined;
};

View File

@@ -0,0 +1,95 @@
import * as cp from "child_process";
import { ServerProxy } from "../../common/proxy";
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 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.instance.kill(signal);
}
public async disconnect(): Promise<void> {
this.instance.disconnect();
}
public async ref(): Promise<void> {
this.instance.ref();
}
public async unref(): Promise<void> {
this.instance.unref();
}
// tslint:disable-next-line no-any
public async send(message: any): Promise<void> {
return new Promise((resolve, reject): void => {
this.instance.send(message, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
public async getPid(): Promise<number> {
return this.instance.pid;
}
public async dispose(): Promise<void> {
this.instance.kill();
setTimeout(() => this.instance.kill("SIGKILL"), 5000); // Double tap.
await super.dispose();
}
}
export interface ChildProcessProxies {
childProcess: ChildProcessProxy;
stdin?: WritableProxy | null;
stdout?: ReadableProxy | null;
stderr?: ReadableProxy | null;
}
export class ChildProcessModuleProxy {
public constructor(private readonly forkProvider?: ForkProvider) {}
public async exec(
command: string,
options?: { encoding?: string | null } & cp.ExecOptions | null,
callback?: ((error: cp.ExecException | null, stdin: string | Buffer, stdout: string | Buffer) => void),
): Promise<ChildProcessProxies> {
return this.returnProxies(cp.exec(command, options && withEnv(options), callback));
}
public async fork(modulePath: string, args?: string[], options?: cp.ForkOptions): Promise<ChildProcessProxies> {
return this.returnProxies((this.forkProvider || cp.fork)(modulePath, args, withEnv(options)));
}
public async spawn(command: string, args?: string[], options?: cp.SpawnOptions): Promise<ChildProcessProxies> {
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),
// 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

@@ -0,0 +1,272 @@
import * as fs from "fs";
import { promisify } from "util";
import { ServerProxy } from "../../common/proxy";
import { IEncodingOptions } from "../../common/util";
import { ReadableProxy, WritableProxy } from "./stream";
// tslint:disable completed-docs no-any
/**
* A serializable version of fs.Stats.
*/
export interface Stats {
dev: number;
ino: number;
mode: number;
nlink: number;
uid: number;
gid: number;
rdev: number;
size: number;
blksize: number;
blocks: number;
atimeMs: number;
mtimeMs: number;
ctimeMs: number;
birthtimeMs: number;
atime: Date;
mtime: Date;
ctime: Date;
birthtime: Date;
_isFile: boolean;
_isDirectory: boolean;
_isBlockDevice: boolean;
_isCharacterDevice: boolean;
_isSymbolicLink: boolean;
_isFIFO: boolean;
_isSocket: boolean;
}
export class ReadStreamProxy extends ReadableProxy<fs.ReadStream> {
public constructor(stream: fs.ReadStream) {
super(stream, ["open"]);
}
public async close(): Promise<void> {
this.instance.close();
}
public async dispose(): Promise<void> {
this.instance.close();
await super.dispose();
}
}
export class WriteStreamProxy extends WritableProxy<fs.WriteStream> {
public constructor(stream: fs.WriteStream) {
super(stream, ["open"]);
}
public async close(): Promise<void> {
this.instance.close();
}
public async dispose(): Promise<void> {
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 close(): Promise<void> {
this.instance.close();
}
public async dispose(): Promise<void> {
this.instance.close();
await super.dispose();
}
}
export class FsModuleProxy {
public access(path: fs.PathLike, mode?: number): Promise<void> {
return promisify(fs.access)(path, mode);
}
public appendFile(file: fs.PathLike | number, data: any, options?: fs.WriteFileOptions): Promise<void> {
return promisify(fs.appendFile)(file, data, options);
}
public chmod(path: fs.PathLike, mode: string | number): Promise<void> {
return promisify(fs.chmod)(path, mode);
}
public chown(path: fs.PathLike, uid: number, gid: number): Promise<void> {
return promisify(fs.chown)(path, uid, gid);
}
public close(fd: number): Promise<void> {
return promisify(fs.close)(fd);
}
public copyFile(src: fs.PathLike, dest: fs.PathLike, flags?: number): Promise<void> {
return promisify(fs.copyFile)(src, dest, flags);
}
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));
}
public exists(path: fs.PathLike): Promise<boolean> {
return promisify(fs.exists)(path); // tslint:disable-line deprecation
}
public fchmod(fd: number, mode: string | number): Promise<void> {
return promisify(fs.fchmod)(fd, mode);
}
public fchown(fd: number, uid: number, gid: number): Promise<void> {
return promisify(fs.fchown)(fd, uid, gid);
}
public fdatasync(fd: number): Promise<void> {
return promisify(fs.fdatasync)(fd);
}
public async fstat(fd: number): Promise<Stats> {
return this.makeStatsSerializable(await promisify(fs.fstat)(fd));
}
public fsync(fd: number): Promise<void> {
return promisify(fs.fsync)(fd);
}
public ftruncate(fd: number, len?: number | null): Promise<void> {
return promisify(fs.ftruncate)(fd, len);
}
public futimes(fd: number, atime: string | number | Date, mtime: string | number | Date): Promise<void> {
return promisify(fs.futimes)(fd, atime, mtime);
}
public lchmod(path: fs.PathLike, mode: string | number): Promise<void> {
return promisify(fs.lchmod)(path, mode);
}
public lchown(path: fs.PathLike, uid: number, gid: number): Promise<void> {
return promisify(fs.lchown)(path, uid, gid);
}
public link(existingPath: fs.PathLike, newPath: fs.PathLike): Promise<void> {
return promisify(fs.link)(existingPath, newPath);
}
public async lstat(path: fs.PathLike): Promise<Stats> {
return this.makeStatsSerializable(await promisify(fs.lstat)(path));
}
public async lstatBatch(args: { path: fs.PathLike }[]): Promise<(Stats | Error)[]> {
return Promise.all(args.map((a) => this.lstat(a.path).catch((e) => e)));
}
public mkdir(path: fs.PathLike, mode: number | string | fs.MakeDirectoryOptions | undefined | null): Promise<void> {
return promisify(fs.mkdir)(path, mode);
}
public mkdtemp(prefix: string, options: IEncodingOptions): Promise<string | Buffer> {
return promisify(fs.mkdtemp)(prefix, options);
}
public open(path: fs.PathLike, flags: string | number, mode: string | number | undefined | null): Promise<number> {
return promisify(fs.open)(path, flags, mode);
}
public read(fd: number, length: number, position: number | null): Promise<{ bytesRead: number, buffer: Buffer }> {
const buffer = Buffer.alloc(length);
return promisify(fs.read)(fd, buffer, 0, length, position);
}
public readFile(path: fs.PathLike | number, options: IEncodingOptions): Promise<string | Buffer> {
return promisify(fs.readFile)(path, options);
}
public readdir(path: fs.PathLike, options: IEncodingOptions): Promise<Buffer[] | fs.Dirent[] | string[]> {
return promisify(fs.readdir)(path, options);
}
public readdirBatch(args: { path: fs.PathLike, options: IEncodingOptions }[]): Promise<(Buffer[] | fs.Dirent[] | string[] | Error)[]> {
return Promise.all(args.map((a) => this.readdir(a.path, a.options).catch((e) => e)));
}
public readlink(path: fs.PathLike, options: IEncodingOptions): Promise<string | Buffer> {
return promisify(fs.readlink)(path, options);
}
public realpath(path: fs.PathLike, options: IEncodingOptions): Promise<string | Buffer> {
return promisify(fs.realpath)(path, options);
}
public rename(oldPath: fs.PathLike, newPath: fs.PathLike): Promise<void> {
return promisify(fs.rename)(oldPath, newPath);
}
public rmdir(path: fs.PathLike): Promise<void> {
return promisify(fs.rmdir)(path);
}
public async stat(path: fs.PathLike): Promise<Stats> {
return this.makeStatsSerializable(await promisify(fs.stat)(path));
}
public async statBatch(args: { path: fs.PathLike }[]): Promise<(Stats | Error)[]> {
return Promise.all(args.map((a) => this.stat(a.path).catch((e) => e)));
}
public symlink(target: fs.PathLike, path: fs.PathLike, type?: fs.symlink.Type | null): Promise<void> {
return promisify(fs.symlink)(target, path, type);
}
public truncate(path: fs.PathLike, len?: number | null): Promise<void> {
return promisify(fs.truncate)(path, len);
}
public unlink(path: fs.PathLike): Promise<void> {
return promisify(fs.unlink)(path);
}
public utimes(path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date): Promise<void> {
return promisify(fs.utimes)(path, atime, mtime);
}
public async write(fd: number, buffer: Buffer, offset?: number, length?: number, position?: number): Promise<{ bytesWritten: number, buffer: Buffer }> {
return promisify(fs.write)(fd, buffer, offset, length, position);
}
public writeFile (path: fs.PathLike | number, data: any, options: IEncodingOptions): Promise<void> {
return promisify(fs.writeFile)(path, data, options);
}
public async watch(filename: fs.PathLike, options?: IEncodingOptions): Promise<WatcherProxy> {
return new WatcherProxy(fs.watch(filename, options));
}
private makeStatsSerializable(stats: fs.Stats): Stats {
return {
...stats,
/**
* We need to check if functions exist because nexe's implemented FS
* lib doesnt implement fs.stats properly.
*/
_isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false,
_isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false,
_isDirectory: stats.isDirectory(),
_isFIFO: stats.isFIFO ? stats.isFIFO() : false,
_isFile: stats.isFile(),
_isSocket: stats.isSocket ? stats.isSocket() : false,
_isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false,
};
}
}

View File

@@ -0,0 +1,6 @@
export * from "./child_process";
export * from "./fs";
export * from "./net";
export * from "./node-pty";
export * from "./spdlog";
export * from "./trash";

View File

@@ -0,0 +1,79 @@
import * as net from "net";
import { ServerProxy } from "../../common/proxy";
import { DuplexProxy } from "./stream";
// 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.instance.connect(options as any, host as any);
}
public async unref(): Promise<void> {
this.instance.unref();
}
public async ref(): Promise<void> {
this.instance.ref();
}
public async dispose(): Promise<void> {
this.instance.end();
this.instance.destroy();
this.instance.unref();
await super.dispose();
}
}
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.instance.listen(handle, hostname as any, backlog as any);
}
public async ref(): Promise<void> {
this.instance.ref();
}
public async unref(): Promise<void> {
this.instance.unref();
}
public async close(): Promise<void> {
this.instance.close();
}
public async onConnection(cb: (proxy: NetSocketProxy) => void): Promise<void> {
this.instance.on("connection", (socket) => cb(new NetSocketProxy(socket)));
}
public async dispose(): Promise<void> {
this.instance.close();
this.instance.removeAllListeners();
}
}
export class NetModuleProxy {
public async createSocket(options?: net.SocketConstructorOpts): Promise<NetSocketProxy> {
return new NetSocketProxy(new net.Socket(options));
}
public async createConnection(target: string | number | net.NetConnectOpts, host?: string): Promise<NetSocketProxy> {
return new NetSocketProxy(net.createConnection(target as any, host));
}
public async createServer(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean }): Promise<NetServerProxy> {
return new NetServerProxy(net.createServer(options));
}
}

View File

@@ -0,0 +1,71 @@
/// <reference path="../../../../../lib/vscode/src/typings/node-pty.d.ts" />
import { EventEmitter } from "events";
import * as pty from "node-pty";
import { ServerProxy } from "../../common/proxy";
import { withEnv } from "../../common/util";
// tslint:disable completed-docs
/**
* Server-side IPty proxy.
*/
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.instance.emit("process", name);
}, 1);
const timer = setInterval(() => {
if (process.process !== name) {
name = process.process;
this.instance.emit("process", name);
}
}, 200);
this.process.on("exit", () => clearInterval(timer));
}
public async getPid(): Promise<number> {
return this.process.pid;
}
public async getProcess(): Promise<string> {
return this.process.process;
}
public async kill(signal?: string): Promise<void> {
this.process.kill(signal);
}
public async resize(columns: number, rows: number): Promise<void> {
this.process.resize(columns, rows);
}
public async write(data: string): Promise<void> {
this.process.write(data);
}
public async dispose(): Promise<void> {
this.process.kill();
setTimeout(() => this.process.kill("SIGKILL"), 5000); // Double tap.
await super.dispose();
}
}
/**
* Server-side node-pty proxy.
*/
export class NodePtyModuleProxy {
public async spawn(file: string, args: string[] | string, options: pty.IPtyForkOptions): Promise<NodePtyProcessProxy> {
return new NodePtyProcessProxy(require("node-pty").spawn(file, args, withEnv(options)));
}
}

View File

@@ -0,0 +1,43 @@
/// <reference path="../../../../../lib/vscode/src/typings/spdlog.d.ts" />
import { EventEmitter } from "events";
import * as spdlog from "spdlog";
import { ServerProxy } from "../../common/proxy";
// tslint:disable completed-docs
export class RotatingLoggerProxy 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); }
public async info (message: string): Promise<void> { this.logger.info(message); }
public async warn (message: string): Promise<void> { this.logger.warn(message); }
public async error (message: string): Promise<void> { this.logger.error(message); }
public async critical (message: string): Promise<void> { this.logger.critical(message); }
public async setLevel (level: number): Promise<void> { this.logger.setLevel(level); }
public async clearFormatters (): Promise<void> { this.logger.clearFormatters(); }
public async flush (): Promise<void> { this.logger.flush(); }
public async drop (): Promise<void> { this.logger.drop(); }
public async dispose(): Promise<void> {
await this.flush();
this.instance.emit("dispose");
await super.dispose();
}
}
export class SpdlogModuleProxy {
public async createLogger(name: string, filePath: string, fileSize: number, fileCount: number): Promise<RotatingLoggerProxy> {
return new RotatingLoggerProxy(new (require("spdlog") as typeof import("spdlog")).RotatingLogger(name, filePath, fileSize, fileCount));
}
public async setAsyncMode(bufferSize: number, flushInterval: number): Promise<void> {
require("spdlog").setAsyncMode(bufferSize, flushInterval);
}
}

View File

@@ -0,0 +1,109 @@
import { EventEmitter } from "events";
import * as stream from "stream";
import { ServerProxy } from "../../common/proxy";
// tslint:disable completed-docs no-any
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();
}
public async end(data?: any, encoding?: string): Promise<void> {
return new Promise((resolve): void => {
this.instance.end(data, encoding, () => {
resolve();
});
});
}
public async setDefaultEncoding(encoding: string): Promise<void> {
this.instance.setDefaultEncoding(encoding);
}
public async write(data: any, encoding?: string): Promise<void> {
return new Promise((resolve, reject): void => {
this.instance.write(data, encoding, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
public async dispose(): Promise<void> {
this.instance.end();
await super.dispose();
}
}
/**
* 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<T extends EventEmitter> extends ServerProxy<T> {
pipe<P extends WritableProxy>(destination: P, options?: { end?: boolean; }): Promise<void>;
setEncoding(encoding: string): Promise<void>;
}
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.instance.destroy();
}
public async setEncoding(encoding: string): Promise<void> {
this.instance.setEncoding(encoding);
}
public async dispose(): Promise<void> {
this.instance.destroy();
await super.dispose();
}
}
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.instance.setEncoding(encoding);
}
public async dispose(): Promise<void> {
this.instance.destroy();
await super.dispose();
}
}

View File

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

View File

@@ -1,128 +1,369 @@
import { mkdirp } from "fs-extra";
import * as os from "os";
import * as path from "path";
import { mkdir } from "fs";
import { promisify } from "util";
import { logger, field } from "@coder/logger";
import { Pong, ClientMessage, WorkingInitMessage, ServerMessage } from "../proto";
import { evaluate, ActiveEvaluation } from "./evaluate";
import { ForkProvider } from "../common/helpers";
import { field, logger} from "@coder/logger";
import { ReadWriteConnection } from "../common/connection";
import { Module, ServerProxy } from "../common/proxy";
import { isPromise, isProxy, moduleToProto, protoToArgument, platformToProto, protoToModule, argumentToProto } from "../common/util";
import { Argument, Callback, ClientMessage, Event, Method, Pong, ServerMessage, WorkingInit } from "../proto";
import { ChildProcessModuleProxy, ForkProvider, FsModuleProxy, NetModuleProxy, NodePtyModuleProxy, SpdlogModuleProxy, TrashModuleProxy } from "./modules";
// tslint:disable no-any
export interface ServerOptions {
readonly workingDirectory: string;
readonly dataDirectory: string;
readonly cacheDirectory: string;
readonly builtInExtensionsDirectory: string;
readonly extensionsDirectory: string;
readonly extraExtensionDirectories?: string[];
readonly extraBuiltinExtensionDirectories?: string[];
readonly fork?: ForkProvider;
}
interface ProxyData {
disposeTimeout?: number | NodeJS.Timer;
instance: any;
}
/**
* Handle messages from the client.
*/
export class Server {
private readonly evals = new Map<number, ActiveEvaluation>();
private proxyId = 0;
private readonly proxies = new Map<number | Module, ProxyData>();
private disconnected: boolean = false;
private readonly responseTimeout = 10000;
public constructor(
private readonly connection: ReadWriteConnection,
private readonly options?: ServerOptions,
) {
connection.onMessage((data) => {
connection.onMessage(async (data) => {
try {
this.handleMessage(ClientMessage.deserializeBinary(data));
await this.handleMessage(ClientMessage.deserializeBinary(data));
} catch (ex) {
logger.error("Failed to handle client message", field("length", data.byteLength), field("exception", {
message: ex.message,
stack: ex.stack,
}));
logger.error(
"Failed to handle client message",
field("length", data.byteLength),
field("exception", {
message: ex.message,
stack: ex.stack,
}),
);
}
});
connection.onClose(() => {
this.evals.forEach((e) => e.dispose());
this.disconnected = true;
logger.trace(() => [
"disconnected from client",
field("proxies", this.proxies.size),
]);
this.proxies.forEach((proxy, proxyId) => {
if (isProxy(proxy.instance)) {
proxy.instance.dispose().catch((error) => {
logger.error(error.message);
});
}
this.removeProxy(proxyId);
});
});
this.storeProxy(new ChildProcessModuleProxy(this.options ? this.options.fork : undefined), Module.ChildProcess);
this.storeProxy(new FsModuleProxy(), Module.Fs);
this.storeProxy(new NetModuleProxy(), Module.Net);
this.storeProxy(new NodePtyModuleProxy(), Module.NodePty);
this.storeProxy(new SpdlogModuleProxy(), Module.Spdlog);
this.storeProxy(new TrashModuleProxy(), Module.Trash);
if (!this.options) {
logger.warn("No server options provided. InitMessage will not be sent.");
return;
}
// Ensure the data directory exists.
const mkdirP = async (path: string): Promise<void> => {
const split = path.replace(/^\/*|\/*$/g, "").split("/");
let dir = "";
while (split.length > 0) {
dir += "/" + split.shift();
try {
await promisify(mkdir)(dir);
} catch (error) {
if (error.code !== "EEXIST" && error.code !== "EISDIR") {
throw error;
}
}
}
};
Promise.all([ mkdirP(path.join(this.options.dataDirectory, "User", "workspaceStorage")) ]).then(() => {
logger.info("Created data directory");
}).catch((error) => {
Promise.all([
mkdirp(this.options.cacheDirectory),
mkdirp(this.options.dataDirectory),
mkdirp(this.options.workingDirectory),
]).catch((error) => {
logger.error(error.message, field("error", error));
});
const initMsg = new WorkingInitMessage();
const initMsg = new WorkingInit();
initMsg.setDataDirectory(this.options.dataDirectory);
initMsg.setWorkingDirectory(this.options.workingDirectory);
initMsg.setBuiltinExtensionsDir(this.options.builtInExtensionsDirectory);
initMsg.setExtensionsDirectory(this.options.extensionsDirectory);
initMsg.setHomeDirectory(os.homedir());
initMsg.setTmpDirectory(os.tmpdir());
const platform = os.platform();
let operatingSystem: WorkingInitMessage.OperatingSystem;
switch (platform) {
case "win32":
operatingSystem = WorkingInitMessage.OperatingSystem.WINDOWS;
break;
case "linux":
operatingSystem = WorkingInitMessage.OperatingSystem.LINUX;
break;
case "darwin":
operatingSystem = WorkingInitMessage.OperatingSystem.MAC;
break;
default:
throw new Error(`unrecognized platform "${platform}"`);
initMsg.setOperatingSystem(platformToProto(os.platform()));
initMsg.setShell(os.userInfo().shell || global.process.env.SHELL || "");
initMsg.setExtraExtensionDirectoriesList(this.options.extraExtensionDirectories || []);
initMsg.setExtraBuiltinExtensionDirectoriesList(this.options.extraBuiltinExtensionDirectories || []);
for (let key in process.env) {
initMsg.getEnvMap().set(key, process.env[key] as string);
}
initMsg.setOperatingSystem(operatingSystem);
initMsg.setShell(os.userInfo().shell || global.process.env.SHELL);
const srvMsg = new ServerMessage();
srvMsg.setInit(initMsg);
connection.send(srvMsg.serializeBinary());
}
private handleMessage(message: ClientMessage): void {
if (message.hasNewEval()) {
const evalMessage = message.getNewEval()!;
logger.trace(() => [
"EvalMessage",
field("id", evalMessage.getId()),
field("args", evalMessage.getArgsList()),
field("function", evalMessage.getFunction()),
]);
const resp = evaluate(this.connection, evalMessage, () => {
this.evals.delete(evalMessage.getId());
logger.trace(() => [
`dispose ${evalMessage.getId()}, ${this.evals.size} left`,
]);
}, this.options ? this.options.fork : undefined);
if (resp) {
this.evals.set(evalMessage.getId(), resp);
}
} else if (message.hasEvalEvent()) {
const evalEventMessage = message.getEvalEvent()!;
const e = this.evals.get(evalEventMessage.getId());
if (!e) {
return;
}
e.onEvent(evalEventMessage);
} else if (message.hasPing()) {
logger.trace("ping");
const srvMsg = new ServerMessage();
srvMsg.setPong(new Pong());
this.connection.send(srvMsg.serializeBinary());
} else {
throw new Error("unknown message type");
/**
* Handle all messages from the client.
*/
private async handleMessage(message: ClientMessage): Promise<void> {
switch (message.getMsgCase()) {
case ClientMessage.MsgCase.METHOD:
await this.runMethod(message.getMethod()!);
break;
case ClientMessage.MsgCase.PING:
logger.trace("ping");
const srvMsg = new ServerMessage();
srvMsg.setPong(new Pong());
this.connection.send(srvMsg.serializeBinary());
break;
default:
throw new Error("unknown message type");
}
}
/**
* Run a method on a proxy.
*/
private async runMethod(message: Method): Promise<void> {
const proxyMessage = message.getNamedProxy()! || message.getNumberedProxy()!;
const id = proxyMessage.getId();
const proxyId = message.hasNamedProxy()
? protoToModule(message.getNamedProxy()!.getModule())
: message.getNumberedProxy()!.getProxyId();
const method = proxyMessage.getMethod();
const args = proxyMessage.getArgsList().map((a) => protoToArgument(
a,
(id, args) => this.sendCallback(proxyId, id, args),
(id) => this.getProxy(id).instance,
));
logger.trace(() => [
"received",
field("id", id),
field("proxyId", proxyId),
field("method", method),
]);
let response: any;
try {
const proxy = this.getProxy(proxyId);
if (typeof proxy.instance[method] !== "function") {
throw new Error(`"${method}" is not a function on proxy ${proxyId}`);
}
response = proxy.instance[method](...args);
// We wait for the client to call "dispose" instead of doing it onDone to
// ensure all the messages it sent get processed before we get rid of it.
if (method === "dispose") {
this.removeProxy(proxyId);
}
// Proxies must always return promises.
if (!isPromise(response)) {
throw new Error(`"${method}" must return a promise`);
}
} catch (error) {
logger.error(
error.message,
field("type", typeof response),
field("proxyId", proxyId),
);
this.sendException(id, error);
}
try {
this.sendResponse(id, await response);
} catch (error) {
this.sendException(id, error);
}
}
/**
* Send a callback to the client.
*/
private sendCallback(proxyId: number | Module, callbackId: number, args: any[]): void {
logger.trace(() => [
"sending callback",
field("proxyId", proxyId),
field("callbackId", callbackId),
]);
const message = new Callback();
let callbackMessage: Callback.Named | Callback.Numbered;
if (typeof proxyId === "string") {
callbackMessage = new Callback.Named();
callbackMessage.setModule(moduleToProto(proxyId));
message.setNamedCallback(callbackMessage);
} else {
callbackMessage = new Callback.Numbered();
callbackMessage.setProxyId(proxyId);
message.setNumberedCallback(callbackMessage);
}
callbackMessage.setCallbackId(callbackId);
callbackMessage.setArgsList(args.map((a) => this.argumentToProto(a)));
const serverMessage = new ServerMessage();
serverMessage.setCallback(message);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Store a numbered proxy and bind events to send them back to the client.
*/
private storeProxy(instance: ServerProxy): number;
/**
* Store a unique proxy and bind events to send them back to the client.
*/
private storeProxy(instance: any, moduleProxyId: Module): Module;
/**
* Store a proxy and bind events to send them back to the client.
*/
private storeProxy(instance: ServerProxy | any, moduleProxyId?: Module): number | Module {
// In case we disposed while waiting for a function to return.
if (this.disconnected) {
if (isProxy(instance)) {
instance.dispose().catch((error) => {
logger.error(error.message);
});
}
throw new Error("disposed");
}
const proxyId = moduleProxyId || this.proxyId++;
logger.trace(() => [
"storing proxy",
field("proxyId", proxyId),
]);
this.proxies.set(proxyId, { instance });
if (isProxy(instance)) {
instance.onEvent((event, ...args) => this.sendEvent(proxyId, event, ...args));
instance.onDone(() => {
// It might have finished because we disposed it due to a disconnect.
if (!this.disconnected) {
this.sendEvent(proxyId, "done");
this.getProxy(proxyId).disposeTimeout = setTimeout(() => {
instance.dispose().catch((error) => {
logger.error(error.message);
});
this.removeProxy(proxyId);
}, this.responseTimeout);
}
});
}
return proxyId;
}
/**
* Send an event to the client.
*/
private sendEvent(proxyId: number | Module, event: string, ...args: any[]): void {
logger.trace(() => [
"sending event",
field("proxyId", proxyId),
field("event", event),
]);
const message = new Event();
let eventMessage: Event.Named | Event.Numbered;
if (typeof proxyId === "string") {
eventMessage = new Event.Named();
eventMessage.setModule(moduleToProto(proxyId));
message.setNamedEvent(eventMessage);
} else {
eventMessage = new Event.Numbered();
eventMessage.setProxyId(proxyId);
message.setNumberedEvent(eventMessage);
}
eventMessage.setEvent(event);
eventMessage.setArgsList(args.map((a) => this.argumentToProto(a)));
const serverMessage = new ServerMessage();
serverMessage.setEvent(message);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Send a response back to the client.
*/
private sendResponse(id: number, response: any): void {
logger.trace(() => [
"sending resolve",
field("id", id),
]);
const successMessage = new Method.Success();
successMessage.setId(id);
successMessage.setResponse(this.argumentToProto(response));
const serverMessage = new ServerMessage();
serverMessage.setSuccess(successMessage);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Send an exception back to the client.
*/
private sendException(id: number, error: Error): void {
logger.trace(() => [
"sending reject",
field("id", id) ,
field("message", error.message),
]);
const failedMessage = new Method.Fail();
failedMessage.setId(id);
failedMessage.setResponse(argumentToProto(error));
const serverMessage = new ServerMessage();
serverMessage.setFail(failedMessage);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Call after disposing a proxy.
*/
private removeProxy(proxyId: number | Module): void {
clearTimeout(this.getProxy(proxyId).disposeTimeout as any);
this.proxies.delete(proxyId);
logger.trace(() => [
"disposed and removed proxy",
field("proxyId", proxyId),
field("proxies", this.proxies.size),
]);
}
/**
* Same as argumentToProto but provides storeProxy.
*/
private argumentToProto(value: any): Argument {
return argumentToProto(value, undefined, (p) => this.storeProxy(p));
}
/**
* Get a proxy. Error if it doesn't exist.
*/
private getProxy(proxyId: number | Module): ProxyData {
if (!this.proxies.has(proxyId)) {
throw new Error(`proxy ${proxyId} disposed too early`);
}
return this.proxies.get(proxyId)!;
}
}

View File

@@ -2,33 +2,33 @@ syntax = "proto3";
import "node.proto";
import "vscode.proto";
// Messages that the client can send to the server.
message ClientMessage {
oneof msg {
// node.proto
NewEvalMessage new_eval = 11;
EvalEventMessage eval_event = 12;
Ping ping = 13;
Method method = 20;
Ping ping = 21;
}
}
// Messages that the server can send to the client.
message ServerMessage {
oneof msg {
// node.proto
EvalFailedMessage eval_failed = 13;
EvalDoneMessage eval_done = 14;
EvalEventMessage eval_event = 15;
Method.Fail fail = 13;
Method.Success success = 14;
Event event = 19;
Callback callback = 22;
Pong pong = 18;
WorkingInitMessage init = 16;
WorkingInit init = 16;
// vscode.proto
SharedProcessActiveMessage shared_process_active = 17;
Pong pong = 18;
SharedProcessActive shared_process_active = 17;
}
}
message WorkingInitMessage {
message WorkingInit {
string home_directory = 1;
string tmp_directory = 2;
string data_directory = 3;
@@ -41,4 +41,9 @@ message WorkingInitMessage {
OperatingSystem operating_system = 5;
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;
map<string, string> env = 11;
}

View File

@@ -6,15 +6,10 @@ import * as node_pb from "./node_pb";
import * as vscode_pb from "./vscode_pb";
export class ClientMessage extends jspb.Message {
hasNewEval(): boolean;
clearNewEval(): void;
getNewEval(): node_pb.NewEvalMessage | undefined;
setNewEval(value?: node_pb.NewEvalMessage): void;
hasEvalEvent(): boolean;
clearEvalEvent(): void;
getEvalEvent(): node_pb.EvalEventMessage | undefined;
setEvalEvent(value?: node_pb.EvalEventMessage): void;
hasMethod(): boolean;
clearMethod(): void;
getMethod(): node_pb.Method | undefined;
setMethod(value?: node_pb.Method): void;
hasPing(): boolean;
clearPing(): void;
@@ -34,50 +29,53 @@ export class ClientMessage extends jspb.Message {
export namespace ClientMessage {
export type AsObject = {
newEval?: node_pb.NewEvalMessage.AsObject,
evalEvent?: node_pb.EvalEventMessage.AsObject,
method?: node_pb.Method.AsObject,
ping?: node_pb.Ping.AsObject,
}
export enum MsgCase {
MSG_NOT_SET = 0,
NEW_EVAL = 11,
EVAL_EVENT = 12,
PING = 13,
METHOD = 20,
PING = 21,
}
}
export class ServerMessage extends jspb.Message {
hasEvalFailed(): boolean;
clearEvalFailed(): void;
getEvalFailed(): node_pb.EvalFailedMessage | undefined;
setEvalFailed(value?: node_pb.EvalFailedMessage): void;
hasFail(): boolean;
clearFail(): void;
getFail(): node_pb.Method.Fail | undefined;
setFail(value?: node_pb.Method.Fail): void;
hasEvalDone(): boolean;
clearEvalDone(): void;
getEvalDone(): node_pb.EvalDoneMessage | undefined;
setEvalDone(value?: node_pb.EvalDoneMessage): void;
hasSuccess(): boolean;
clearSuccess(): void;
getSuccess(): node_pb.Method.Success | undefined;
setSuccess(value?: node_pb.Method.Success): void;
hasEvalEvent(): boolean;
clearEvalEvent(): void;
getEvalEvent(): node_pb.EvalEventMessage | undefined;
setEvalEvent(value?: node_pb.EvalEventMessage): void;
hasEvent(): boolean;
clearEvent(): void;
getEvent(): node_pb.Event | undefined;
setEvent(value?: node_pb.Event): void;
hasInit(): boolean;
clearInit(): void;
getInit(): WorkingInitMessage | undefined;
setInit(value?: WorkingInitMessage): void;
hasSharedProcessActive(): boolean;
clearSharedProcessActive(): void;
getSharedProcessActive(): vscode_pb.SharedProcessActiveMessage | undefined;
setSharedProcessActive(value?: vscode_pb.SharedProcessActiveMessage): void;
hasCallback(): boolean;
clearCallback(): void;
getCallback(): node_pb.Callback | undefined;
setCallback(value?: node_pb.Callback): void;
hasPong(): boolean;
clearPong(): void;
getPong(): node_pb.Pong | undefined;
setPong(value?: node_pb.Pong): void;
hasInit(): boolean;
clearInit(): void;
getInit(): WorkingInit | undefined;
setInit(value?: WorkingInit): void;
hasSharedProcessActive(): boolean;
clearSharedProcessActive(): void;
getSharedProcessActive(): vscode_pb.SharedProcessActive | undefined;
setSharedProcessActive(value?: vscode_pb.SharedProcessActive): void;
getMsgCase(): ServerMessage.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ServerMessage.AsObject;
@@ -91,26 +89,28 @@ export class ServerMessage extends jspb.Message {
export namespace ServerMessage {
export type AsObject = {
evalFailed?: node_pb.EvalFailedMessage.AsObject,
evalDone?: node_pb.EvalDoneMessage.AsObject,
evalEvent?: node_pb.EvalEventMessage.AsObject,
init?: WorkingInitMessage.AsObject,
sharedProcessActive?: vscode_pb.SharedProcessActiveMessage.AsObject,
fail?: node_pb.Method.Fail.AsObject,
success?: node_pb.Method.Success.AsObject,
event?: node_pb.Event.AsObject,
callback?: node_pb.Callback.AsObject,
pong?: node_pb.Pong.AsObject,
init?: WorkingInit.AsObject,
sharedProcessActive?: vscode_pb.SharedProcessActive.AsObject,
}
export enum MsgCase {
MSG_NOT_SET = 0,
EVAL_FAILED = 13,
EVAL_DONE = 14,
EVAL_EVENT = 15,
FAIL = 13,
SUCCESS = 14,
EVENT = 19,
CALLBACK = 22,
PONG = 18,
INIT = 16,
SHARED_PROCESS_ACTIVE = 17,
PONG = 18,
}
}
export class WorkingInitMessage extends jspb.Message {
export class WorkingInit extends jspb.Message {
getHomeDirectory(): string;
setHomeDirectory(value: string): void;
@@ -123,8 +123,8 @@ export class WorkingInitMessage extends jspb.Message {
getWorkingDirectory(): string;
setWorkingDirectory(value: string): void;
getOperatingSystem(): WorkingInitMessage.OperatingSystem;
setOperatingSystem(value: WorkingInitMessage.OperatingSystem): void;
getOperatingSystem(): WorkingInit.OperatingSystem;
setOperatingSystem(value: WorkingInit.OperatingSystem): void;
getShell(): string;
setShell(value: string): void;
@@ -132,25 +132,44 @@ export class WorkingInitMessage extends jspb.Message {
getBuiltinExtensionsDir(): string;
setBuiltinExtensionsDir(value: string): void;
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;
getEnvMap(): jspb.Map<string, string>;
clearEnvMap(): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): WorkingInitMessage.AsObject;
static toObject(includeInstance: boolean, msg: WorkingInitMessage): WorkingInitMessage.AsObject;
toObject(includeInstance?: boolean): WorkingInit.AsObject;
static toObject(includeInstance: boolean, msg: WorkingInit): WorkingInit.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: WorkingInitMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): WorkingInitMessage;
static deserializeBinaryFromReader(message: WorkingInitMessage, reader: jspb.BinaryReader): WorkingInitMessage;
static serializeBinaryToWriter(message: WorkingInit, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): WorkingInit;
static deserializeBinaryFromReader(message: WorkingInit, reader: jspb.BinaryReader): WorkingInit;
}
export namespace WorkingInitMessage {
export namespace WorkingInit {
export type AsObject = {
homeDirectory: string,
tmpDirectory: string,
dataDirectory: string,
workingDirectory: string,
operatingSystem: WorkingInitMessage.OperatingSystem,
operatingSystem: WorkingInit.OperatingSystem,
shell: string,
builtinExtensionsDir: string,
extensionsDirectory: string,
extraExtensionDirectoriesList: Array<string>,
extraBuiltinExtensionDirectoriesList: Array<string>,
envMap: Array<[string, string]>,
}
export enum OperatingSystem {

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +1,141 @@
syntax = "proto3";
message NewEvalMessage {
uint64 id = 1;
string function = 2;
repeated string args = 3;
// Timeout in ms
uint32 timeout = 4;
// Create active eval message.
// Allows for dynamic communication for an eval
bool active = 5;
enum Module {
ChildProcess = 0;
Fs = 1;
Net = 2;
NodePty = 3;
Spdlog = 4;
Trash = 5;
}
message EvalEventMessage {
uint64 id = 1;
string event = 2;
repeated string args = 3;
message Argument {
message ErrorValue {
string message = 1;
string stack = 2;
string code = 3;
}
message BufferValue {
bytes data = 1;
}
message ObjectValue {
map<string, Argument> data = 1;
}
message ArrayValue {
repeated Argument data = 1;
}
message ProxyValue {
uint64 id = 1;
}
message FunctionValue {
uint64 id = 1;
}
message NullValue {}
message UndefinedValue {}
message DateValue {
string date = 1;
}
oneof msg {
ErrorValue error = 1;
BufferValue buffer = 2;
ObjectValue object = 3;
ArrayValue array = 4;
ProxyValue proxy = 5;
FunctionValue function = 6;
NullValue null = 7;
UndefinedValue undefined = 8;
double number = 9;
string string = 10;
bool boolean = 11;
DateValue date = 12;
}
}
message EvalFailedMessage {
uint64 id = 1;
string response = 2;
// Call a remote method.
message Method {
// A proxy identified by a unique name like "fs".
message Named {
uint64 id = 1;
Module module = 2;
string method = 3;
repeated Argument args = 4;
}
// A general proxy identified by an ID like WriteStream.
message Numbered {
uint64 id = 1;
uint64 proxy_id = 2;
string method = 3;
repeated Argument args = 4;
}
// Remote method failed.
message Fail {
uint64 id = 1;
Argument response = 2;
}
// Remote method succeeded.
message Success {
uint64 id = 1;
Argument response = 2;
}
oneof msg {
Method.Named named_proxy = 1;
Method.Numbered numbered_proxy = 2;
}
}
message EvalDoneMessage {
uint64 id = 1;
string response = 2;
message Callback {
// A remote callback for uniquely named proxy.
message Named {
Module module = 1;
uint64 callback_id = 2;
repeated Argument args = 3;
}
// A remote callback for a numbered proxy.
message Numbered {
uint64 proxy_id = 1;
uint64 callback_id = 2;
repeated Argument args = 3;
}
oneof msg {
Callback.Named named_callback = 1;
Callback.Numbered numbered_callback = 2;
}
}
message Event {
// Emit an event on a uniquely named proxy.
message Named {
Module module = 1;
string event = 2;
repeated Argument args = 3;
}
// Emit an event on a numbered proxy.
message Numbered {
uint64 proxy_id = 1;
string event = 2;
repeated Argument args = 3;
}
oneof msg {
Event.Named named_event = 1;
Event.Numbered numbered_event = 2;
}
}
message Ping {}

View File

@@ -3,119 +3,636 @@
import * as jspb from "google-protobuf";
export class NewEvalMessage extends jspb.Message {
getId(): number;
setId(value: number): void;
export class Argument extends jspb.Message {
hasError(): boolean;
clearError(): void;
getError(): Argument.ErrorValue | undefined;
setError(value?: Argument.ErrorValue): void;
getFunction(): string;
setFunction(value: string): void;
hasBuffer(): boolean;
clearBuffer(): void;
getBuffer(): Argument.BufferValue | undefined;
setBuffer(value?: Argument.BufferValue): void;
clearArgsList(): void;
getArgsList(): Array<string>;
setArgsList(value: Array<string>): void;
addArgs(value: string, index?: number): string;
hasObject(): boolean;
clearObject(): void;
getObject(): Argument.ObjectValue | undefined;
setObject(value?: Argument.ObjectValue): void;
getTimeout(): number;
setTimeout(value: number): void;
hasArray(): boolean;
clearArray(): void;
getArray(): Argument.ArrayValue | undefined;
setArray(value?: Argument.ArrayValue): void;
getActive(): boolean;
setActive(value: boolean): void;
hasProxy(): boolean;
clearProxy(): void;
getProxy(): Argument.ProxyValue | undefined;
setProxy(value?: Argument.ProxyValue): void;
hasFunction(): boolean;
clearFunction(): void;
getFunction(): Argument.FunctionValue | undefined;
setFunction(value?: Argument.FunctionValue): void;
hasNull(): boolean;
clearNull(): void;
getNull(): Argument.NullValue | undefined;
setNull(value?: Argument.NullValue): void;
hasUndefined(): boolean;
clearUndefined(): void;
getUndefined(): Argument.UndefinedValue | undefined;
setUndefined(value?: Argument.UndefinedValue): void;
hasNumber(): boolean;
clearNumber(): void;
getNumber(): number;
setNumber(value: number): void;
hasString(): boolean;
clearString(): void;
getString(): string;
setString(value: string): void;
hasBoolean(): boolean;
clearBoolean(): void;
getBoolean(): boolean;
setBoolean(value: boolean): void;
hasDate(): boolean;
clearDate(): void;
getDate(): Argument.DateValue | undefined;
setDate(value?: Argument.DateValue): void;
getMsgCase(): Argument.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NewEvalMessage.AsObject;
static toObject(includeInstance: boolean, msg: NewEvalMessage): NewEvalMessage.AsObject;
toObject(includeInstance?: boolean): Argument.AsObject;
static toObject(includeInstance: boolean, msg: Argument): Argument.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: NewEvalMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NewEvalMessage;
static deserializeBinaryFromReader(message: NewEvalMessage, reader: jspb.BinaryReader): NewEvalMessage;
static serializeBinaryToWriter(message: Argument, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Argument;
static deserializeBinaryFromReader(message: Argument, reader: jspb.BinaryReader): Argument;
}
export namespace NewEvalMessage {
export namespace Argument {
export type AsObject = {
id: number,
pb_function: string,
argsList: Array<string>,
timeout: number,
active: boolean,
error?: Argument.ErrorValue.AsObject,
buffer?: Argument.BufferValue.AsObject,
object?: Argument.ObjectValue.AsObject,
array?: Argument.ArrayValue.AsObject,
proxy?: Argument.ProxyValue.AsObject,
pb_function?: Argument.FunctionValue.AsObject,
pb_null?: Argument.NullValue.AsObject,
undefined?: Argument.UndefinedValue.AsObject,
number: number,
string: string,
pb_boolean: boolean,
date?: Argument.DateValue.AsObject,
}
export class ErrorValue extends jspb.Message {
getMessage(): string;
setMessage(value: string): void;
getStack(): string;
setStack(value: string): void;
getCode(): string;
setCode(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ErrorValue.AsObject;
static toObject(includeInstance: boolean, msg: ErrorValue): ErrorValue.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: ErrorValue, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): ErrorValue;
static deserializeBinaryFromReader(message: ErrorValue, reader: jspb.BinaryReader): ErrorValue;
}
export namespace ErrorValue {
export type AsObject = {
message: string,
stack: string,
code: string,
}
}
export class BufferValue extends jspb.Message {
getData(): Uint8Array | string;
getData_asU8(): Uint8Array;
getData_asB64(): string;
setData(value: Uint8Array | string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): BufferValue.AsObject;
static toObject(includeInstance: boolean, msg: BufferValue): BufferValue.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: BufferValue, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): BufferValue;
static deserializeBinaryFromReader(message: BufferValue, reader: jspb.BinaryReader): BufferValue;
}
export namespace BufferValue {
export type AsObject = {
data: Uint8Array | string,
}
}
export class ObjectValue extends jspb.Message {
getDataMap(): jspb.Map<string, Argument>;
clearDataMap(): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ObjectValue.AsObject;
static toObject(includeInstance: boolean, msg: ObjectValue): ObjectValue.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: ObjectValue, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): ObjectValue;
static deserializeBinaryFromReader(message: ObjectValue, reader: jspb.BinaryReader): ObjectValue;
}
export namespace ObjectValue {
export type AsObject = {
dataMap: Array<[string, Argument.AsObject]>,
}
}
export class ArrayValue extends jspb.Message {
clearDataList(): void;
getDataList(): Array<Argument>;
setDataList(value: Array<Argument>): void;
addData(value?: Argument, index?: number): Argument;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ArrayValue.AsObject;
static toObject(includeInstance: boolean, msg: ArrayValue): ArrayValue.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: ArrayValue, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): ArrayValue;
static deserializeBinaryFromReader(message: ArrayValue, reader: jspb.BinaryReader): ArrayValue;
}
export namespace ArrayValue {
export type AsObject = {
dataList: Array<Argument.AsObject>,
}
}
export class ProxyValue extends jspb.Message {
getId(): number;
setId(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ProxyValue.AsObject;
static toObject(includeInstance: boolean, msg: ProxyValue): ProxyValue.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: ProxyValue, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): ProxyValue;
static deserializeBinaryFromReader(message: ProxyValue, reader: jspb.BinaryReader): ProxyValue;
}
export namespace ProxyValue {
export type AsObject = {
id: number,
}
}
export class FunctionValue extends jspb.Message {
getId(): number;
setId(value: number): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): FunctionValue.AsObject;
static toObject(includeInstance: boolean, msg: FunctionValue): FunctionValue.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: FunctionValue, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): FunctionValue;
static deserializeBinaryFromReader(message: FunctionValue, reader: jspb.BinaryReader): FunctionValue;
}
export namespace FunctionValue {
export type AsObject = {
id: number,
}
}
export class NullValue extends jspb.Message {
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NullValue.AsObject;
static toObject(includeInstance: boolean, msg: NullValue): NullValue.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: NullValue, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NullValue;
static deserializeBinaryFromReader(message: NullValue, reader: jspb.BinaryReader): NullValue;
}
export namespace NullValue {
export type AsObject = {
}
}
export class UndefinedValue extends jspb.Message {
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): UndefinedValue.AsObject;
static toObject(includeInstance: boolean, msg: UndefinedValue): UndefinedValue.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: UndefinedValue, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): UndefinedValue;
static deserializeBinaryFromReader(message: UndefinedValue, reader: jspb.BinaryReader): UndefinedValue;
}
export namespace UndefinedValue {
export type AsObject = {
}
}
export class DateValue extends jspb.Message {
getDate(): string;
setDate(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): DateValue.AsObject;
static toObject(includeInstance: boolean, msg: DateValue): DateValue.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: DateValue, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): DateValue;
static deserializeBinaryFromReader(message: DateValue, reader: jspb.BinaryReader): DateValue;
}
export namespace DateValue {
export type AsObject = {
date: string,
}
}
export enum MsgCase {
MSG_NOT_SET = 0,
ERROR = 1,
BUFFER = 2,
OBJECT = 3,
ARRAY = 4,
PROXY = 5,
FUNCTION = 6,
NULL = 7,
UNDEFINED = 8,
NUMBER = 9,
STRING = 10,
BOOLEAN = 11,
DATE = 12,
}
}
export class EvalEventMessage extends jspb.Message {
getId(): number;
setId(value: number): void;
export class Method extends jspb.Message {
hasNamedProxy(): boolean;
clearNamedProxy(): void;
getNamedProxy(): Method.Named | undefined;
setNamedProxy(value?: Method.Named): void;
getEvent(): string;
setEvent(value: string): void;
clearArgsList(): void;
getArgsList(): Array<string>;
setArgsList(value: Array<string>): void;
addArgs(value: string, index?: number): string;
hasNumberedProxy(): boolean;
clearNumberedProxy(): void;
getNumberedProxy(): Method.Numbered | undefined;
setNumberedProxy(value?: Method.Numbered): void;
getMsgCase(): Method.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): EvalEventMessage.AsObject;
static toObject(includeInstance: boolean, msg: EvalEventMessage): EvalEventMessage.AsObject;
toObject(includeInstance?: boolean): Method.AsObject;
static toObject(includeInstance: boolean, msg: Method): Method.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: EvalEventMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): EvalEventMessage;
static deserializeBinaryFromReader(message: EvalEventMessage, reader: jspb.BinaryReader): EvalEventMessage;
static serializeBinaryToWriter(message: Method, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Method;
static deserializeBinaryFromReader(message: Method, reader: jspb.BinaryReader): Method;
}
export namespace EvalEventMessage {
export namespace Method {
export type AsObject = {
id: number,
event: string,
argsList: Array<string>,
namedProxy?: Method.Named.AsObject,
numberedProxy?: Method.Numbered.AsObject,
}
export class Named extends jspb.Message {
getId(): number;
setId(value: number): void;
getModule(): Module;
setModule(value: Module): void;
getMethod(): string;
setMethod(value: string): void;
clearArgsList(): void;
getArgsList(): Array<Argument>;
setArgsList(value: Array<Argument>): void;
addArgs(value?: Argument, index?: number): Argument;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Named.AsObject;
static toObject(includeInstance: boolean, msg: Named): Named.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Named, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Named;
static deserializeBinaryFromReader(message: Named, reader: jspb.BinaryReader): Named;
}
export namespace Named {
export type AsObject = {
id: number,
module: Module,
method: string,
argsList: Array<Argument.AsObject>,
}
}
export class Numbered extends jspb.Message {
getId(): number;
setId(value: number): void;
getProxyId(): number;
setProxyId(value: number): void;
getMethod(): string;
setMethod(value: string): void;
clearArgsList(): void;
getArgsList(): Array<Argument>;
setArgsList(value: Array<Argument>): void;
addArgs(value?: Argument, index?: number): Argument;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Numbered.AsObject;
static toObject(includeInstance: boolean, msg: Numbered): Numbered.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Numbered, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Numbered;
static deserializeBinaryFromReader(message: Numbered, reader: jspb.BinaryReader): Numbered;
}
export namespace Numbered {
export type AsObject = {
id: number,
proxyId: number,
method: string,
argsList: Array<Argument.AsObject>,
}
}
export class Fail extends jspb.Message {
getId(): number;
setId(value: number): void;
hasResponse(): boolean;
clearResponse(): void;
getResponse(): Argument | undefined;
setResponse(value?: Argument): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Fail.AsObject;
static toObject(includeInstance: boolean, msg: Fail): Fail.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Fail, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Fail;
static deserializeBinaryFromReader(message: Fail, reader: jspb.BinaryReader): Fail;
}
export namespace Fail {
export type AsObject = {
id: number,
response?: Argument.AsObject,
}
}
export class Success extends jspb.Message {
getId(): number;
setId(value: number): void;
hasResponse(): boolean;
clearResponse(): void;
getResponse(): Argument | undefined;
setResponse(value?: Argument): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Success.AsObject;
static toObject(includeInstance: boolean, msg: Success): Success.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Success, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Success;
static deserializeBinaryFromReader(message: Success, reader: jspb.BinaryReader): Success;
}
export namespace Success {
export type AsObject = {
id: number,
response?: Argument.AsObject,
}
}
export enum MsgCase {
MSG_NOT_SET = 0,
NAMED_PROXY = 1,
NUMBERED_PROXY = 2,
}
}
export class EvalFailedMessage extends jspb.Message {
getId(): number;
setId(value: number): void;
export class Callback extends jspb.Message {
hasNamedCallback(): boolean;
clearNamedCallback(): void;
getNamedCallback(): Callback.Named | undefined;
setNamedCallback(value?: Callback.Named): void;
getResponse(): string;
setResponse(value: string): void;
hasNumberedCallback(): boolean;
clearNumberedCallback(): void;
getNumberedCallback(): Callback.Numbered | undefined;
setNumberedCallback(value?: Callback.Numbered): void;
getMsgCase(): Callback.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): EvalFailedMessage.AsObject;
static toObject(includeInstance: boolean, msg: EvalFailedMessage): EvalFailedMessage.AsObject;
toObject(includeInstance?: boolean): Callback.AsObject;
static toObject(includeInstance: boolean, msg: Callback): Callback.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: EvalFailedMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): EvalFailedMessage;
static deserializeBinaryFromReader(message: EvalFailedMessage, reader: jspb.BinaryReader): EvalFailedMessage;
static serializeBinaryToWriter(message: Callback, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Callback;
static deserializeBinaryFromReader(message: Callback, reader: jspb.BinaryReader): Callback;
}
export namespace EvalFailedMessage {
export namespace Callback {
export type AsObject = {
id: number,
response: string,
namedCallback?: Callback.Named.AsObject,
numberedCallback?: Callback.Numbered.AsObject,
}
export class Named extends jspb.Message {
getModule(): Module;
setModule(value: Module): void;
getCallbackId(): number;
setCallbackId(value: number): void;
clearArgsList(): void;
getArgsList(): Array<Argument>;
setArgsList(value: Array<Argument>): void;
addArgs(value?: Argument, index?: number): Argument;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Named.AsObject;
static toObject(includeInstance: boolean, msg: Named): Named.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Named, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Named;
static deserializeBinaryFromReader(message: Named, reader: jspb.BinaryReader): Named;
}
export namespace Named {
export type AsObject = {
module: Module,
callbackId: number,
argsList: Array<Argument.AsObject>,
}
}
export class Numbered extends jspb.Message {
getProxyId(): number;
setProxyId(value: number): void;
getCallbackId(): number;
setCallbackId(value: number): void;
clearArgsList(): void;
getArgsList(): Array<Argument>;
setArgsList(value: Array<Argument>): void;
addArgs(value?: Argument, index?: number): Argument;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Numbered.AsObject;
static toObject(includeInstance: boolean, msg: Numbered): Numbered.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Numbered, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Numbered;
static deserializeBinaryFromReader(message: Numbered, reader: jspb.BinaryReader): Numbered;
}
export namespace Numbered {
export type AsObject = {
proxyId: number,
callbackId: number,
argsList: Array<Argument.AsObject>,
}
}
export enum MsgCase {
MSG_NOT_SET = 0,
NAMED_CALLBACK = 1,
NUMBERED_CALLBACK = 2,
}
}
export class EvalDoneMessage extends jspb.Message {
getId(): number;
setId(value: number): void;
export class Event extends jspb.Message {
hasNamedEvent(): boolean;
clearNamedEvent(): void;
getNamedEvent(): Event.Named | undefined;
setNamedEvent(value?: Event.Named): void;
getResponse(): string;
setResponse(value: string): void;
hasNumberedEvent(): boolean;
clearNumberedEvent(): void;
getNumberedEvent(): Event.Numbered | undefined;
setNumberedEvent(value?: Event.Numbered): void;
getMsgCase(): Event.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): EvalDoneMessage.AsObject;
static toObject(includeInstance: boolean, msg: EvalDoneMessage): EvalDoneMessage.AsObject;
toObject(includeInstance?: boolean): Event.AsObject;
static toObject(includeInstance: boolean, msg: Event): Event.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: EvalDoneMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): EvalDoneMessage;
static deserializeBinaryFromReader(message: EvalDoneMessage, reader: jspb.BinaryReader): EvalDoneMessage;
static serializeBinaryToWriter(message: Event, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Event;
static deserializeBinaryFromReader(message: Event, reader: jspb.BinaryReader): Event;
}
export namespace EvalDoneMessage {
export namespace Event {
export type AsObject = {
id: number,
response: string,
namedEvent?: Event.Named.AsObject,
numberedEvent?: Event.Numbered.AsObject,
}
export class Named extends jspb.Message {
getModule(): Module;
setModule(value: Module): void;
getEvent(): string;
setEvent(value: string): void;
clearArgsList(): void;
getArgsList(): Array<Argument>;
setArgsList(value: Array<Argument>): void;
addArgs(value?: Argument, index?: number): Argument;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Named.AsObject;
static toObject(includeInstance: boolean, msg: Named): Named.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Named, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Named;
static deserializeBinaryFromReader(message: Named, reader: jspb.BinaryReader): Named;
}
export namespace Named {
export type AsObject = {
module: Module,
event: string,
argsList: Array<Argument.AsObject>,
}
}
export class Numbered extends jspb.Message {
getProxyId(): number;
setProxyId(value: number): void;
getEvent(): string;
setEvent(value: string): void;
clearArgsList(): void;
getArgsList(): Array<Argument>;
setArgsList(value: Array<Argument>): void;
addArgs(value?: Argument, index?: number): Argument;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Numbered.AsObject;
static toObject(includeInstance: boolean, msg: Numbered): Numbered.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: Numbered, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): Numbered;
static deserializeBinaryFromReader(message: Numbered, reader: jspb.BinaryReader): Numbered;
}
export namespace Numbered {
export type AsObject = {
proxyId: number,
event: string,
argsList: Array<Argument.AsObject>,
}
}
export enum MsgCase {
MSG_NOT_SET = 0,
NAMED_EVENT = 1,
NUMBERED_EVENT = 2,
}
}
@@ -151,3 +668,12 @@ export namespace Pong {
}
}
export enum Module {
CHILDPROCESS = 0,
FS = 1,
NET = 2,
NODEPTY = 3,
SPDLOG = 4,
TRASH = 5,
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
syntax = "proto3";
// Sent when a shared process becomes active
message SharedProcessActiveMessage {
message SharedProcessActive {
string socket_path = 1;
string log_path = 2;
}
}

View File

@@ -3,7 +3,7 @@
import * as jspb from "google-protobuf";
export class SharedProcessActiveMessage extends jspb.Message {
export class SharedProcessActive extends jspb.Message {
getSocketPath(): string;
setSocketPath(value: string): void;
@@ -11,16 +11,16 @@ export class SharedProcessActiveMessage extends jspb.Message {
setLogPath(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): SharedProcessActiveMessage.AsObject;
static toObject(includeInstance: boolean, msg: SharedProcessActiveMessage): SharedProcessActiveMessage.AsObject;
toObject(includeInstance?: boolean): SharedProcessActive.AsObject;
static toObject(includeInstance: boolean, msg: SharedProcessActive): SharedProcessActive.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: SharedProcessActiveMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): SharedProcessActiveMessage;
static deserializeBinaryFromReader(message: SharedProcessActiveMessage, reader: jspb.BinaryReader): SharedProcessActiveMessage;
static serializeBinaryToWriter(message: SharedProcessActive, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): SharedProcessActive;
static deserializeBinaryFromReader(message: SharedProcessActive, reader: jspb.BinaryReader): SharedProcessActive;
}
export namespace SharedProcessActiveMessage {
export namespace SharedProcessActive {
export type AsObject = {
socketPath: string,
logPath: string,

View File

@@ -11,8 +11,7 @@ var jspb = require('google-protobuf');
var goog = jspb;
var global = Function('return this')();
goog.exportSymbol('proto.SharedProcessActiveMessage', null, global);
goog.exportSymbol('proto.SharedProcessActive', null, global);
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
@@ -23,15 +22,20 @@ goog.exportSymbol('proto.SharedProcessActiveMessage', null, global);
* @extends {jspb.Message}
* @constructor
*/
proto.SharedProcessActiveMessage = function(opt_data) {
proto.SharedProcessActive = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.SharedProcessActiveMessage, jspb.Message);
goog.inherits(proto.SharedProcessActive, jspb.Message);
if (goog.DEBUG && !COMPILED) {
proto.SharedProcessActiveMessage.displayName = 'proto.SharedProcessActiveMessage';
/**
* @public
* @override
*/
proto.SharedProcessActive.displayName = 'proto.SharedProcessActive';
}
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto suitable for use in Soy templates.
@@ -43,8 +47,8 @@ if (jspb.Message.GENERATE_TO_OBJECT) {
* for transitional soy proto support: http://goto/soy-param-migration
* @return {!Object}
*/
proto.SharedProcessActiveMessage.prototype.toObject = function(opt_includeInstance) {
return proto.SharedProcessActiveMessage.toObject(opt_includeInstance, this);
proto.SharedProcessActive.prototype.toObject = function(opt_includeInstance) {
return proto.SharedProcessActive.toObject(opt_includeInstance, this);
};
@@ -53,11 +57,11 @@ proto.SharedProcessActiveMessage.prototype.toObject = function(opt_includeInstan
* @param {boolean|undefined} includeInstance Whether to include the JSPB
* instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.SharedProcessActiveMessage} msg The msg instance to transform.
* @param {!proto.SharedProcessActive} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.SharedProcessActiveMessage.toObject = function(includeInstance, msg) {
proto.SharedProcessActive.toObject = function(includeInstance, msg) {
var f, obj = {
socketPath: jspb.Message.getFieldWithDefault(msg, 1, ""),
logPath: jspb.Message.getFieldWithDefault(msg, 2, "")
@@ -74,23 +78,23 @@ proto.SharedProcessActiveMessage.toObject = function(includeInstance, msg) {
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.SharedProcessActiveMessage}
* @return {!proto.SharedProcessActive}
*/
proto.SharedProcessActiveMessage.deserializeBinary = function(bytes) {
proto.SharedProcessActive.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.SharedProcessActiveMessage;
return proto.SharedProcessActiveMessage.deserializeBinaryFromReader(msg, reader);
var msg = new proto.SharedProcessActive;
return proto.SharedProcessActive.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.SharedProcessActiveMessage} msg The message object to deserialize into.
* @param {!proto.SharedProcessActive} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.SharedProcessActiveMessage}
* @return {!proto.SharedProcessActive}
*/
proto.SharedProcessActiveMessage.deserializeBinaryFromReader = function(msg, reader) {
proto.SharedProcessActive.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
@@ -118,9 +122,9 @@ proto.SharedProcessActiveMessage.deserializeBinaryFromReader = function(msg, rea
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.SharedProcessActiveMessage.prototype.serializeBinary = function() {
proto.SharedProcessActive.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.SharedProcessActiveMessage.serializeBinaryToWriter(this, writer);
proto.SharedProcessActive.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
@@ -128,11 +132,11 @@ proto.SharedProcessActiveMessage.prototype.serializeBinary = function() {
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.SharedProcessActiveMessage} message
* @param {!proto.SharedProcessActive} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.SharedProcessActiveMessage.serializeBinaryToWriter = function(message, writer) {
proto.SharedProcessActive.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getSocketPath();
if (f.length > 0) {
@@ -155,13 +159,13 @@ proto.SharedProcessActiveMessage.serializeBinaryToWriter = function(message, wri
* optional string socket_path = 1;
* @return {string}
*/
proto.SharedProcessActiveMessage.prototype.getSocketPath = function() {
proto.SharedProcessActive.prototype.getSocketPath = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
};
/** @param {string} value */
proto.SharedProcessActiveMessage.prototype.setSocketPath = function(value) {
proto.SharedProcessActive.prototype.setSocketPath = function(value) {
jspb.Message.setProto3StringField(this, 1, value);
};
@@ -170,13 +174,13 @@ proto.SharedProcessActiveMessage.prototype.setSocketPath = function(value) {
* optional string log_path = 2;
* @return {string}
*/
proto.SharedProcessActiveMessage.prototype.getLogPath = function() {
proto.SharedProcessActive.prototype.getLogPath = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
};
/** @param {string} value */
proto.SharedProcessActiveMessage.prototype.setLogPath = function(value) {
proto.SharedProcessActive.prototype.setLogPath = function(value) {
jspb.Message.setProto3StringField(this, 2, value);
};

View File

@@ -3,14 +3,14 @@ import * as path from "path";
import { Readable } from "stream";
import * as util from "util";
import { createClient } from "@coder/protocol/test";
const client = createClient();
jest.mock("../src/fill/client", () => ({ client }));
const cp = require("../src/fill/child_process") as typeof import("child_process");
import { Module } from "../src/common/proxy";
describe("child_process", () => {
const client = createClient();
const cp = client.modules[Module.ChildProcess];
const getStdout = async (proc: ChildProcess): Promise<string> => {
return new Promise((r): Readable => proc.stdout.on("data", r))
return new Promise((r): Readable => proc.stdout!.once("data", r))
.then((s) => s.toString());
};
@@ -36,10 +36,10 @@ describe("child_process", () => {
it("should cat", async () => {
const proc = cp.spawn("cat", []);
expect(proc.pid).toBe(-1);
proc.stdin.write("banana");
proc.stdin!.write("banana");
await expect(getStdout(proc)).resolves.toBe("banana");
proc.stdin.end();
proc.stdin!.end();
proc.kill();
expect(proc.pid).toBeGreaterThan(-1);
@@ -53,6 +53,11 @@ describe("child_process", () => {
await expect(getStdout(proc)).resolves.toContain("hi=donkey\n");
});
it("should eval", async () => {
const proc = cp.spawn("node", ["-e", "console.log('foo')"]);
await expect(getStdout(proc)).resolves.toContain("foo");
});
});
describe("fork", () => {
@@ -71,4 +76,28 @@ describe("child_process", () => {
await new Promise((r): ChildProcess => proc.on("exit", r));
});
});
it("should dispose", (done) => {
setTimeout(() => {
client.dispose();
done();
}, 100);
});
it("should disconnect", async () => {
const client = createClient();
const cp = client.modules[Module.ChildProcess];
const proc = cp.fork(path.join(__dirname, "forker.js"));
const fn = jest.fn();
proc.on("error", fn);
proc.send({ bananas: true });
await expect(new Promise((r): ChildProcess => proc.on("message", r)))
.resolves.toMatchObject({
bananas: true,
});
client.dispose();
expect(fn).toHaveBeenCalledWith(new Error("disconnected"));
});
});

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