Compare commits

..

211 Commits

Author SHA1 Message Date
Anmol Sethi
a4a03c1492 Fix CI 2020-10-12 03:08:24 -04:00
Anmol Sethi
d7ba9ae633 v3.6.0 2020-10-12 01:18:55 -04:00
Anmol Sethi
00383b79b9 Merge pull request #2099 from cdr/open-in
Make opening in an existing instance work outside code-server
2020-10-12 01:14:15 -04:00
Asher
c6ba12942c Filter blank plugin directories (#2187)
I neglected to realize that "".split(":") is an array with "" in it.
2020-10-11 02:14:43 -04:00
Asher
d7e3112625 Update standalone test 2020-10-09 18:01:43 -05:00
Asher
26c735b434 Remove tryParse
Now that the exception handling happens further up there doesn't seem to
be an advantage in having this in a separate method.
2020-10-09 17:05:21 -05:00
Asher
466a04f874 Remove pointless use of openInFlagCount
It'll always be zero here.
2020-10-09 16:57:45 -05:00
Asher
e0769dc13a Move config file info log
Otherwise it outputs when trying to open a file in an existing instance
externally. Externally there isn't an environment variable to branch on
to skip this line so instead output it with the other info lines in the
child process.
2020-10-09 16:57:44 -05:00
Asher
fe19391c03 Read most recent socket path from file 2020-10-09 16:57:43 -05:00
Asher
021c084e43 Move log level defaults into setDefaults
This will allow cliArgs to be only the actual arguments the user passed
which will be used for some logic around opening in existing instances.
2020-10-09 16:57:42 -05:00
Asher
1902296702 Remove references to --open-in flag 2020-10-09 16:57:41 -05:00
Asher
bb1bf88439 Fix wrapper.start not actually waiting for anything 2020-10-09 16:57:41 -05:00
Asher
0a8e71c647 Refactor wrapper
- Immediately create ipcMain so it doesn't have to be a function which I
  think feels cleaner.
  - Move exit handling to a separate function to compensate (otherwise
    the VS Code CLI for example won't be able to exit on its own).
- New isChild prop that is clearer than checking for parentPid (IMO).
- Skip all the checks that aren't necessary for the child process (like
  --help, --version, etc).
  - Since we check if we're the child in entry go ahead and move the
    wrap code into entry as well since that's basically what it does.
- Use a single catch at the end of the entry.
- Split out the VS Code CLI and existing instance code into separate
  functions.
2020-10-09 16:57:40 -05:00
Asher
6bdaada689 Move uncaught exception handler to wrapper
Feels more appropriate there to me.
2020-10-09 16:50:24 -05:00
Anmol Sethi
811cf3364a install.sh: Allow installing directly onto a remote host (#2183)
Updates #1729

To fully close that issue see the various TODOs.
2020-10-09 15:33:58 -04:00
Anmol Sethi
64a6a460c8 Adjust npm package postinstall to install extension dependencies (#2180)
Closes #1961
2020-10-09 15:00:49 -04:00
Anmol Sethi
1e4e72aa5b Merge pull request #2184 from nhooyr/link-flag-1547
cloud: Rename --coder-bind to --link
2020-10-09 13:53:55 -04:00
Anmol Sethi
fcfb03382a cloud: Add mention of cloud repo 2020-10-09 12:57:48 -04:00
Anmol Sethi
d67bd3f604 cloud: Rename --coder-bind to --link 2020-10-09 12:57:20 -04:00
Anmol Sethi
2d1de749f4 Unlink socket before using (#2181)
See https://stackoverflow.com/a/34881585/4283659

Closes #1538
2020-10-09 12:34:52 -04:00
Asher
c6c293d53a Merge pull request #2147 from cdr/multi-plugin 2020-10-09 11:02:25 -05:00
Anmol Sethi
daa1c86fe0 Merge pull request #2086 from nhooyr/master
Integrate Coder Cloud Agent
2020-10-09 07:52:09 -04:00
Anmol Sethi
9002f118c3 Remove the extra releases for autoupdating purposes 2020-10-09 07:50:58 -04:00
Anmol Sethi
a5b6d080bd Add CS_BETA and note --coder-bind is in beta 2020-10-09 07:50:51 -04:00
Anmol Sethi
9ff37977a8 Make --coder-bind disable HTTPS 2020-10-09 07:39:10 -04:00
Anmol Sethi
f5489cd3a0 Hide -coder-bind for now 2020-10-09 07:38:38 -04:00
Asher
c86d7398ab Use system data directory for plugins 2020-10-08 16:18:31 -05:00
Asher
9f963c7e66 Update Node to 12.18.4 (#2175) 2020-10-08 16:15:05 -05:00
Anmol Sethi
8063c79e44 Patch VS Code to avoid deleting extension dependencies (#2170)
Closes #1961
2020-10-08 16:55:13 -04:00
Anmol Sethi
febf4ead96 Fix the clean script
🤦
2020-10-07 17:40:19 -04:00
Anmol Sethi
3e28ab85a0 Add debug log for options passed to the agent 2020-10-07 17:40:19 -04:00
Anmol Sethi
85b0804be5 Remove cliArgs from main
No purpose when all the args are in the args parameter.

We only need configArgs for bindAddrFromAllSources.
2020-10-07 17:40:19 -04:00
Anmol Sethi
ebbcb8d6a7 Update yarn.lock 2020-10-07 17:40:19 -04:00
Anmol Sethi
df3089f3ad coder-cloud: Use consolidated bind command 2020-10-07 17:40:19 -04:00
Anmol Sethi
7cc16ceb3a Document KEEP_MODULES 2020-10-07 16:16:01 -04:00
Anmol Sethi
bfe731f4f3 Ensure socket is undefined with --coder-bind 2020-10-07 16:16:01 -04:00
Anmol Sethi
c4f1c053bf Show valid values for --auth in --help
See https://github.com/nhooyr/code-server/pull/1/files#r485847134
2020-10-07 15:58:30 -04:00
Anmol Sethi
4b3c089630 Remove dead code 2020-10-07 15:58:30 -04:00
Anmol Sethi
1c16814a89 Update coder-bind docs 2020-10-07 15:58:30 -04:00
Anmol Sethi
c3c24fe4d2 Fixes for @ammarb 2020-10-07 15:58:30 -04:00
Anmol Sethi
6e8248cf0c Fix zip release creation 2020-10-07 15:58:30 -04:00
Anmol Sethi
dd996d8f60 v3.6.0 2020-10-07 15:58:30 -04:00
Anmol Sethi
fae07e14fb Fix Go inside dev image 2020-10-07 15:58:30 -04:00
Anmol Sethi
c308ae0edd Ignore dirty lib/vscode 2020-10-07 15:58:30 -04:00
Anmol Sethi
9035bfa871 Add coder cloud agent binary to build process 2020-10-07 15:58:30 -04:00
Anmol Sethi
22c4a7e10f Make linking and starting code-server to the cloud a single command 2020-10-07 15:58:30 -04:00
Anmol Sethi
607444c695 Switch off debian:8 to debian:10 for the typescript build image
We only want to use an old version for glibc which the centos:7
image takes care of.

The old version of git used in debian:8 was causing problems
with the uid/gid passthrough with no user in passwd.
2020-10-07 15:58:30 -04:00
Anmol Sethi
b22f3cb72f Add $HOME to ./ci/dev/image/run.sh 2020-10-07 15:58:30 -04:00
Anmol Sethi
eacca7d692 Unrelated fixes for CI 2020-10-07 15:58:30 -04:00
Anmol Sethi
0aa98279d6 Fixes for CI 2020-10-07 15:58:30 -04:00
Anmol Sethi
55a7e8b56f Implement automatic cloud proxying 2020-10-07 15:58:30 -04:00
Anmol Sethi
916e24e109 Add support for multiline descriptions 2020-10-07 15:58:30 -04:00
Anmol Sethi
c7c62daa67 Remove unused code in optionDescriptions 2020-10-07 15:58:30 -04:00
Anmol Sethi
579bb94a6c Add coder cloud expose command 2020-10-07 15:58:30 -04:00
Asher
a44b4455f5 Read plugin name from package.json 2020-10-07 12:54:48 -05:00
Asher
548a35c0ee Merge pull request #2146 from cdr/listen 2020-10-07 12:50:54 -05:00
Asher
402f5ebd77 Update VS code to 1.49.3 (#2081) 2020-10-07 12:37:37 -05:00
Asher
c2ac126a50 Log all plugin errors as errors 2020-10-07 12:25:42 -05:00
Anmol Sethi
b3811a67e0 Add $KEEP_MODULES argument to build-release.sh (#2167) 2020-10-07 13:24:31 -04:00
Asher
ddda280df4 Rename plugin vars and make both colon-separated
Only one was colon separated but now they both are.
2020-10-07 12:18:57 -05:00
Ben Potter
b415b7524f Add social badges (#2142) 2020-10-06 16:29:53 -05:00
Asher
7a982555a8 Add version to plugin load log 2020-09-30 15:52:40 -05:00
Asher
e64b186527 Add variables to better customize plugin directories 2020-09-30 15:52:39 -05:00
Asher
11eaf0b470 Fix being unable to use [::] for the host
Fixes #1582.
2020-09-30 12:49:36 -05:00
Asher
8b5deac92b Fix 80 getting dropped from bind-addr 2020-09-30 11:57:23 -05:00
Asher
9d87c5328c Add robots.txt (#2080)
Closes #1886.
2020-09-14 17:34:48 -05:00
Anmol Sethi
cc5ed1eb57 Allow installing extensions from the CLI while $VSCODE_IPC_HOOK_CLI
Closes #2083
2020-09-11 11:49:22 -04:00
David Harkness
e998dc1e82 Minor readme grammar fixes (#2074) 2020-09-10 18:01:39 -05:00
Asher
ffe6a663aa Add /vscode to nls fetch
A plugin may modify the root endpoint which will make /resource no
longer work so always use /vscode/resource instead.
2020-09-09 12:05:44 -05:00
Asher
938b460685 Add trailing slash to service worker scope
This will ensure it always matches or is underneath the allowed service
worker scope.

Fixes #2076.
2020-09-09 12:05:04 -05:00
Asher
fef619aef8 Fix incorrect login script src path 2020-09-08 14:06:41 -05:00
Asher
0a2328c1f6 Don't require auth for healthz (#2055)
* Don't require authentication for healthz endpoint

* Add FAQ entry for /healthz
2020-09-08 13:59:01 -05:00
Asher
e44e574ce1 Fix language packs (#2058)
* Fix incorrect nls.json fetch

When moving this out of the HTML I didn't remove {{BASE}}.

* Fix language package installation

Updates #2046.
2020-09-04 10:10:40 -05:00
Anmol Sethi
7991e09bbc Skip update tests (#2059)
We don't use auto updating anymore and the tests are randomly failing
so just disabling for now.
2020-09-04 06:30:15 -04:00
Anmol Sethi
9fb318cf15 docker: Fix $DOCKER_USER (#2057)
We do not try renaming $HOME anymore as there is no good way
to do it.

We also only try to convert if the user hasn't been changed.

Finally I added usage to the docker docs in install.md

Closes #2056
2020-09-03 18:38:40 -04:00
Asher
4a250be79a Use --full-index for patch
This should eliminate potential noise in the diffs for the patch since
different versions seem to default to different hash lengths.
2020-09-03 14:32:51 -05:00
Asher
3761f7bd51 Patch VS Code to wait for storage write (#2049)
VS Code has a short delay before writing storage (probably to queue up
rapid changes). In the web version of VS Code this happens on the client
which means if the page is reloaded before the delay expires the write
never happens.

Storage updates are already promises so this simply returns the promise
returned by the delayer so it won't resolve until the write actually
happens.

Fixes #2021.
2020-09-03 13:57:46 -05:00
Anmol Sethi
ceceef1dae Add documentation issue template 2020-09-03 14:56:24 -04:00
Anmol Sethi
35a2d71b67 Minor release process fixes (#2042) 2020-09-03 02:16:57 -04:00
Asher
617cd38c71 Fix my bad conflict resolution from the github ui 2020-08-31 11:10:12 -05:00
Jacob Goldman
75c8fdeed2 Added /healthz JSON response for heartbeat data. #1940 (#1984) 2020-08-31 10:29:12 -05:00
Anmol Sethi
de41646fc4 Fix path of systemd system service in nfpm 2020-08-31 05:22:52 -04:00
Anmol Sethi
882a2bfd5a Merge pull request #2024 from cdr/v3.5.0
v3.5.0
2020-08-31 04:55:25 -04:00
Anmol Sethi
b509063e14 v3.5.0 2020-08-29 17:23:20 -04:00
Anmol Sethi
1a82b2138d Revert accidental version update 2020-08-29 17:17:49 -04:00
Anmol Sethi
ada69969ac Link to code-server job listing (#2019) 2020-08-28 18:39:51 -04:00
Siva
3f508e5e12 Escape $ inside the systemctl doc string (#2018) 2020-08-28 12:21:00 -04:00
Asher
ce8577b1c3 Remove open-in flag (#2013) 2020-08-27 15:04:37 -05:00
Anmol Sethi
d8d5908d85 Merge pull request #2001 from cdr/docker-user-1c5d
docker: Allow passing $DOCKER_USER to set the username in the container
2020-08-27 15:51:28 -04:00
Anmol Sethi
1558ff6dac Streamline dev container workflow (#2014) 2020-08-27 15:39:24 -04:00
Anmol Sethi
4b7c2ea322 Use static version of node for all builds, not just darwin
This way, building a standalone release locally and putting it in the
release contianer for testing is less likely to break.
2020-08-27 14:20:56 -04:00
Anmol Sethi
4c4a7413a1 docker: Allow passing $DOCKER_USER to set the username in the container
Needs to be reflected in the documentation and the dockerhub description now.

Closes #881
2020-08-27 14:20:56 -04:00
shayne
ceb2265b14 Allow opening files, folders, and workspaces in existing code-server from CLI (#1994)
Add initial support for opening files / folders in running code-server instance.

Current limitations:

- unable to open a file in a new window, only folders
- unable to use addMode feature
- others...
2020-08-27 13:06:21 -05:00
Anmol Sethi
221e95ee89 Bundle systemd system unit (#1997)
systemd's user units are buggy on certain versions
and do not linger by default.

Closes #1771
Closes #1673
Closes #1882
Closes #1861
2020-08-27 13:20:50 -04:00
Anmol Sethi
255fa37e1d Bundle systemd system unit
systemd's user units are buggy on certain versions
and do not linger by default.

Closes #1771
Closes #1673
Closes #1882
Closes #1861
2020-08-27 13:20:21 -04:00
Anmol Sethi
864a9e7bd6 Merge pull request #1999 from cdr/update
Update dependencies in package.json
2020-08-27 13:17:18 -04:00
Asher
a839da34d7 Remove custom offline text (#2007)
We need the handler to be recognized as a PWA but we can just let the
original offline browser message show instead of our own message.

See #1925 and #1979.
2020-08-27 11:33:34 -05:00
Anmol Sethi
3912e9e333 Downgrade node types to v12 2020-08-27 11:04:43 -04:00
Asher
eebb8bb314 Add proposed API flag (#2002)
Co-authored-by: giddyuptiger <65830808+giddyuptiger@users.noreply.github.com>
2020-08-26 14:18:40 -05:00
Anmol Sethi
ebbb1187da Update remaining dependencies 2020-08-26 14:27:30 -04:00
Anmol Sethi
c8f63b61c4 Fix fmt and lint 2020-08-26 14:21:37 -04:00
Anmol Sethi
c80d093dc4 Update dependencies in package.json
See #1898 and #1905
2020-08-26 13:59:41 -04:00
Anmol Sethi
6cc91869d3 doc: Update npm docs for debian sid
There is no python package anymore, you have to explicitly
pick which version of python to install and we need to inform
npm of the executable name.
2020-08-26 13:54:42 -04:00
Anmol Sethi
bf09c294cc Revert "Upgrade to latest typescript"
This reverts commit 6539dd4dbe.

Breaks linting and wasn't required.
2020-08-26 13:32:50 -04:00
Anmol Sethi
536ccc0f10 doc: Simplify build process docs (#2004)
Much easier for users to use our CI scripts instead of each individual
yarn step.
2020-08-26 13:29:42 -04:00
Anmol Sethi
312a4d584c doc: Improve docker example to mount in $HOME for easy config access
I'm surprised no one has asked any questions about this.
2020-08-26 13:00:10 -04:00
Anmol Sethi
a730bec6f4 Ship with node 12 (#1998)
See #1894
See #1892
See #1810
2020-08-26 11:54:50 -04:00
Asher
ce2eaf2f10 Update to VS Code 1.48.2 (#2000) 2020-08-26 10:18:47 -05:00
Anmol Sethi
5c6cd11836 Fix clean.sh
tsc doesn't check if the output exists when incremental is true.

i.e if I delete the out directory, but keep the tsbuildinfo and
try to rebuild, nothing happens cause it thinks everything is
up to date I guess...

With this change, yarn clean will now remove the tsbuildinfo correctly
so things work as expected.
2020-08-26 10:33:59 -04:00
Anmol Sethi
6539dd4dbe Upgrade to latest typescript
Otherwise the build keeps failing for me as tsc won't compile anymore.

Not sure why things work on CI/for asher but I don't think this will
cause any additional issues.
2020-08-26 07:50:51 -04:00
Anmol Sethi
e8ac0d33f9 Document release branches (#1995)
* Document release branches

* Update doc/CONTRIBUTING.md

Co-authored-by: Asher <ash@coder.com>

Co-authored-by: Asher <ash@coder.com>
2020-08-26 07:08:02 -04:00
Asher
e237589f2e Update VS Code to 1.48.0 (#1982) 2020-08-25 13:06:41 -05:00
Anmol Sethi
98d8d848a5 Switch from subreddit to GitHub discussions for support (#1993) 2020-08-25 12:13:11 -04:00
josephrocca
c8ce380f10 instanceIp --> username@instanceIp (#1921)
* instanceIp --> username@instanceIp

* [user]@<instance-ip>

Co-authored-by: Anmol Sethi <hi@nhooyr.io>

Co-authored-by: Anmol Sethi <hi@nhooyr.io>
2020-08-20 10:55:03 -04:00
Asher
c6f054ad6f Fix watch exiting if no plugin 2020-08-18 16:34:59 -05:00
Asher
74910ffcdf Hotswap on SIGUSR1 (#1970) 2020-08-17 14:17:55 -05:00
Asher
3c90b1e327 Merge pull request #1969 from cdr/qol
qol changes
2020-08-17 14:17:29 -05:00
Asher
0dcf469725 Add @version information to --help
This mimics a recent change in VS Code's help. See #1965.
2020-08-13 18:08:35 -05:00
Asher
d8568ebaa9 Enforce import order 2020-08-13 17:11:35 -05:00
Asher
f7790c9719 Remove unused deep merge code 2020-08-13 17:11:34 -05:00
Asher
150d37868a Enforce strict equals 2020-08-13 17:11:33 -05:00
Asher
8590f80c31 Remove unnecessary tsc settings 2020-08-13 17:11:33 -05:00
Asher
d6d24966be Move vscode JS to a separate file
Mostly to match everything else.
2020-08-13 17:10:59 -05:00
Asher
751a5ea3ad Move login JS to a separate file
Mostly so the base URL resolution code can be shared.
2020-08-13 17:10:54 -05:00
Asher
de568d446b Add cookie domain debug logs
To help debug login issues.
2020-08-13 17:06:32 -05:00
Asher
7d02f34f71 Merge pull request #1934 from cdr/plugin
Add plugin system for adding http endpoints
2020-08-13 16:59:44 -05:00
G r e y
2fad8a2a58 Merge pull request #1955 from cdr/callback-type
Add Callback type
2020-08-11 00:41:28 -04:00
G r e y
a0ff2014c3 Add Callback type
Adds a reusable Callback type that is applied to emitter.ts for improved
readability/simplicity.
2020-08-10 21:41:46 -05:00
G r e y
8d03c22cb0 Merge pull request #1956 from cdr/plural
Update common/util::plural
2020-08-10 17:44:06 -04:00
G r e y
6e27869c09 Add str param to plural util
Adds a str param to common/util::plural for pluralizing a string.
Applies plural to entry.ts.
2020-08-09 00:06:18 -05:00
Asher
934c8d4eb6 Clarify exported types and ipc.d.ts 2020-08-05 13:00:37 -05:00
Asher
9b979ac869 Document code-server injection 2020-08-05 13:00:37 -05:00
Asher
3badf6bf7b Use ?? for base default 2020-08-05 13:00:36 -05:00
Asher
10c2b956ac Remove leading slash trim in base resolver
It's not necessary since we return early if the path starts with a
slash.
2020-08-05 13:00:35 -05:00
Asher
543d64268d Simplify valid path check 2020-08-05 13:00:34 -05:00
Asher
fd36f8c168 Use error log level for plugin load failure 2020-08-05 13:00:33 -05:00
G r e y
c78d164948 Fix nfpm typo (#1943) 2020-08-05 12:48:41 -04:00
Anmol Sethi
4dd2c86cca FAQ: Demonstrate how to switch the marketplace 2020-08-04 10:11:55 -04:00
Asher
42467b3e66 Watch plugin and restart when it changes 2020-07-31 17:42:49 -05:00
Asher
361e7103ea Enable loading external plugins 2020-07-31 17:42:48 -05:00
Asher
bac948ea6f Add plugin system 2020-07-31 15:08:02 -05:00
Asher
1c8eede1aa Add missing types to release
code-server exports its types but they weren't complete since it imports
ipc.d.ts and that wasn't being included.
2020-07-31 14:08:00 -05:00
Asher
486652abaf Update standalone test to account for timestamp
The updated logger outputs timestamps now.
2020-07-31 14:06:49 -05:00
Asher
5370f7876d Merge pull request #1927 from cdr/dead-code
Remove dead code
2020-07-31 12:25:56 -05:00
Asher
eccaf8eb50 Merge pull request #1931 from cdr/rimraf
Fix package step
2020-07-31 12:25:18 -05:00
Asher
cbf7c9556c Merge pull request #1920 from fxxjdedd/patch-1
feat: persist route query to local
2020-07-31 11:36:25 -05:00
Asher
b63cf192b5 Remove broken symlinks in extensions node modules
The broken symlinks cause nfpm to fail.
2020-07-31 10:49:45 -05:00
Asher
50ed29e0f0 Move rimraf to prod deps in extensions
The postinstall uses rimraf so it needs to exist in the final build.
2020-07-31 10:49:40 -05:00
futengda
ecb9bb2428 refactor: write lastVisited and query at the same time
In addition, the `settings.write` method now uses shallow merge by default
2020-07-31 12:25:20 +08:00
Asher
e86c066438 Add helper functions to make some code clearer 2020-07-30 12:14:31 -05:00
futengda
b6e791f7d0 refactor: write route.query via settings.write
I added a shallow parameter, because the query should not be extends, but should be replaced directly.
2020-07-30 16:54:02 +08:00
Asher
c581bca29d Force minimist update 2020-07-29 18:48:08 -05:00
Asher
2fa5037859 Log output to disk 2020-07-29 18:48:07 -05:00
Asher
7c2ca7d03e Add the ability to prepend to the proxy path
This is for applications like Jupyter that aren't base path agnostic.
2020-07-29 18:48:06 -05:00
Asher
c67d31580f Include details if any in JSON requests 2020-07-29 18:48:05 -05:00
Asher
58bd7008b4 Make dispose async 2020-07-29 18:48:04 -05:00
Asher
4b6c0a6fc3 Update logger 2020-07-29 18:48:03 -05:00
Asher
554b6d6fcf Remove apply portion of update endpoint
It can still be used to check for updates but will not apply them.

For now also remove the update check loop in VS Code since it's
currently unused (update check is hardcoded off right now) and won't
work anyway since it also applies the update which now won't work. In
the future we should integrate the check into the browser update
service.
2020-07-29 18:48:02 -05:00
jae
8021385ac4 Add enterprise context (#1923) 2020-07-29 13:37:19 -04:00
fxxjdedd
5ba650bb6f feat: persist route query to local
Provide a way for the shell script running in the docker container to get the url query.
2020-07-28 20:14:52 +08:00
Asher
e8f6d30055 Make providers endpoint-agnostic
A provider can now be registered on multiple endpoints (or potentially
moved if needed).
2020-07-27 12:00:48 -05:00
Asher
2819fd51e2 Remove unused endpoints
- dashboard
- app api
2020-07-27 12:00:42 -05:00
Anmol Sethi
638ab7c557 Fix CI 2020-07-22 18:31:24 -04:00
Anmol Sethi
0bd808270d doc/guide: Fix TOC 2020-07-22 17:29:45 -04:00
Anmol Sethi
bc78e16146 doc/guide: Improve nginx docs (#1902)
Made it a full alternative to caddy, just so we don't ever have to explain how to configure Nginx again.
2020-07-22 16:05:39 -04:00
Anmol Sethi
3764d296c6 .github/lock.yml: Formatting 2020-07-22 15:15:30 -04:00
Anmol Sethi
90eec91f9c Add .github/lock.yml
Too many people comment on super old issues.
2020-07-22 14:16:53 -04:00
Asher
d3164fc910 Merge pull request #1867 from Niek/patch-1
Add Nginx instructions to guide
2020-07-21 17:16:57 -05:00
Asher
6c5a9edced Tiny text changes 2020-07-21 17:16:32 -05:00
Asher
4727385a01 Merge pull request #1885 from cdr/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-07-21 13:56:29 -05:00
Asher
89cfe6876e Merge pull request #1896 from cdr/vscode-1.47.2
Update to VS Code 1.47.2
2020-07-21 13:51:22 -05:00
Asher
de8e9804ad Update to VS Code 1.47.2 2020-07-21 13:16:44 -05:00
Asher
81d25dd048 Add missing bootstrap-node.js to final build
Fixes #1884.
2020-07-21 11:31:27 -05:00
dependabot[bot]
0193516f55 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-17 01:02:31 +00:00
Kyle Carberry
19d14d2414 Add ThirdPartyNotices.txt 2020-07-16 19:01:09 -06:00
Niek van der Maas
739ac468ed Merge branch 'master' into patch-1 2020-07-16 19:25:25 +02:00
Niek van der Maas
6c3e4d2a76 Add trailing / 2020-07-16 19:24:13 +02:00
Asher
fb9ebeb7aa Merge pull request #1874 from cdr/vscode-1.47.0
Update VS Code to 1.47.0
2020-07-10 16:02:34 -05:00
Asher
641b36be6a Update VS Code to 1.47.0 2020-07-09 17:04:11 -05:00
Asher
e858a4f4c7 Merge pull request #1869 from cdr/vscode-1.46.1
Update VS Code to 1.46.1
2020-07-07 18:09:36 -05:00
Asher
a06522f254 Update VS Code to 1.46.1 2020-07-07 17:01:23 -05:00
Niek van der Maas
96af9761b7 Add Nginx instructions to guide
Added Nginx instructions for people who prefer to use this instead of Caddy
2020-07-06 10:45:09 +02:00
Asher
9ff0e455c3 Merge pull request #1853 from cdr/fix-heartbeat
Fix connections sticking around indefinitely
2020-06-30 17:43:36 -05:00
Asher
ebef18d626 Fix connections sticking around indefinitely
For some reason it only affects the extension host socket (something to
do with passing it via IPC?) but I changed both just to be sure.

Fixes #1795.
2020-06-30 16:41:47 -05:00
Ammar Bandukwala
a942531079 Update remote location restrictions in README 2020-06-25 15:33:55 -05:00
Kyle Carberry
cc9c9e9db5 Remove funding statement 2020-06-25 14:01:34 -06:00
Anmol Sethi
a7026cc82c Remove funding figure 2020-06-25 12:18:15 -04:00
Asher
ed285f97fd Merge pull request #1830 from cdr/fix-config
Initialize config and use correct settings path
2020-06-24 12:26:18 -05:00
Asher
1b7d4b5a18 Initialize config and use correct settings path
Fixes #1829.
2020-06-24 11:40:17 -05:00
Anmol Sethi
364f9dd854 Ask for resume/github re hiring 2020-06-23 07:58:28 -04:00
Anmol Sethi
7e1aebe009 Fix formatting 2020-06-22 01:42:46 -04:00
Anmol Sethi
609c7ef4ec Fix bad $PATH when building MacOS
The previous release mistakenly distributed the wrong version
of node...

Very sad.

See https://github.com/cdr/code-server/issues/1710#issuecomment-646472716
2020-06-22 00:57:40 -04:00
Anmol Sethi
5a6411fa49 Make our funding situation clear in Hiring section 2020-06-22 00:57:24 -04:00
Anmol Sethi
3da6c561b8 Fix wording in FAQ.md 2020-06-16 11:14:02 -04:00
Anmol Sethi
bb118eba6e Merge pull request #1808 from SAB-6/typo-fixes
fix typos in doc/FAQ.md
2020-06-16 11:13:07 -04:00
Shereef Bankole
96e57f1e6f fix typos in doc/FAQ.md 2020-06-14 23:46:01 +01:00
Anmol Sethi
2a9b7a4d5f Merge pull request #1806 from cdr/bsd
Add FreeBSD support to install script
2020-06-13 11:16:13 -04:00
Anmol Sethi
3d9e3b8717 Add FreeBSD support to install script 2020-06-13 11:14:32 -04:00
Anmol Sethi
264abed82c Fix typo 2020-06-12 02:32:40 -04:00
Anmol Sethi
19257a8bc2 Fix typo 2020-06-09 18:14:21 -04:00
Anmol Sethi
034266db47 Merge pull request #1790 from cdr/hiring
Mention we're hiring in the README.md
2020-06-09 18:13:57 -04:00
Anmol Sethi
69b8096eb3 Mention we're hiring in the README.md 2020-06-09 18:13:20 -04:00
Anmol Sethi
7b982ae782 Merge pull request #1773 from jeffjose/patch-1
Update --data-dir flag to --user-data-dir
2020-06-05 17:39:30 -04:00
Jeffrey Jose
3a37add48d Update --data-dir flag to --user-data-dir
Update --data-dir flag to --user-data-dir
2020-06-04 23:42:10 -07:00
Anmol Sethi
7958cc7e29 install.sh: Print creation of CACHE_DIR 2020-06-04 18:23:01 -04:00
Anmol Sethi
88c76d4794 Fix typo in guide.md 2020-06-04 16:49:49 -04:00
Anmol Sethi
022a2e0860 Merge branch 'docs' 2020-06-04 16:47:36 -04:00
Anmol Sethi
bd2e55dcf3 Make README more clear 2020-06-04 16:47:27 -04:00
Anmol Sethi
d3773c11f1 Merge pull request #1766 from cdr/v3.4.1
v3.4.1
2020-06-04 12:30:34 -04:00
Anmol Sethi
59694fb72e FAQ: Explain differences compared to Theia
Closes #1756
2020-06-04 07:30:41 -04:00
Anmol Sethi
ac2bf56ebc Explain $SERVICE_URL and $ITEM_URL in more detail
Closes #1762
2020-06-04 07:25:32 -04:00
84 changed files with 3837 additions and 3793 deletions

View File

@@ -23,6 +23,14 @@ rules:
no-dupe-class-members: off
"@typescript-eslint/no-use-before-define": off
"@typescript-eslint/no-non-null-assertion": off
"@typescript-eslint/ban-types": off
"@typescript-eslint/no-var-requires": off
"@typescript-eslint/explicit-module-boundary-types": off
"@typescript-eslint/no-explicit-any": off
eqeqeq: error
import/order:
[error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }]
no-async-promise-executor: off
settings:
# Does not work with CommonJS unfortunately.

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Question
url: https://github.com/cdr/code-server/discussions/new?category_id=22503114
about: Ask the community for help

7
.github/ISSUE_TEMPLATE/doc.md vendored Normal file
View File

@@ -0,0 +1,7 @@
---
name: Documentation improvement
about: Suggest a documentation improvement
title: ""
labels: "docs"
assignees: ""
---

View File

@@ -1,4 +0,0 @@
<!--
Please file all questions and support requests at https://www.reddit.com/r/codeserver/
The issue tracker is only for bugs and features.
-->

37
.github/lock.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 90
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: false
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: []
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false
# Comment to post before locking. Set to `false` to disable
lockComment: >
This thread has been automatically locked since there has not been
any recent activity after it was closed. Please open a new issue for
related bugs.
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: true
# Limit to only `issues` or `pulls`
# only: issues
# Optionally, specify configuration settings just for `issues` or `pulls`
# issues:
# exemptLabels:
# - help-wanted
# lockLabel: outdated
# pulls:
# daysUntilLock: 30
# Repository to extend settings from
# _extends: repo

View File

@@ -1,4 +1,6 @@
<!--
Please link to the issue this PR solves.
If there is no existing issue, please first create one unless the fix is minor.
Please make sure the base of your PR is the master branch!
-->

View File

@@ -8,7 +8,7 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Run ./ci/steps/fmt.sh
uses: ./ci/images/debian8
uses: ./ci/images/debian10
with:
args: ./ci/steps/fmt.sh
@@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Run ./ci/steps/lint.sh
uses: ./ci/images/debian8
uses: ./ci/images/debian10
with:
args: ./ci/steps/lint.sh
@@ -26,7 +26,7 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Run ./ci/steps/test.sh
uses: ./ci/images/debian8
uses: ./ci/images/debian10
with:
args: ./ci/steps/test.sh
@@ -35,7 +35,7 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Run ./ci/steps/release.sh
uses: ./ci/images/debian8
uses: ./ci/images/debian10
with:
args: ./ci/steps/release.sh
- name: Upload npm package artifact
@@ -116,7 +116,7 @@ jobs:
name: release-packages
path: ./release-packages
- name: Run ./ci/steps/build-docker-image.sh
uses: ./ci/images/debian8
uses: ./ci/images/debian10
with:
args: ./ci/steps/build-docker-image.sh
- name: Upload release image

View File

@@ -10,7 +10,7 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Run ./ci/steps/publish-npm.sh
uses: ./ci/images/debian8
uses: ./ci/images/debian10
with:
args: ./ci/steps/publish-npm.sh
env:
@@ -22,7 +22,7 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Run ./ci/steps/push-docker-manifest.sh
uses: ./ci/images/debian8
uses: ./ci/images/debian10
with:
args: ./ci/steps/push-docker-manifest.sh
env:

3
.gitignore vendored
View File

@@ -10,3 +10,6 @@ release-gcp/
release-images/
node_modules
node-*
/plugins
/lib/coder-cloud-agent
.home

1
.gitmodules vendored
View File

@@ -1,3 +1,4 @@
[submodule "lib/vscode"]
path = lib/vscode
url = https://github.com/microsoft/vscode
ignore = dirty

View File

@@ -1,4 +1,4 @@
# code-server
# code-server &middot; [!["GitHub Discussions"](https://img.shields.io/badge/%20GitHub-%20Discussions-gray.svg?longCache=true&logo=github&colorB=purple)](https://github.com/cdr/code-server/discussions) [!["Join us on Slack"](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen)](https://cdr.co/join-community) [![Twitter Follow](https://img.shields.io/twitter/follow/CoderHQ?label=%40CoderHQ&style=social)](https://twitter.com/coderhq)
Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and access it in the browser.
@@ -11,14 +11,16 @@ Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and a
- Develop on a Linux machine and pick up from any device with a web browser.
- **Server-powered**
- Take advantage of large cloud servers to speed up tests, compilations, downloads, and more.
- Preserve battery life when you're on the go as all intensive tasks runs on your server.
- Preserve battery life when you're on the go as all intensive tasks run on your server.
- Make use of a spare computer you have lying around and turn it into a full development environment.
## Getting Started
For a full setup and walkthrough, please see [./doc/guide.md](./doc/guide.md).
We have a [script](./install.sh) to install code-server for Linux and macOS.
### Quick Install
We have a [script](./install.sh) to install code-server for Linux, macOS and FreeBSD.
It tries to use the system package manager if possible.
@@ -36,14 +38,10 @@ curl -fsSL https://code-server.dev/install.sh | sh
The install script will print out how to run and start using code-server.
If you believe an install script used with `curl | sh` is insecure, please give
[this wonderful blogpost](https://sandstorm.io/news/2015-09-24-is-curl-bash-insecure-pgp-verified-install) by
[sandstorm.io](https://sandstorm.io) a read.
### Manual Install
Docs on the install script, manual installation and docker image are at [./doc/install.md](./doc/install.md).
We also highly recommend reading the [FAQ](./doc/FAQ.md) on the [Differences compared to VS Code](./doc/FAQ.md#differences-compared-to-vs-code).
## FAQ
See [./doc/FAQ.md](./doc/FAQ.md).
@@ -52,7 +50,19 @@ See [./doc/FAQ.md](./doc/FAQ.md).
See [./doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md).
## Enterprise
## Hiring
Visit [our website](https://coder.com) for more information about our
enterprise offerings.
We ([@cdr](https://github.com/cdr)) are looking for engineers to help maintain
code-server, innovate on open source and streamline dev workflows.
Our main office is in Austin, Texas. Remote is ok as long as
you're in North America or Europe.
Please get in [touch](mailto:jobs@coder.com) with your resume/github if interested.
We're also hiring someone specifically to help maintain code-server.
See the listing [here](https://jobs.lever.co/coder/e40becde-2cbd-4885-9029-e5c7b0a734b8).
## For Organizations
Visit [our website](https://coder.com) for more information about remote development for your organization or enterprise.

22
ThirdPartyNotices.txt Normal file
View File

@@ -0,0 +1,22 @@
code-server
THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
Do Not Translate or Localize
1. Microsoft/vscode version 1.47.0 (https://github.com/Microsoft/vscode)
%% Microsoft/vscode NOTICES AND INFORMATION BEGIN HERE
=========================================
MIT License
Copyright (c) 2015 - present Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -18,14 +18,17 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub)
1. Update in `package.json`
2. Update in [./doc/install.md](../doc/install.md)
2. GitHub actions will generate the `npm-package`, `release-packages` and `release-images` artifacts.
1. You do not have to wait for these.
3. Run `yarn release:github-draft` to create a GitHub draft release from the template with
the updated version.
1. Summarize the major changes in the release notes and link to the relevant issues.
4. Wait for the artifacts in step 2 to build.
5. Run `yarn release:github-assets` to download the `release-packages` artifact and then
upload them to the draft release.
5. Run `yarn release:github-assets` to download the `release-packages` artifact.
- It will upload them to the draft release.
6. Run some basic sanity tests on one of the released packages.
7. Make sure the github release tag is the commit with the artifacts.
- Especially make sure the terminal works fine.
7. Make sure the github release tag is the commit with the artifacts. This is a bug in
`hub` where uploading assets in step 5 will break the tag.
8. Publish the release and merge the PR.
1. CI will automatically grab the artifacts and then:
1. Publish the NPM package from `npm-package`.
@@ -35,7 +38,6 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub)
10. Wait for the npm package to be published.
11. Update the homebrew package.
- Send a pull request to [homebrew-core](https://github.com/Homebrew/homebrew-core) with the URL in the [formula](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb) updated.
12. Make sure to add a release without the `v` prefix for autoupdate from `3.2.0`.
## dev

View File

@@ -9,7 +9,8 @@ MINIFY=${MINIFY-true}
main() {
cd "$(dirname "${0}")/../.."
tsc --outDir out --tsBuildInfoFile .cache/out.tsbuildinfo
tsc
# If out/node/entry.js does not already have the shebang,
# we make sure to add it and make it executable.
if ! grep -q -m1 "^#!/usr/bin/env node" out/node/entry.js; then
@@ -17,13 +18,20 @@ main() {
chmod +x out/node/entry.js
fi
if ! [ -f ./lib/coder-cloud-agent ]; then
OS="$(uname | tr '[:upper:]' '[:lower:]')"
curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent
chmod +x ./lib/coder-cloud-agent
fi
parcel build \
--public-url "/static/$(git rev-parse HEAD)/dist" \
--public-url "." \
--out-dir dist \
$([[ $MINIFY ]] || echo --no-minify) \
src/browser/pages/app.ts \
src/browser/register.ts \
src/browser/serviceWorker.ts
src/browser/serviceWorker.ts \
src/browser/pages/login.ts \
src/browser/pages/vscode.ts
}
main "$@"

View File

@@ -11,15 +11,6 @@ main() {
mkdir -p release-packages
release_archive
# Will stop the auto update issues and allow people to upgrade their scripts
# for the new release structure.
if [[ $ARCH == "amd64" ]]; then
if [[ $OS == "linux" ]]; then
ARCH=x86_64 release_archive
elif [[ $OS == "macos" ]]; then
OS=darwin ARCH=x86_64 release_archive
fi
fi
if [[ $OS == "linux" ]]; then
release_nfpm
@@ -30,12 +21,6 @@ release_archive() {
local release_name="code-server-$VERSION-$OS-$ARCH"
if [[ $OS == "linux" ]]; then
tar -czf "release-packages/$release_name.tar.gz" --transform "s/^\.\/release-standalone/$release_name/" ./release-standalone
elif [[ $OS == "darwin" && $ARCH == "x86_64" ]]; then
# Just exists to make autoupdating from 3.2.0 work again.
mv ./release-standalone "./$release_name"
zip -r "release-packages/$release_name.zip" "./$release_name"
mv "./$release_name" ./release-standalone
return
else
tar -czf "release-packages/$release_name.tar.gz" -s "/^release-standalone/$release_name/" release-standalone
fi

View File

@@ -6,6 +6,10 @@ set -euo pipefail
# MINIFY controls whether minified vscode is bundled.
MINIFY="${MINIFY-true}"
# KEEP_MODULES controls whether the script cleans all node_modules requiring a yarn install
# to run first.
KEEP_MODULES="${KEEP_MODULES-0}"
main() {
cd "$(dirname "${0}")/../.."
source ./ci/lib.sh
@@ -21,6 +25,12 @@ main() {
rsync README.md "$RELEASE_PATH"
rsync LICENSE.txt "$RELEASE_PATH"
rsync ./lib/vscode/ThirdPartyNotices.txt "$RELEASE_PATH"
# code-server exports types which can be imported and used by plugins. Those
# types import ipc.d.ts but it isn't included in the final vscode build so
# we'll copy it ourselves here.
mkdir -p "$RELEASE_PATH/lib/vscode/src/vs/server"
rsync ./lib/vscode/src/vs/server/ipc.d.ts "$RELEASE_PATH/lib/vscode/src/vs/server"
}
bundle_code_server() {
@@ -31,6 +41,7 @@ bundle_code_server() {
rsync src/browser/media/ "$RELEASE_PATH/src/browser/media"
mkdir -p "$RELEASE_PATH/src/browser/pages"
rsync src/browser/pages/*.html "$RELEASE_PATH/src/browser/pages"
rsync src/browser/robots.txt "$RELEASE_PATH/src/browser"
# Adds the commit to package.json
jq --slurp '.[0] * .[1]' package.json <(
@@ -45,6 +56,11 @@ EOF
) > "$RELEASE_PATH/package.json"
rsync yarn.lock "$RELEASE_PATH"
rsync ci/build/npm-postinstall.sh "$RELEASE_PATH/postinstall.sh"
if [ "$KEEP_MODULES" = 1 ]; then
rsync node_modules/ "$RELEASE_PATH/node_modules"
rsync ./lib/coder-cloud-agent "$RELEASE_PATH/lib"
fi
}
bundle_vscode() {
@@ -53,7 +69,11 @@ bundle_vscode() {
rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY+-min}/" "$VSCODE_OUT_PATH/out"
rsync "$VSCODE_SRC_PATH/.build/extensions/" "$VSCODE_OUT_PATH/extensions"
rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules"
if [ "$KEEP_MODULES" = 0 ]; then
rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules"
else
rsync "$VSCODE_SRC_PATH/node_modules/" "$VSCODE_OUT_PATH/node_modules"
fi
rsync "$VSCODE_SRC_PATH/extensions/package.json" "$VSCODE_OUT_PATH/extensions"
rsync "$VSCODE_SRC_PATH/extensions/yarn.lock" "$VSCODE_OUT_PATH/extensions"
rsync "$VSCODE_SRC_PATH/extensions/postinstall.js" "$VSCODE_OUT_PATH/extensions"

View File

@@ -5,16 +5,7 @@ main() {
cd "$(dirname "${0}")/../.."
source ./ci/lib.sh
rm -Rf \
out \
release \
release-standalone \
release-packages \
release-gcp \
release-images/ \
dist \
.tsbuildinfo \
.cache/out.tsbuildinfo
git clean -Xffd
pushd lib/vscode
git clean -xffd

View File

@@ -0,0 +1,12 @@
[Unit]
Description=code-server
After=network.target
[Service]
Type=exec
ExecStart=/usr/bin/code-server
Restart=always
User=%i
[Install]
WantedBy=default.target

View File

@@ -12,5 +12,8 @@ homepage: "https://github.com/cdr/code-server"
license: "MIT"
files:
./ci/build/code-server-nfpm.sh: /usr/bin/code-server
./ci/build/code-server.service: /usr/lib/systemd/user/code-server.service
./ci/build/code-server@.service: /usr/lib/systemd/system/code-server@.service
# Only included for backwards compat with previous releases that shipped
# the user service. See #1997
./ci/build/code-server-user.service: /usr/lib/systemd/user/code-server.service
./release-standalone/**/*: "/usr/lib/code-server/"

View File

@@ -24,6 +24,10 @@ main() {
;;
esac
OS="$(uname | tr '[:upper:]' '[:lower:]')"
curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent
chmod +x ./lib/coder-cloud-agent
if ! vscode_yarn; then
echo "You may not have the required dependencies to build the native modules."
echo "Please see https://github.com/cdr/code-server/blob/master/doc/npm.md"
@@ -36,6 +40,13 @@ vscode_yarn() {
yarn --production --frozen-lockfile
cd extensions
yarn --production --frozen-lockfile
for ext in */; do
ext="${ext%/}"
echo "extensions/$ext: installing dependencies"
cd "$ext"
yarn --production --frozen-lockfile
cd "$OLDPWD"
done
}
main "$@"

View File

@@ -15,8 +15,7 @@ main() {
./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --install-extension ms-python.python
local installed_extensions
installed_extensions="$(./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --list-extensions 2>&1)"
if [[ $installed_extensions != "info Using config file ~/.config/code-server/config.yaml
ms-python.python" ]]; then
if [[ $installed_extensions != "ms-python.python" ]]; then
echo "Unexpected output from listing extensions:"
echo "$installed_extensions"
exit 1

View File

@@ -6,7 +6,7 @@ main() {
cd ./lib/vscode
git add -A
git diff HEAD > ../../ci/dev/vscode.patch
git diff HEAD --full-index > ../../ci/dev/vscode.patch
}
main "$@"

View File

@@ -1,13 +0,0 @@
FROM node:12
RUN apt-get update && apt-get install -y \
curl \
iproute2 \
vim \
iptables \
net-tools \
libsecret-1-dev \
libx11-dev \
libxkbfile-dev
CMD ["/bin/bash"]

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Opens an interactive bash session inside of a docker container
# for improved isolation during development.
# If the container exists it is restarted if necessary, then reused.
main() {
cd "$(dirname "${0}")/../../.."
local container_name=code-server-dev
if docker inspect $container_name &> /dev/null; then
echo "-- Starting container"
docker start "$container_name" > /dev/null
enter
exit 0
fi
build
run
enter
}
enter() {
echo "--- Entering $container_name"
docker exec -it "$container_name" /bin/bash
}
run() {
echo "--- Spawning $container_name"
docker run \
-it \
--name $container_name \
"-v=$PWD:/code-server" \
"-w=/code-server" \
"-p=127.0.0.1:8080:8080" \
$(if [[ -t 0 ]]; then echo -it; fi) \
"$container_name"
}
build() {
echo "--- Building $container_name"
docker build -t $container_name ./ci/dev/image > /dev/null
}
main "$@"

31
ci/dev/image/run.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
main() {
cd "$(dirname "$0")/../../.."
source ./ci/lib.sh
mkdir -p .home
docker run \
-it \
--rm \
-v "$PWD:/src" \
-e HOME="/src/.home" \
-e USER="coder" \
-e GITHUB_TOKEN \
-e KEEP_MODULES \
-e MINIFY \
-w /src \
-p 127.0.0.1:8080:8080 \
-u "$(id -u):$(id -g)" \
-e CI \
"$(docker_build ./ci/images/"${IMAGE-debian10}")" \
"$@"
}
docker_build() {
docker build "$@" >&2
docker build -q "$@"
}
main "$@"

View File

@@ -7,10 +7,7 @@ main() {
eslint --max-warnings=0 --fix $(git ls-files "*.ts" "*.tsx" "*.js")
stylelint $(git ls-files "*.css")
tsc --noEmit
# See comment in ./ci/image/debian8
if [[ ! ${CI-} ]]; then
shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh")
fi
shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh")
}
main "$@"

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,9 @@ class Watcher {
const vscode = cp.spawn("yarn", ["watch"], { cwd: this.vscodeSourcePath })
const tsc = cp.spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath })
const plugin = process.env.PLUGIN_DIR
? cp.spawn("yarn", ["build", "--watch"], { cwd: process.env.PLUGIN_DIR })
: undefined
const bundler = this.createBundler()
const cleanup = (code?: number | null): void => {
@@ -48,6 +51,12 @@ class Watcher {
tsc.removeAllListeners()
tsc.kill()
if (plugin) {
Watcher.log("killing plugin")
plugin.removeAllListeners()
plugin.kill()
}
if (server) {
Watcher.log("killing server")
server.removeAllListeners()
@@ -69,6 +78,12 @@ class Watcher {
Watcher.log("tsc terminated unexpectedly")
cleanup(code)
})
if (plugin) {
plugin.on("exit", (code) => {
Watcher.log("plugin terminated unexpectedly")
cleanup(code)
})
}
const bundle = bundler.bundle().catch(() => {
Watcher.log("parcel watcher terminated unexpectedly")
cleanup(1)
@@ -82,6 +97,9 @@ class Watcher {
vscode.stderr.on("data", (d) => process.stderr.write(d))
tsc.stderr.on("data", (d) => process.stderr.write(d))
if (plugin) {
plugin.stderr.on("data", (d) => process.stderr.write(d))
}
// From https://github.com/chalk/ansi-regex
const pattern = [
@@ -140,21 +158,34 @@ class Watcher {
bundle.then(restartServer)
}
})
if (plugin) {
onLine(plugin, (line, original) => {
// tsc outputs blank lines; skip them.
if (line !== "") {
console.log("[plugin]", original)
}
if (line.includes("Watching for file changes")) {
bundle.then(restartServer)
}
})
}
}
private createBundler(out = "dist"): Bundler {
return new Bundler(
[
path.join(this.rootPath, "src/browser/pages/app.ts"),
path.join(this.rootPath, "src/browser/register.ts"),
path.join(this.rootPath, "src/browser/serviceWorker.ts"),
path.join(this.rootPath, "src/browser/pages/login.ts"),
path.join(this.rootPath, "src/browser/pages/vscode.ts"),
],
{
outDir: path.join(this.rootPath, out),
cacheDir: path.join(this.rootPath, ".cache"),
minify: !!process.env.MINIFY,
logLevel: 1,
publicUrl: "/static/development/dist",
publicUrl: ".",
},
)
}

View File

@@ -1,9 +1,10 @@
FROM centos:7
ARG NODE_VERSION=v12.18.4
RUN ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')" && \
curl -fsSL "https://nodejs.org/dist/v14.4.0/node-v14.4.0-linux-$ARCH.tar.xz" | tar -C /usr/local -xJ && \
mv /usr/local/node-v14.4.0-linux-$ARCH /usr/local/node-v14.4.0
ENV PATH=/usr/local/node-v14.4.0/bin:$PATH
curl -fsSL "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-$ARCH.tar.xz" | tar -C /usr/local -xJ && \
mv "/usr/local/node-$NODE_VERSION-linux-$ARCH" "/usr/local/node-$NODE_VERSION"
ENV PATH=/usr/local/node-$NODE_VERSION/bin:$PATH
RUN npm install -g yarn
RUN yum groupinstall -y 'Development Tools'
@@ -14,11 +15,16 @@ RUN npm config set python python2
RUN yum install -y epel-release && yum install -y jq
RUN yum install -y rsync
# Copied from ../debian8/Dockerfile
# Install Go dependencies
# Copied from ../debian10/Dockerfile
# Install Go.
RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \
curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz
ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH
ENV GOPATH=/gopath
# Ensures running this image as another user works.
RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH
ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH
# Install Go dependencies
ENV GO111MODULE=on
RUN go get mvdan.cc/sh/v3/cmd/shfmt
RUN go get github.com/goreleaser/nfpm/cmd/nfpm

View File

@@ -1,4 +1,4 @@
FROM debian:8
FROM debian:10
RUN apt-get update
@@ -6,7 +6,7 @@ RUN apt-get update
RUN apt-get install -y curl gnupg
# Installs node.
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - && \
RUN curl -fsSL https://deb.nodesource.com/setup_12.x | bash - && \
apt-get install -y nodejs
# Installs yarn.
@@ -24,28 +24,23 @@ RUN apt-get install -y build-essential \
RUN apt-get install -y gettext-base
# Misc build dependencies.
RUN apt-get install -y git rsync unzip
# We need latest jq from debian buster for date support.
RUN ARCH="$(dpkg --print-architecture)" && \
curl -fsSOL http://http.us.debian.org/debian/pool/main/libo/libonig/libonig5_6.9.1-1_$ARCH.deb && \
dpkg -i libonig*.deb && \
curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/libjq1_1.5+dfsg-2+b1_$ARCH.deb && \
dpkg -i libjq*.deb && \
curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/jq_1.5+dfsg-2+b1_$ARCH.deb && \
dpkg -i jq*.deb && rm *.deb
RUN apt-get install -y git rsync unzip jq
# Installs shellcheck.
# Unfortunately coredumps on debian:8 so disabled for now.
#RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \
# tar -xJ && \
# mv shellcheck*/shellcheck /usr/local/bin && \
# rm -R shellcheck*
RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \
tar -xJ && \
mv shellcheck*/shellcheck /usr/local/bin && \
rm -R shellcheck*
# Install Go dependencies
# Install Go.
RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \
curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz
ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH
ENV GOPATH=/gopath
# Ensures running this image as another user works.
RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH
ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH
# Install Go dependencies
ENV GO111MODULE=on
RUN go get mvdan.cc/sh/v3/cmd/shfmt
RUN go get github.com/goreleaser/nfpm/cmd/nfpm

View File

@@ -35,9 +35,13 @@ RUN ARCH="$(dpkg --print-architecture)" && \
printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml
COPY release-packages/code-server*.deb /tmp/
COPY ci/release-image/entrypoint.sh /usr/bin/entrypoint.sh
RUN dpkg -i /tmp/code-server*$(dpkg --print-architecture).deb && rm /tmp/code-server*.deb
EXPOSE 8080
USER coder
# This way, if someone sets $DOCKER_USER, docker-exec will still work as
# the uid will remain the same. note: only relevant if -u isn't passed to
# docker-run.
USER 1000
WORKDIR /home/coder
ENTRYPOINT ["dumb-init", "fixuid", "-q", "/usr/bin/code-server", "--bind-addr", "0.0.0.0:8080", "."]
ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."]

21
ci/release-image/entrypoint.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
set -eu
# This isn't set by default.
USER="$(whoami)"
export USER
if [ "${DOCKER_USER-}" != "$USER" ]; then
echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null
# Unfortunately we cannot change $HOME as we cannot move any bind mounts
# nor can we bind mount $HOME into a new home as that requires a privileged container.
sudo usermod --login "$DOCKER_USER" coder
sudo groupmod -n "$DOCKER_USER" coder
USER="$DOCKER_USER"
sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd
sudo sed -i "s/coder/$DOCKER_USER/g" /etc/fixuid/config.yml
fi
dumb-init fixuid -q /usr/bin/code-server "$@"

View File

@@ -4,10 +4,11 @@ set -euo pipefail
main() {
cd "$(dirname "$0")/../.."
if [[ $OSTYPE == darwin* ]]; then
curl -L https://nodejs.org/dist/v14.4.0/node-v14.4.0-darwin-x64.tar.gz | tar -xz
PATH="$PATH:node-v14.4.0-darwin-x64/bin"
fi
NODE_VERSION=v12.18.4
NODE_OS="$(uname | tr '[:upper:]' '[:lower:]')"
NODE_ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')"
curl -L "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-$NODE_OS-$NODE_ARCH.tar.gz" | tar -xz
PATH="$PWD/node-$NODE_VERSION-$NODE_OS-$NODE_ARCH/bin:$PATH"
# https://github.com/actions/upload-artifact/issues/38
tar -xzf release-npm-package/package.tar.gz

View File

@@ -2,6 +2,7 @@
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
# Contributing
- [Pull Requests](#pull-requests)
- [Requirements](#requirements)
- [Development Workflow](#development-workflow)
- [Build](#build)
@@ -12,6 +13,16 @@
- [Detailed CI and build process docs](../ci)
## Pull Requests
Please link to the issue each PR solves.
If there is no existing issue, please first create one unless the fix is minor.
Please make sure the base of your PR is the master branch. We keep the GitHub
default branch the latest release branch to avoid confusion as the
documentation is on GitHub and we don't want users to see docs on unreleased
features.
## Requirements
Please refer to [VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites).
@@ -19,9 +30,9 @@ Please refer to [VS Code's prerequisites](https://github.com/Microsoft/vscode/wi
Differences:
- We require a minimum of node v12 but later versions should work.
- We use [fnpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages.
- We use [nfpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages.
- We use [jq](https://stedolan.github.io/jq/) to build code-server releases.
- The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all our dependencies.
- The [CI container](../ci/images/debian10/Dockerfile) is a useful reference for all our dependencies.
## Development Workflow
@@ -35,40 +46,64 @@ yarn watch
To develop inside of an isolated docker container:
```shell
./ci/dev/image/exec.sh
root@12345:/code-server# yarn
root@12345:/code-server# yarn vscode
root@12345:/code-server# yarn watch
./ci/dev/image/run.sh yarn
./ci/dev/image/run.sh yarn vscode
./ci/dev/image/run.sh yarn watch
```
Any changes made to the source will be live reloaded.
`yarn watch` will live reload changes to the source.
If changes are made to the patch and you've built previously you must manually
reset VS Code then run `yarn vscode:patch`.
## Build
You can build with:
```shell
./ci/dev/image/run.sh ./ci/steps/release.sh
```
Run your build with:
```
cd release
yarn --production
# Runs the built JavaScript with Node.
node .
```
Build release packages (make sure you run `./ci/steps/release.sh` first):
```
IMAGE=centos7 ./ci/dev/image/run.sh ./ci/steps/release-packages.sh
# The standalone release is in ./release-standalone
# .deb, .rpm and the standalone archive are in ./release-packages
```
The `release.sh` script is the equivalent of:
```shell
yarn
yarn vscode
yarn build
yarn build:vscode
yarn release
cd release
yarn --production
# Runs the built JavaScript with Node.
node .
```
Now you can build release packages with:
And `release-packages.sh` is:
```
yarn release:standalone
# The standalone release is in ./release-standalone
yarn test:standalone-release
yarn package
# .deb, .rpm and the standalone archive are in ./release-packages
```
For a faster release build you can also run:
```
KEEP_MODULES=1 ./ci/steps/release.sh
node ./release
```
## Structure

View File

@@ -6,6 +6,7 @@
- [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration)
- [Differences compared to VS Code?](#differences-compared-to-vs-code)
- [How can I request a missing extension?](#how-can-i-request-a-missing-extension)
- [How do I configure the marketplace URL?](#how-do-i-configure-the-marketplace-url)
- [Where are extensions stored?](#where-are-extensions-stored)
- [How is this different from VS Code Codespaces?](#how-is-this-different-from-vs-code-codespaces)
- [How should I expose code-server to the internet?](#how-should-i-expose-code-server-to-the-internet)
@@ -18,25 +19,25 @@
- [How does code-server decide what workspace or folder to open?](#how-does-code-server-decide-what-workspace-or-folder-to-open)
- [How do I debug issues with code-server?](#how-do-i-debug-issues-with-code-server)
- [Heartbeat File](#heartbeat-file)
- [Healthz endpoint](#healthz-endpoint)
- [How does the config file work?](#how-does-the-config-file-work)
- [Blank screen on iPad?](#blank-screen-on-ipad)
- [Isn't an install script piped into sh insecure?](#isnt-an-install-script-piped-into-sh-insecure)
- [How do I make my keyboard shortcuts work?](#how-do-i-make-my-keyboard-shortcuts-work)
- [Differences compared to Theia?](#differences-compared-to-theia)
- [Enterprise](#enterprise)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Questions?
Please file all questions and support requests at https://www.reddit.com/r/codeserver/.
The issue tracker is **only** for bugs and features.
Please file all questions and support requests at https://github.com/cdr/code-server/discussions.
## How can I reuse my VS Code configuration?
The very popular [Settings Sync](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) extension works.
You can also pass `--data-dir ~/.vscode` to reuse your existing VS Code extensions and configuration.
You can also pass `--user-data-dir ~/.vscode` to reuse your existing VS Code extensions and configuration.
Or copy `~/.vscode` into `~/.local/share/code-server`.
@@ -78,8 +79,26 @@ point to the .vsix file.
See below for installing an extension from the cli.
If you have your own custom marketplace, it is possible to point code-server to it by setting
`$SERVICE_URL` and `$ITEM_URL` to point to it.
## How do I configure the marketplace URL?
If you have your own marketplace that implements the VS Code Extension Gallery API, it is possible to
point code-server to it by setting `$SERVICE_URL` and `$ITEM_URL`. These correspond directly
to `serviceUrl` and `itemUrl` in VS Code's `product.json`.
e.g. to use [open-vsx.org](https://open-vsx.org):
```bash
export SERVICE_URL=https://open-vsx.org/vscode/gallery
export ITEM_URL=https://open-vsx.org/vscode/item
```
While you can technically use Microsoft's marketplace with these, please do not do so as it
is against their terms of use. See [above](#differences-compared-to-vs-code) and this
discussion regarding the use of the Microsoft URLs in forks:
https://github.com/microsoft/vscode/issues/31168#issue-244533026
These variables are most valuable to our enterprise customers for whom we have a self hosted marketplace product.
## Where are extensions stored?
@@ -103,11 +122,11 @@ code-server --install-extension downloaded-ms-python.python.vsix
VS Code Codespaces is a closed source and paid service by Microsoft. It also allows you to access
VS Code via the browser.
However, code-server is free, open source and can be ran on any machine without any limitations.
However, code-server is free, open source and can be run on any machine without any limitations.
While you can self host environments with VS Code Codespaces, you still need to an Azure billing
account and you access VS Code via the Codespaces web dashboard instead of directly connecting to
your instance.
While you can self host environments with VS Code Codespaces, you still need an Azure billing
account and you have to access VS Code via the Codespaces web dashboard instead of directly
connecting to your instance.
## How should I expose code-server to the internet?
@@ -224,6 +243,20 @@ older than X minutes, kill `code-server`.
[#1636](https://github.com/cdr/code-server/issues/1636) will make the experience here better.
## Healthz endpoint
`code-server` exposes an endpoint at `/healthz` which can be used to check
whether `code-server` is up without triggering a heartbeat. The response will
include a status (`alive` or `expired`) and a timestamp for the last heartbeat
(defaults to `0`). This endpoint does not require authentication.
```json
{
"status": "alive",
"lastHeartbeat": 1599166210566
}
```
## How does the config file work?
When `code-server` starts up, it creates a default config file in `~/.config/code-server/config.yaml` that looks
@@ -250,7 +283,7 @@ The default location also respects `$XDG_CONFIG_HOME`.
Unfortunately at the moment self signed certificates cause a blank screen on iPadOS
There does seem to a way to get it to work if you create your own CA and create a
There does seem to be a way to get it to work if you create your own CA and create a
certificate using the CA and then import the CA onto your iPad.
See [#1566](https://github.com/cdr/code-server/issues/1566#issuecomment-623159434).
@@ -272,6 +305,19 @@ This will install a Chrome PWA and now all keybindings will work!
For other browsers you'll have to remap keybindings unfortunately.
## Differences compared to Theia?
[Theia](https://github.com/eclipse-theia/theia) is a browser IDE loosely based on VS Code. It uses the same
text editor library named [Monaco](https://github.com/Microsoft/monaco-editor) and the same
extension API but everything else is very different. It also uses [open-vsx.org](https://open-vsx.org)
for extensions which has an order of magnitude less extensions than our marketplace.
See [#1473](https://github.com/cdr/code-server/issues/1473).
You can't just use your VS Code config in Theia like you can with code-server.
To summarize, code-server is a patched fork of VS Code to run in the browser whereas
Theia takes some parts of VS Code but is an entirely different editor.
## Enterprise
Visit [our enterprise page](https://coder.com) for more information about our

View File

@@ -9,6 +9,7 @@
- [3. Expose code-server](#3-expose-code-server)
- [SSH forwarding](#ssh-forwarding)
- [Let's Encrypt](#lets-encrypt)
- [NGINX](#nginx)
- [Self Signed Certificate](#self-signed-certificate)
- [Change the password?](#change-the-password)
- [How do I securely access development web services?](#how-do-i-securely-access-development-web-services)
@@ -25,6 +26,8 @@ Further docs are at:
- [FAQ](./FAQ.md) for common questions.
- [CONTRIBUTING](../doc/CONTRIBUTING.md) for development docs
We highly recommend reading the [FAQ](./FAQ.md) on the [Differences compared to VS Code](./FAQ.md#differences-compared-to-vs-code) before beginning.
We'll walk you through acquiring a remote machine to run `code-server` on
and then exposing `code-server` so you can securely access it.
@@ -78,7 +81,7 @@ to avoid the slow dashboard.
## 2. Install code-server
We have a [script](../install.sh) to install `code-server` for Linux and macOS.
We have a [script](../install.sh) to install `code-server` for Linux, macOS and FreeBSD.
It tries to use the system package manager if possible.
@@ -128,16 +131,16 @@ sed -i.bak 's/auth: password/auth: none/' ~/.config/code-server/config.yaml
Restart `code-server` with (assuming you followed the guide):
```bash
systemctl --user restart code-server
sudo systemctl restart code-server@$USER
```
Now forward local port 8080 to `127.0.0.1:8080` on the remote instance.
Now forward local port 8080 to `127.0.0.1:8080` on the remote instance by running the following command on your local machine.
Recommended reading: https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding.
```bash
# -N disables executing a remote shell
ssh -N -L 8080:127.0.0.1:8080 <instance-ip>
ssh -N -L 8080:127.0.0.1:8080 [user]@<instance-ip>
```
Now if you access http://127.0.0.1:8080 locally, you should see `code-server`!
@@ -191,6 +194,8 @@ mydomain.com
reverse_proxy 127.0.0.1:8080
```
Remember to replace `mydomain.com` with your domain name!
5. Reload caddy with:
```bash
@@ -202,6 +207,48 @@ Visit `https://<your-domain-name>` to access `code-server`. Congratulations!
In a future release we plan to integrate Let's Encrypt directly with `code-server` to avoid
the dependency on caddy.
#### NGINX
If you prefer to use NGINX instead of Caddy then please follow steps 1-2 above and then:
3. Install `nginx`:
```bash
sudo apt update
sudo apt install -y nginx certbot python-certbot-nginx
```
4. Put the following config into `/etc/nginx/sites-available/code-server` with sudo:
```nginx
server {
listen 80;
listen [::]:80;
server_name mydomain.com;
location / {
proxy_pass http://localhost:8080/;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
proxy_set_header Accept-Encoding gzip;
}
}
```
Remember to replace `mydomain.com` with your domain name!
5. Enable the config:
```bash
sudo ln -s ../sites-available/code-server /etc/nginx/sites-enabled/code-server
sudo certbot --non-interactive --redirect --agree-tos --nginx -d mydomain.com -m me@example.com
```
Make sure to substitute `me@example.com` with your actual email.
Visit `https://<your-domain-name>` to access `code-server`. Congratulations!
### Self Signed Certificate
**note:** Self signed certificates do not work with iPad and will cause a blank page. You'll
@@ -230,7 +277,7 @@ sudo setcap cap_net_bind_service=+ep /usr/lib/code-server/lib/node
Assuming you have been following the guide, restart `code-server` with:
```bash
systemctl --user restart code-server
sudo systemctl restart code-server@$USER
```
Edit your instance and checkmark the allow HTTPS traffic option.
@@ -248,7 +295,7 @@ Edit the `password` field in the `code-server` config file at `~/.config/code-se
and then restart `code-server` with:
```bash
systemctl --user restart code-server
sudo systemctl restart code-server@$USER
```
### How do I securely access development web services?

View File

@@ -4,7 +4,7 @@
- [install.sh](#installsh)
- [Flags](#flags)
- [Detect Reference](#detect-reference)
- [Detection Reference](#detection-reference)
- [Debian, Ubuntu](#debian-ubuntu)
- [Fedora, CentOS, RHEL, SUSE](#fedora-centos-rhel-suse)
- [Arch Linux](#arch-linux)
@@ -20,7 +20,7 @@ various distros and operating systems.
## install.sh
We have a [script](../install.sh) to install code-server for Linux and macOS.
We have a [script](../install.sh) to install code-server for Linux, macOS and FreeBSD.
It tries to use the system package manager if possible.
@@ -42,7 +42,7 @@ If you believe an install script used with `curl | sh` is insecure, please give
[this wonderful blogpost](https://sandstorm.io/news/2015-09-24-is-curl-bash-insecure-pgp-verified-install) by
[sandstorm.io](https://sandstorm.io) a read.
If you'd still prefer manual installation despite the below [detect reference](#detect-reference) and `--dry-run`
If you'd still prefer manual installation despite the below [detection reference](#detection-reference) and `--dry-run`
then continue on for docs on manual installation. The [`install.sh`](../install.sh) script runs the _exact_ same
commands presented in the rest of this document.
@@ -56,7 +56,7 @@ commands presented in the rest of this document.
- `--version=X.X.X` to install version `X.X.X` instead of latest.
- `--help` to see full usage docs.
### Detect Reference
### Detection Reference
- For Debian, Ubuntu and Raspbian it will install the latest deb package.
- For Fedora, CentOS, RHEL and openSUSE it will install the latest rpm package.
@@ -70,6 +70,8 @@ commands presented in the rest of this document.
- If Homebrew is not installed it will install the latest standalone release into `~/.local`.
- Add `~/.local/bin` to your `$PATH` to run code-server.
- For FreeBSD, it will install the [npm package](#yarn-npm) with `yarn` or `npm`.
- If ran on an architecture with no releases, it will install the [npm package](#yarn-npm) with `yarn` or `npm`.
- We only have releases for amd64 and arm64 presently.
- The [npm package](#yarn-npm) builds the native modules on postinstall.
@@ -77,18 +79,18 @@ commands presented in the rest of this document.
## Debian, Ubuntu
```bash
curl -fOL https://github.com/cdr/code-server/releases/download/v3.4.1/code-server_3.4.1_amd64.deb
sudo dpkg -i code-server_3.4.1_amd64.deb
systemctl --user enable --now code-server
curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.0/code-server_3.6.0_amd64.deb
sudo dpkg -i code-server_3.6.0_amd64.deb
sudo systemctl enable --now code-server@$USER
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
```
## Fedora, CentOS, RHEL, SUSE
```bash
curl -fOL https://github.com/cdr/code-server/releases/download/v3.4.1/code-server-3.4.1-amd64.rpm
sudo rpm -i code-server-3.4.1-amd64.rpm
systemctl --user enable --now code-server
curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.0/code-server-3.6.0-amd64.rpm
sudo rpm -i code-server-3.6.0-amd64.rpm
sudo systemctl enable --now code-server@$USER
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
```
@@ -97,7 +99,7 @@ systemctl --user enable --now code-server
```bash
# Installs code-server from the AUR using yay.
yay -S code-server
systemctl --user enable --now code-server
sudo systemctl enable --now code-server@$USER
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
```
@@ -106,7 +108,7 @@ systemctl --user enable --now code-server
git clone https://aur.archlinux.org/code-server.git
cd code-server
makepkg -si
systemctl --user enable --now code-server
sudo systemctl enable --now code-server@$USER
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
```
@@ -156,10 +158,10 @@ Here is an example script for installing and using a standalone `code-server` re
```bash
mkdir -p ~/.local/lib ~/.local/bin
curl -fL https://github.com/cdr/code-server/releases/download/v3.4.1/code-server-3.4.1-linux-amd64.tar.gz \
curl -fL https://github.com/cdr/code-server/releases/download/v3.6.0/code-server-3.6.0-linux-amd64.tar.gz \
| tar -C ~/.local/lib -xz
mv ~/.local/lib/code-server-3.4.1-linux-amd64 ~/.local/lib/code-server-3.4.1
ln -s ~/.local/lib/code-server-3.4.1/bin/code-server ~/.local/bin/code-server
mv ~/.local/lib/code-server-3.6.0-linux-amd64 ~/.local/lib/code-server-3.6.0
ln -s ~/.local/lib/code-server-3.6.0/bin/code-server ~/.local/bin/code-server
PATH="~/.local/bin:$PATH"
code-server
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
@@ -172,9 +174,16 @@ code-server
# It will also mount your current directory into the container as `/home/coder/project`
# and forward your UID/GID so that all file system operations occur as your user outside
# the container.
docker run -it -p 127.0.0.1:8080:8080 \
#
# Your $HOME/.config is mounted at $HOME/.config within the container to ensure you can
# easily access/modify your code-server config in $HOME/.config/code-server/config.json
# outside the container.
mkdir -p ~/.config
docker run -it --name code-server -p 127.0.0.1:8080:8080 \
-v "$HOME/.config:/home/coder/.config" \
-v "$PWD:/home/coder/project" \
-u "$(id -u):$(id -g)" \
-e "DOCKER_USER=$USER" \
codercom/code-server:latest
```

View File

@@ -21,7 +21,9 @@ sudo apt-get install -y \
pkg-config \
libx11-dev \
libxkbfile-dev \
libsecret-1-dev
libsecret-1-dev \
python3
npm config set python python3
```
## Fedora, CentOS, RHEL

View File

@@ -14,24 +14,31 @@ usage() {
fi
cath << EOF
Installs code-server for Linux and macOS.
Installs code-server for Linux, macOS and FreeBSD.
It tries to use the system package manager if possible.
After successful installation it explains how to start using code-server.
Pass in user@host to install code-server on user@host over ssh.
The remote host must have internet access.
${not_curl_usage-}
Usage:
$arg0 [--dry-run] [--version X.X.X] [--method detect] [--prefix ~/.local]
$arg0 [--dry-run] [--version X.X.X] [--method detect] \
[--prefix ~/.local] [user@host]
--dry-run
Echo the commands for the install process without running them.
--version X.X.X
Install a specific version instead of the latest.
--method [detect | standalone]
Choose the installation method. Defaults to detect.
- detect detects the system package manager and tries to use it.
Full reference on the process is further below.
- standalone installs a standalone release archive into ~/.local
Add ~/.local/bin to your \$PATH to use it.
--prefix <dir>
Sets the prefix used by standalone release archives. Defaults to ~/.local
The release is unarchived into ~/.local/lib/code-server-X.X.X
@@ -48,6 +55,8 @@ Usage:
- If Homebrew is not installed it will install the latest standalone release
into ~/.local
- For FreeBSD, it will install the npm package with yarn or npm.
- If ran on an architecture with no releases, it will install the
npm package with yarn or npm.
- We only have releases for amd64 and arm64 presently.
@@ -82,7 +91,7 @@ echo_systemd_postinstall() {
echoh
cath << EOF
To have systemd start code-server now and restart on boot:
systemctl --user enable --now code-server
sudo systemctl enable --now code-server@\$USER
Or, if you don't want/need a background service you can run:
code-server
EOF
@@ -98,9 +107,18 @@ main() {
METHOD \
STANDALONE_INSTALL_PREFIX \
VERSION \
OPTIONAL
OPTIONAL \
ALL_FLAGS \
SSH_ARGS
ALL_FLAGS=""
while [ "$#" -gt 0 ]; do
case "$1" in
-*)
ALL_FLAGS="${ALL_FLAGS} $1"
;;
esac
case "$1" in
--dry-run)
DRY_RUN=1
@@ -130,16 +148,33 @@ main() {
usage
exit 0
;;
*)
--)
shift
# We remove the -- added above.
ALL_FLAGS="${ALL_FLAGS% --}"
SSH_ARGS="$*"
break
;;
-*)
echoerr "Unknown flag $1"
echoerr "Run with --help to see usage."
exit 1
;;
*)
SSH_ARGS="$*"
break
;;
esac
shift
done
if [ "${SSH_ARGS-}" ]; then
echoh "Installing remotely with ssh $SSH_ARGS"
curl -fsSL https://code-server.dev/install.sh | prefix "$SSH_ARGS" ssh "$SSH_ARGS" sh -s -- "$ALL_FLAGS"
return
fi
VERSION="${VERSION-$(echo_latest_version)}"
METHOD="${METHOD-detect}"
if [ "$METHOD" != detect ] && [ "$METHOD" != standalone ]; then
@@ -160,7 +195,7 @@ main() {
ARCH="$(arch)"
if [ ! "$ARCH" ]; then
if [ "$METHOD" = standalone ]; then
echoerr "No releases available for the architecture $(uname -m)."
echoerr "No precompiled releases for $(uname -m)."
echoerr 'Please rerun without the "--method standalone" flag to install from npm.'
exit 1
fi
@@ -169,8 +204,18 @@ main() {
return
fi
if [ "$OS" = "freebsd" ]; then
if [ "$METHOD" = standalone ]; then
echoerr "No precompiled releases available for $OS."
echoerr 'Please rerun without the "--method standalone" flag to install from npm.'
exit 1
fi
echoh "No precompiled releases available for $OS."
install_npm
return
fi
CACHE_DIR="$(echo_cache_dir)"
mkdir -p "$CACHE_DIR"
if [ "$METHOD" = standalone ]; then
install_standalone
@@ -234,10 +279,11 @@ fetch() {
FILE="$2"
if [ -e "$FILE" ]; then
echoh "+ Reusing $CACHE_DIR/${URL##*/}"
echoh "+ Reusing $FILE"
return
fi
sh_c mkdir -p "$CACHE_DIR"
sh_c curl \
-#fL \
-o "$FILE.incomplete" \
@@ -360,6 +406,9 @@ os() {
Darwin)
echo macos
;;
FreeBSD)
echo freebsd
;;
esac
}
@@ -371,11 +420,12 @@ os() {
# - centos, fedora, rhel, opensuse
# - alpine
# - arch
# - freebsd
#
# Inspired by https://github.com/docker/docker-install/blob/26ff363bcf3b3f5a00498ac43694bf1c7d9ce16c/install.sh#L111-L120.
distro() {
if [ "$(uname)" = "Darwin" ]; then
echo "macos"
if [ "$OS" = "macos" ] || [ "$OS" = "freebsd" ]; then
echo "$OS"
return
fi
@@ -422,11 +472,14 @@ arch() {
x86_64)
echo amd64
;;
amd64) # FreeBSD.
echo amd64
;;
esac
}
command_exists() {
command -v "$@" > /dev/null 2>&1
command -v "$@" > /dev/null
}
sh_c() {
@@ -480,4 +533,15 @@ humanpath() {
sed "s# $HOME# ~#g; s#\"$HOME#\"\$HOME#g"
}
# We need to make sure we exit with a non zero exit if the command fails.
# /bin/sh does not support -o pipefail unfortunately.
prefix() {
PREFIX="$1"
shift
fifo="$(mktemp -d)/fifo"
mkfifo "$fifo"
sed -e "s#^#$PREFIX: #" "$fifo" &
"$@" > "$fifo" 2>&1
}
main "$@"

View File

@@ -1,7 +1,7 @@
{
"name": "code-server",
"license": "MIT",
"version": "3.4.1",
"version": "3.6.0",
"description": "Run VS Code on a remote server.",
"homepage": "https://github.com/cdr/code-server",
"bugs": {
@@ -26,37 +26,38 @@
"lint": "./ci/dev/lint.sh",
"test": "./ci/dev/test.sh",
"ci": "./ci/dev/ci.sh",
"watch": "NODE_OPTIONS=--max_old_space_size=32384 ts-node ./ci/dev/watch.ts"
"watch": "VSCODE_IPC_HOOK_CLI= NODE_OPTIONS=--max_old_space_size=32384 ts-node ./ci/dev/watch.ts"
},
"main": "out/node/entry.js",
"devDependencies": {
"@types/fs-extra": "^8.0.1",
"@types/http-proxy": "^1.17.4",
"@types/js-yaml": "^3.12.3",
"@types/mocha": "^5.2.7",
"@types/mocha": "^8.0.3",
"@types/node": "^12.12.7",
"@types/parcel-bundler": "^1.12.1",
"@types/pem": "^1.9.5",
"@types/safe-compare": "^1.1.0",
"@types/semver": "^7.1.0",
"@types/tar-fs": "^1.16.2",
"@types/tar-stream": "^1.6.1",
"@types/ws": "^6.0.4",
"@typescript-eslint/eslint-plugin": "^2.0.0",
"@typescript-eslint/parser": "^2.0.0",
"@types/split2": "^2.1.6",
"@types/tar-fs": "^2.0.0",
"@types/tar-stream": "^2.1.0",
"@types/ws": "^7.2.6",
"@typescript-eslint/eslint-plugin": "^3.10.1",
"@typescript-eslint/parser": "^3.10.1",
"doctoc": "^1.4.0",
"eslint": "^6.2.0",
"eslint": "^7.7.0",
"eslint-config-prettier": "^6.0.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.0",
"leaked-handles": "^5.2.0",
"mocha": "^6.2.0",
"mocha": "^8.1.2",
"parcel-bundler": "^1.12.4",
"prettier": "^2.0.5",
"stylelint": "^13.0.0",
"stylelint-config-recommended": "^3.0.0",
"ts-node": "^8.4.1",
"typescript": "3.7.2"
"ts-node": "^9.0.0",
"typescript": "4.0.2"
},
"resolutions": {
"@types/node": "^12.12.7",
@@ -64,16 +65,19 @@
"vfile-message": "^2.0.2"
},
"dependencies": {
"@coder/logger": "1.1.11",
"@coder/logger": "1.1.16",
"env-paths": "^2.2.0",
"fs-extra": "^8.1.0",
"fs-extra": "^9.0.1",
"http-proxy": "^1.18.0",
"httpolyglot": "^0.1.2",
"js-yaml": "^3.13.1",
"limiter": "^1.1.5",
"pem": "^1.14.2",
"rotating-file-stream": "^2.1.1",
"safe-buffer": "^5.1.1",
"safe-compare": "^1.1.4",
"semver": "^7.1.3",
"split2": "^3.2.2",
"tar": "^6.0.1",
"tar-fs": "^2.0.0",
"ws": "^7.2.0",

View File

@@ -7,32 +7,32 @@
"description": "Run editors on a remote server.",
"icons": [
{
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-96.png",
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-96.png",
"type": "image/png",
"sizes": "96x96"
},
{
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-128.png",
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-128.png",
"type": "image/png",
"sizes": "128x128"
},
{
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-192.png",
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-256.png",
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-256.png",
"type": "image/png",
"sizes": "256x256"
},
{
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png",
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png",
"type": "image/png",
"sizes": "384x384"
},
{
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-512.png",
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png",
"type": "image/png",
"sizes": "512x512"
}

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
/>
<meta
http-equiv="Content-Security-Policy"
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
/>
<title>code-server</title>
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link
rel="manifest"
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" />
</head>
<body>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/pages/app.js"></script>
</body>
</html>

View File

@@ -1,37 +0,0 @@
import { getOptions, normalize } from "../../common/util"
import { ApiEndpoint } from "../../common/http"
import "./error.css"
import "./global.css"
import "./home.css"
import "./login.css"
import "./update.css"
const options = getOptions()
const isInput = (el: Element): el is HTMLInputElement => {
return !!(el as HTMLInputElement).name
}
document.querySelectorAll("form").forEach((form) => {
if (!form.classList.contains("-x11")) {
return
}
form.addEventListener("submit", (event) => {
event.preventDefault()
const values: { [key: string]: string } = {}
Array.from(form.elements).forEach((element) => {
if (isInput(element)) {
values[element.name] = element.value
}
})
fetch(normalize(`${options.base}/api/${ApiEndpoint.process}`), {
method: "POST",
body: JSON.stringify(values),
})
})
})
// TEMP: Until we can get the real ready event.
const event = new CustomEvent("ide-ready")
window.dispatchEvent(event)

View File

@@ -11,28 +11,22 @@
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
/>
<title>{{ERROR_TITLE}} - code-server</title>
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link
rel="manifest"
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" />
</head>
<body>
<div class="center-container">
<div class="error-display">
<h2 class="header">{{ERROR_HEADER}}</h2>
<div class="body">
{{ERROR_BODY}}
</div>
<div class="body">{{ERROR_BODY}}</div>
<div class="links">
<a class="link" href="{{BASE}}{{TO}}">go home</a>
</div>
</div>
</div>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
</body>
</html>

View File

@@ -1,51 +0,0 @@
.block-row {
display: flex;
}
.block-row > .item {
flex: 1;
margin: 2px 0;
}
.block-row > button.item {
background: none;
border: none;
cursor: pointer;
text-align: left;
}
.block-row > .item > .sub {
font-size: 0.95em;
}
.block-row .-link {
color: rgb(87, 114, 245);
display: block;
text-decoration: none;
}
.block-row .-link:hover {
text-decoration: underline;
}
.block-row > .item > .icon {
height: 1rem;
margin-right: 5px;
vertical-align: top;
width: 1rem;
}
.block-row > .item > .icon.-missing {
background-color: rgba(87, 114, 245, 0.2);
display: inline-block;
text-align: center;
}
.kill-form {
display: inline-block;
}
.kill-form > .kill {
border-radius: 3px;
padding: 2px 5px;
}

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
/>
<meta
http-equiv="Content-Security-Policy"
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
/>
<title>code-server</title>
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link
rel="manifest"
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" />
</head>
<body>
<div class="center-container">
<div class="card-box">
<div class="header">
<h2 class="main">Editors</h2>
<div class="sub">Choose an editor to launch below.</div>
</div>
<div class="content">
{{APP_LIST:EDITORS}}
</div>
</div>
<div class="card-box">
<div class="header">
<h2 class="main">Other</h2>
<div class="sub">Choose an application to launch below.</div>
</div>
<div class="content">
{{APP_LIST:OTHER}}
</div>
</div>
<div class="card-box">
<div class="header">
<h2 class="main">Version</h2>
<div class="sub">Version information and updates.</div>
</div>
<div class="content">
{{UPDATE:NAME}}
</div>
</div>
</div>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/pages/app.js"></script>
</body>
</html>

View File

@@ -11,14 +11,10 @@
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
/>
<title>code-server login</title>
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link
rel="manifest"
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" />
</head>
<body>
@@ -50,11 +46,6 @@
</div>
</div>
</body>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
<script>
const parts = window.location.pathname.replace(/^\//g, "").split("/")
parts[parts.length - 1] = "{{BASE}}"
const url = new URL(window.location.origin + "/" + parts.join("/"))
document.getElementById("base").value = url.pathname
</script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/pages/login.js"></script>
</html>

View File

@@ -0,0 +1,7 @@
import { getOptions } from "../../common/util"
const options = getOptions()
const el = document.getElementById("base") as HTMLInputElement
if (el) {
el.value = options.base
}

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
/>
<meta
http-equiv="Content-Security-Policy"
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
/>
<title>code-server</title>
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link
rel="manifest"
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" />
</head>
<body>
<div class="center-container">
<div class="card-box">
<div class="header">
<h1 class="main">Update</h1>
<div class="sub">Update code-server.</div>
</div>
<div class="content">
<form class="update-form" action="{{BASE}}/update/apply">
{{UPDATE_STATUS}} {{ERROR}}
<div class="links">
<a class="link" href="{{BASE}}{{TO}}">go home</a>
</div>
</form>
</div>
</div>
</div>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
</body>
</html>

View File

@@ -2,6 +2,11 @@
<!DOCTYPE html>
<html>
<head>
<script>
globalThis.MonacoPerformanceMarks = globalThis.MonacoPerformanceMarks || []
globalThis.MonacoPerformanceMarks.push("renderer/started", Date.now())
</script>
<meta charset="utf-8" />
<meta
@@ -24,21 +29,17 @@
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
<!-- Workbench Icon/Manifest/CSS -->
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link
rel="manifest"
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
<!-- PROD_ONLY
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/workbench/workbench.web.api.css">
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.css">
END_PROD_ONLY -->
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- Prefetch to avoid waterfall -->
<!-- PROD_ONLY
<link rel="prefetch" href="{{BASE}}/static/{{COMMIT}}/lib/vscode/node_modules/semver-umd/lib/semver-umd.js">
<link rel="prefetch" href="{{CS_STATIC_BASE}}/lib/vscode/node_modules/semver-umd/lib/semver-umd.js">
END_PROD_ONLY -->
<meta id="coder-options" data-settings="{{OPTIONS}}" />
@@ -47,64 +48,17 @@
<body aria-label=""></body>
<!-- Startup (do not modify order of script tags!) -->
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/pages/vscode.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/loader.js"></script>
<script>
const parts = window.location.pathname.replace(/^\//g, "").split("/")
parts[parts.length - 1] = "{{BASE}}"
const url = new URL(window.location.origin + "/" + parts.join("/"))
const staticBase = url.href.replace(/\/+$/, "") + "/static/{{COMMIT}}/lib/vscode"
let nlsConfig
try {
nlsConfig = JSON.parse(document.getElementById("vscode-remote-nls-configuration").getAttribute("data-settings"))
if (nlsConfig._resolvedLanguagePackCoreLocation) {
const bundles = Object.create(null)
nlsConfig.loadBundle = (bundle, language, cb) => {
let result = bundles[bundle]
if (result) {
return cb(undefined, result)
}
// FIXME: Only works if path separators are /.
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json"
fetch(`${url.href}/resource/?path=${encodeURIComponent(path)}`)
.then((response) => response.json())
.then((json) => {
bundles[bundle] = json
cb(undefined, json)
})
.catch(cb)
}
}
} catch (error) {
/* Probably fine. */
}
self.require = {
baseUrl: `${staticBase}/out`,
paths: {
"vscode-textmate": `${staticBase}/node_modules/vscode-textmate/release/main`,
"vscode-oniguruma": `${staticBase}/node_modules/vscode-oniguruma/release/main`,
xterm: `${staticBase}/node_modules/xterm/lib/xterm.js`,
"xterm-addon-search": `${staticBase}/node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
"xterm-addon-unicode11": `${staticBase}/node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
"xterm-addon-web-links": `${staticBase}/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`,
"xterm-addon-webgl": `${staticBase}/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
"semver-umd": `${staticBase}/node_modules/semver-umd/lib/semver-umd.js`,
},
"vs/nls": nlsConfig,
}
globalThis.MonacoPerformanceMarks.push("willLoadWorkbenchMain", Date.now())
</script>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/loader.js"></script>
<!-- PROD_ONLY
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/workbench/workbench.web.api.nls.js"></script>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/workbench/workbench.web.api.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.nls.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.js"></script>
END_PROD_ONLY -->
<script>
require(["vs/code/browser/workbench/workbench"], function () {})
</script>
<script>
try {
document.body.style.background = JSON.parse(localStorage.getItem("colorThemeData")).colorMap["editor.background"]
} catch (error) {
// Oh well.
}
</script>
</html>

View File

@@ -0,0 +1,54 @@
import { getOptions } from "../../common/util"
const options = getOptions()
// TODO: Add proper types.
/* eslint-disable @typescript-eslint/no-explicit-any */
let nlsConfig: any
try {
nlsConfig = JSON.parse(document.getElementById("vscode-remote-nls-configuration")!.getAttribute("data-settings")!)
if (nlsConfig._resolvedLanguagePackCoreLocation) {
const bundles = Object.create(null)
nlsConfig.loadBundle = (bundle: any, _language: any, cb: any): void => {
const result = bundles[bundle]
if (result) {
return cb(undefined, result)
}
// FIXME: Only works if path separators are /.
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json"
fetch(`${options.base}/vscode/resource/?path=${encodeURIComponent(path)}`)
.then((response) => response.json())
.then((json) => {
bundles[bundle] = json
cb(undefined, json)
})
.catch(cb)
}
}
} catch (error) {
/* Probably fine. */
}
;(self.require as any) = {
baseUrl: `${options.csStaticBase}/lib/vscode/out`,
recordStats: true,
paths: {
"vscode-textmate": `../node_modules/vscode-textmate/release/main`,
"vscode-oniguruma": `../node_modules/vscode-oniguruma/release/main`,
xterm: `../node_modules/xterm/lib/xterm.js`,
"xterm-addon-search": `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
"xterm-addon-unicode11": `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
"xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
"semver-umd": `../node_modules/semver-umd/lib/semver-umd.js`,
"iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
jschardet: `../node_modules/jschardet/dist/jschardet.min.js`,
},
"vs/nls": nlsConfig,
}
try {
document.body.style.background = JSON.parse(localStorage.getItem("colorThemeData")!).colorMap["editor.background"]
} catch (error) {
// Oh well.
}

View File

@@ -2,13 +2,17 @@ import { getOptions, normalize } from "../common/util"
const options = getOptions()
import "./pages/error.css"
import "./pages/global.css"
import "./pages/login.css"
if ("serviceWorker" in navigator) {
const path = normalize(`${options.base}/static/${options.commit}/dist/serviceWorker.js`)
const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`)
navigator.serviceWorker
.register(path, {
scope: options.base || "/",
scope: (options.base ?? "") + "/",
})
.then(function () {
.then(() => {
console.log("[Service Worker] registered")
})
}

2
src/browser/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -8,17 +8,6 @@ self.addEventListener("activate", (event: any) => {
event.waitUntil((self as any).clients.claim())
})
self.addEventListener("fetch", (event: any) => {
if (!navigator.onLine) {
event.respondWith(
new Promise((resolve) => {
resolve(
new Response("OFFLINE", {
status: 200,
statusText: "OK",
}),
)
}),
)
}
self.addEventListener("fetch", () => {
// Without this event handler we won't be recognized as a PWA.
})

View File

@@ -1,60 +0,0 @@
export interface Application {
readonly categories?: string[]
readonly comment?: string
readonly directory?: string
readonly exec?: string
readonly genericName?: string
readonly icon?: string
readonly installed?: boolean
readonly name: string
/**
* Path if this is a browser app (like VS Code).
*/
readonly path?: string
/**
* PID if this is a process.
*/
readonly pid?: number
readonly version?: string
}
export interface ApplicationsResponse {
readonly applications: ReadonlyArray<Application>
}
export enum SessionError {
FailedToStart = 4000,
Starting = 4001,
InvalidState = 4002,
Unknown = 4003,
}
export interface SessionResponse {
/**
* Whether the process was spawned or an existing one was returned.
*/
created: boolean
pid: number
}
export interface RecentResponse {
readonly paths: string[]
readonly workspaces: string[]
}
export interface HealthRequest {
readonly event: "health"
}
export type ClientMessage = HealthRequest
export interface HealthResponse {
readonly event: "health"
readonly connections: number
}
export type ServerMessage = HealthResponse
export interface ReadyMessage {
protocol: string
}

View File

@@ -1,19 +1,21 @@
import { Callback } from "./types"
export interface Disposable {
dispose(): void
}
export interface Event<T> {
(listener: (value: T) => void): Disposable
(listener: Callback<T>): Disposable
}
/**
* Emitter typecasts for a single event type.
*/
export class Emitter<T> {
private listeners: Array<(value: T) => void> = []
private listeners: Array<Callback<T>> = []
public get event(): Event<T> {
return (cb: (value: T) => void): Disposable => {
return (cb: Callback<T>): Disposable => {
this.listeners.push(cb)
return {

View File

@@ -9,16 +9,8 @@ export enum HttpCode {
}
export class HttpError extends Error {
public constructor(message: string, public readonly code: number) {
public constructor(message: string, public readonly code: number, public readonly details?: object) {
super(message)
this.name = this.constructor.name
}
}
export enum ApiEndpoint {
applications = "/applications",
process = "/process",
recent = "/recent",
run = "/run",
status = "/status",
}

1
src/common/types.ts Normal file
View File

@@ -0,0 +1 @@
export type Callback<T, R = void> = (t: T) => R

View File

@@ -2,9 +2,8 @@ import { logger, field } from "@coder/logger"
export interface Options {
base: string
commit: string
csStaticBase: string
logLevel: number
pid?: number
}
/**
@@ -16,7 +15,11 @@ export const split = (str: string, delimiter: string): [string, string] => {
return index !== -1 ? [str.substring(0, index).trim(), str.substring(index + 1)] : [str, ""]
}
export const plural = (count: number): string => (count === 1 ? "" : "s")
/**
* Appends an 's' to the provided string if count is greater than one;
* otherwise the string is returned
*/
export const plural = (count: number, str: string): string => (count === 1 ? str : `${str}s`)
export const generateUuid = (length = 24): string => {
const possible = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
@@ -33,21 +36,35 @@ export const normalize = (url: string, keepTrailing = false): string => {
return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "")
}
/**
* Remove leading and trailing slashes.
*/
export const trimSlashes = (url: string): string => {
return url.replace(/^\/+|\/+$/g, "")
}
/**
* Resolve a relative base against the window location. This is used for
* anything that doesn't work with a relative path.
*/
export const resolveBase = (base?: string): string => {
// After resolving the base will either start with / or be an empty string.
if (!base || base.startsWith("/")) {
return base ?? ""
}
const parts = location.pathname.split("/")
parts[parts.length - 1] = base
const url = new URL(location.origin + "/" + parts.join("/"))
return normalize(url.pathname)
}
/**
* Get options embedded in the HTML or query params.
*/
export const getOptions = <T extends Options>(): T => {
let options: T
try {
const el = document.getElementById("coder-options")
if (!el) {
throw new Error("no options element")
}
const value = el.getAttribute("data-settings")
if (!value) {
throw new Error("no options value")
}
options = JSON.parse(value)
options = JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!)
} catch (error) {
options = {} as T
}
@@ -61,17 +78,26 @@ export const getOptions = <T extends Options>(): T => {
}
}
if (typeof options.logLevel !== "undefined") {
logger.level = options.logLevel
}
if (options.base) {
const parts = location.pathname.replace(/^\//g, "").split("/")
parts[parts.length - 1] = options.base
const url = new URL(location.origin + "/" + parts.join("/"))
options.base = normalize(url.pathname, true)
}
logger.level = options.logLevel
options.base = resolveBase(options.base)
options.csStaticBase = resolveBase(options.csStaticBase)
logger.debug("got options", field("options", options))
return options
}
/**
* Wrap the value in an array if it's not already an array. If the value is
* undefined return an empty array.
*/
export const arrayify = <T>(value?: T | T[]): T[] => {
if (Array.isArray(value)) {
return value
}
if (typeof value === "undefined") {
return []
}
return [value]
}

View File

@@ -1,312 +0,0 @@
import { field, logger } from "@coder/logger"
import * as cp from "child_process"
import * as fs from "fs-extra"
import * as http from "http"
import * as net from "net"
import * as path from "path"
import * as url from "url"
import * as WebSocket from "ws"
import {
Application,
ApplicationsResponse,
ClientMessage,
RecentResponse,
ServerMessage,
SessionError,
SessionResponse,
} from "../../common/api"
import { ApiEndpoint, HttpCode, HttpError } from "../../common/http"
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http"
import { findApplications, findWhitelistedApplications, Vscode } from "./bin"
import { VscodeHttpProvider } from "./vscode"
interface VsRecents {
[key: string]: (string | { configURIPath: string })[]
}
type VsSettings = [string, string][]
/**
* API HTTP provider.
*/
export class ApiHttpProvider extends HttpProvider {
private readonly ws = new WebSocket.Server({ noServer: true })
public constructor(
options: HttpProviderOptions,
private readonly server: HttpServer,
private readonly vscode: VscodeHttpProvider,
private readonly dataDir?: string,
) {
super(options)
}
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
this.ensureAuthenticated(request)
if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound)
}
switch (route.base) {
case ApiEndpoint.applications:
this.ensureMethod(request)
return {
mime: "application/json",
content: {
applications: await this.applications(),
},
} as HttpResponse<ApplicationsResponse>
case ApiEndpoint.process:
return this.process(request)
case ApiEndpoint.recent:
this.ensureMethod(request)
return {
mime: "application/json",
content: await this.recent(),
} as HttpResponse<RecentResponse>
}
throw new HttpError("Not found", HttpCode.NotFound)
}
public async handleWebSocket(
route: Route,
request: http.IncomingMessage,
socket: net.Socket,
head: Buffer,
): Promise<void> {
if (!this.authenticated(request)) {
throw new Error("not authenticated")
}
switch (route.base) {
case ApiEndpoint.status:
return this.handleStatusSocket(request, socket, head)
case ApiEndpoint.run:
return this.handleRunSocket(route, request, socket, head)
}
throw new HttpError("Not found", HttpCode.NotFound)
}
private async handleStatusSocket(request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise<void> {
const getMessageResponse = async (event: "health"): Promise<ServerMessage> => {
switch (event) {
case "health":
return { event, connections: await this.server.getConnections() }
default:
throw new Error("unexpected message")
}
}
await new Promise<WebSocket>((resolve) => {
this.ws.handleUpgrade(request, socket, head, (ws) => {
const send = (event: ServerMessage): void => {
ws.send(JSON.stringify(event))
}
ws.on("message", (data) => {
logger.trace("got message", field("message", data))
try {
const message: ClientMessage = JSON.parse(data.toString())
getMessageResponse(message.event).then(send)
} catch (error) {
logger.error(error.message, field("message", data))
}
})
resolve()
})
})
}
/**
* A socket that connects to the process.
*/
private async handleRunSocket(
_route: Route,
request: http.IncomingMessage,
socket: net.Socket,
head: Buffer,
): Promise<void> {
logger.debug("connecting to process")
const ws = await new Promise<WebSocket>((resolve, reject) => {
this.ws.handleUpgrade(request, socket, head, (socket) => {
socket.binaryType = "arraybuffer"
socket.on("error", (error) => {
socket.close(SessionError.FailedToStart)
logger.error("got error while connecting socket", field("error", error))
reject(error)
})
resolve(socket as WebSocket)
})
})
logger.debug("connected to process")
// Send ready message.
ws.send(
Buffer.from(
JSON.stringify({
protocol: "TODO",
}),
),
)
}
/**
* Return whitelisted applications.
*/
public async applications(): Promise<ReadonlyArray<Application>> {
return findWhitelistedApplications()
}
/**
* Return installed applications.
*/
public async installedApplications(): Promise<ReadonlyArray<Application>> {
return findApplications()
}
/**
* Handle /process endpoint.
*/
private async process(request: http.IncomingMessage): Promise<HttpResponse> {
this.ensureMethod(request, ["DELETE", "POST"])
const data = await this.getData(request)
if (!data) {
throw new HttpError("No data was provided", HttpCode.BadRequest)
}
const parsed: Application = JSON.parse(data)
switch (request.method) {
case "DELETE":
if (parsed.pid) {
await this.killProcess(parsed.pid)
} else if (parsed.path) {
await this.killProcess(parsed.path)
} else {
throw new Error("No pid or path was provided")
}
return {
mime: "application/json",
code: HttpCode.Ok,
}
case "POST": {
if (!parsed.exec) {
throw new Error("No exec was provided")
}
return {
mime: "application/json",
content: {
created: true,
pid: await this.spawnProcess(parsed.exec),
},
} as HttpResponse<SessionResponse>
}
}
throw new HttpError("Not found", HttpCode.NotFound)
}
/**
* Kill a process identified by pid or path if a web app.
*/
public async killProcess(pid: number | string): Promise<void> {
if (typeof pid === "string") {
switch (pid) {
case Vscode.path:
await this.vscode.dispose()
break
default:
throw new Error(`Process "${pid}" does not exist`)
}
} else {
process.kill(pid)
}
}
/**
* Spawn a process and return the pid.
*/
public async spawnProcess(exec: string): Promise<number> {
const proc = cp.spawn(exec, {
shell: process.env.SHELL || true,
env: {
...process.env,
},
})
proc.on("error", (error) => logger.error("process errored", field("pid", proc.pid), field("error", error)))
proc.on("exit", () => logger.debug("process exited", field("pid", proc.pid)))
logger.debug("started process", field("pid", proc.pid))
return proc.pid
}
/**
* Return VS Code's recent paths.
*/
public async recent(): Promise<RecentResponse> {
try {
if (!this.dataDir) {
throw new Error("data directory is not set")
}
const state: VsSettings = JSON.parse(await fs.readFile(path.join(this.dataDir, "User/state/global.json"), "utf8"))
const setting = Array.isArray(state) && state.find((item) => item[0] === "recently.opened")
if (!setting) {
return { paths: [], workspaces: [] }
}
const pathPromises: { [key: string]: Promise<string> } = {}
const workspacePromises: { [key: string]: Promise<string> } = {}
Object.values(JSON.parse(setting[1]) as VsRecents).forEach((recents) => {
recents.forEach((recent) => {
try {
const target = typeof recent === "string" ? pathPromises : workspacePromises
const pathname = url.parse(typeof recent === "string" ? recent : recent.configURIPath).pathname
if (pathname && !target[pathname]) {
target[pathname] = new Promise<string>((resolve) => {
fs.stat(pathname)
.then(() => resolve(pathname))
.catch(() => resolve())
})
}
} catch (error) {
logger.debug("invalid path", field("path", recent))
}
})
})
const [paths, workspaces] = await Promise.all([
Promise.all(Object.values(pathPromises)),
Promise.all(Object.values(workspacePromises)),
])
return {
paths: paths.filter((p) => !!p),
workspaces: workspaces.filter((p) => !!p),
}
} catch (error) {
if (error.code !== "ENOENT") {
throw error
}
}
return { paths: [], workspaces: [] }
}
/**
* For these, just return the error message since they'll be requested as
* JSON.
*/
public async getErrorRoot(_route: Route, _title: string, _header: string, error: string): Promise<HttpResponse> {
return {
mime: "application/json",
content: JSON.stringify({ error }),
}
}
}

View File

@@ -1,30 +0,0 @@
import * as fs from "fs"
import * as path from "path"
import { Application } from "../../common/api"
const getVscodeVersion = (): string => {
try {
return require(path.resolve(__dirname, "../../../lib/vscode/package.json")).version
} catch (error) {
return "unknown"
}
}
export const Vscode: Application = {
categories: ["Editor"],
icon: fs.readFileSync(path.resolve(__dirname, "../../../lib/vscode/resources/linux/code.png")).toString("base64"),
installed: true,
name: "VS Code",
path: "/",
version: getVscodeVersion(),
}
export const findApplications = async (): Promise<ReadonlyArray<Application>> => {
const apps: Application[] = [Vscode]
return apps.sort((a, b): number => a.name.localeCompare(b.name))
}
export const findWhitelistedApplications = async (): Promise<ReadonlyArray<Application>> => {
return [Vscode]
}

View File

@@ -1,147 +0,0 @@
import * as http from "http"
import * as querystring from "querystring"
import { Application } from "../../common/api"
import { HttpCode, HttpError } from "../../common/http"
import { normalize } from "../../common/util"
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
import { ApiHttpProvider } from "./api"
import { UpdateHttpProvider } from "./update"
/**
* Dashboard HTTP provider.
*/
export class DashboardHttpProvider extends HttpProvider {
public constructor(
options: HttpProviderOptions,
private readonly api: ApiHttpProvider,
private readonly update: UpdateHttpProvider,
) {
super(options)
}
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound)
}
switch (route.base) {
case "/spawn": {
this.ensureAuthenticated(request)
this.ensureMethod(request, "POST")
const data = await this.getData(request)
const app = data ? querystring.parse(data) : {}
if (app.path) {
return { redirect: Array.isArray(app.path) ? app.path[0] : app.path }
}
if (!app.exec) {
throw new Error("No exec was provided")
}
this.api.spawnProcess(Array.isArray(app.exec) ? app.exec[0] : app.exec)
return { redirect: this.options.base }
}
case "/app":
case "/": {
this.ensureMethod(request)
if (!this.authenticated(request)) {
return { redirect: "/login", query: { to: this.options.base } }
}
return route.base === "/" ? this.getRoot(route) : this.getAppRoot(route)
}
}
throw new HttpError("Not found", HttpCode.NotFound)
}
public async getRoot(route: Route): Promise<HttpResponse> {
const base = this.base(route)
const apps = await this.api.installedApplications()
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/home.html")
response.content = response.content
.replace(/{{UPDATE:NAME}}/, await this.getUpdate(base))
.replace(
/{{APP_LIST:EDITORS}}/,
this.getAppRows(
base,
apps.filter((app) => app.categories && app.categories.includes("Editor")),
),
)
.replace(
/{{APP_LIST:OTHER}}/,
this.getAppRows(
base,
apps.filter((app) => !app.categories || !app.categories.includes("Editor")),
),
)
return this.replaceTemplates(route, response)
}
public async getAppRoot(route: Route): Promise<HttpResponse> {
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/app.html")
return this.replaceTemplates(route, response)
}
private getAppRows(base: string, apps: ReadonlyArray<Application>): string {
return apps.length > 0
? apps.map((app) => this.getAppRow(base, app)).join("\n")
: `<div class="none">No applications found.</div>`
}
private getAppRow(base: string, app: Application): string {
return `<form class="block-row${app.exec ? " -x11" : ""}" method="post" action="${normalize(
`${base}${this.options.base}/spawn`,
)}">
<button class="item -row -link">
<input type="hidden" name="path" value="${app.path || ""}">
<input type="hidden" name="exec" value="${app.exec || ""}">
${
app.icon
? `<img class="icon" src="data:image/png;base64,${app.icon}"></img>`
: `<span class="icon -missing"></span>`
}
<span class="name">${app.name}</span>
</button>
</form>`
}
private async getUpdate(base: string): Promise<string> {
if (!this.update.enabled) {
return `<div class="block-row"><div class="item"><div class="sub">Updates are disabled</div></div></div>`
}
const humanize = (time: number): string => {
const d = new Date(time)
const pad = (t: number): string => (t < 10 ? "0" : "") + t
return (
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
` ${pad(d.getHours())}:${pad(d.getMinutes())}`
)
}
const update = await this.update.getUpdate()
if (this.update.isLatestVersion(update)) {
return `<div class="block-row">
<div class="item">
Latest: ${update.version}
<div class="sub">Up to date</div>
</div>
<div class="item">
${humanize(update.checked)}
<a class="sub -link" href="${base}/update/check?to=${this.options.base}">Check now</a>
</div>
<div class="item" >Current: ${this.update.currentVersion}</div>
</div>`
}
return `<div class="block-row">
<div class="item">
Latest: ${update.version}
<div class="sub">Out of date</div>
</div>
<div class="item">
${humanize(update.checked)}
<a class="sub -link" href="${base}/update?to=${this.options.base}">Update now</a>
</div>
<div class="item" >Current: ${this.update.currentVersion}</div>
</div>`
}
}

21
src/node/app/health.ts Normal file
View File

@@ -0,0 +1,21 @@
import { HttpProvider, HttpResponse, Heart, HttpProviderOptions } from "../http"
/**
* Check the heartbeat.
*/
export class HealthHttpProvider extends HttpProvider {
public constructor(options: HttpProviderOptions, private readonly heart: Heart) {
super(options)
}
public async handleRequest(): Promise<HttpResponse> {
return {
cache: false,
mime: "application/json",
content: {
status: this.heart.alive() ? "alive" : "expired",
lastHeartbeat: this.heart.lastHeartbeat,
},
}
}
}

View File

@@ -24,7 +24,7 @@ export class ProxyHttpProvider extends HttpProvider {
const port = route.base.replace(/^\//, "")
return {
proxy: {
base: `${this.options.base}/${port}`,
strip: `${route.providerBase}/${port}`,
port,
},
}
@@ -35,7 +35,7 @@ export class ProxyHttpProvider extends HttpProvider {
const port = route.base.replace(/^\//, "")
return {
proxy: {
base: `${this.options.base}/${port}`,
strip: `${route.providerBase}/${port}`,
port,
},
}

View File

@@ -8,10 +8,9 @@ import { HttpProvider, HttpResponse, Route } from "../http"
import { pathToFsPath } from "../util"
/**
* Static file HTTP provider. Regular static requests (the path is the request
* itself) do not require authentication and they only allow access to resources
* within the application. Requests for tars (the path is in a query parameter)
* do require permissions and can access any directory.
* Static file HTTP provider. Static requests do not require authentication if
* the resource is in the application's directory except requests to serve a
* directory as a tar which always requires authentication.
*/
export class StaticHttpProvider extends HttpProvider {
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
@@ -22,7 +21,7 @@ export class StaticHttpProvider extends HttpProvider {
return this.getTarredResource(request, pathToFsPath(route.query.tar))
}
const response = await this.getReplacedResource(route)
const response = await this.getReplacedResource(request, route)
if (!this.isDev) {
response.cache = true
}
@@ -32,17 +31,25 @@ export class StaticHttpProvider extends HttpProvider {
/**
* Return a resource with variables replaced where necessary.
*/
protected async getReplacedResource(route: Route): Promise<HttpResponse> {
protected async getReplacedResource(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
// The first part is always the commit (for caching purposes).
const split = route.requestPath.split("/").slice(1)
const resourcePath = path.resolve("/", ...split)
// Make sure it's in code-server or a plugin.
const validPaths = [this.rootPath, process.env.PLUGIN_DIR]
if (!validPaths.find((p) => p && resourcePath.startsWith(p))) {
this.ensureAuthenticated(request)
}
switch (split[split.length - 1]) {
case "manifest.json": {
const response = await this.getUtf8Resource(this.rootPath, ...split)
const response = await this.getUtf8Resource(resourcePath)
return this.replaceTemplates(route, response)
}
}
return this.getResource(this.rootPath, ...split)
return this.getResource(resourcePath)
}
/**

View File

@@ -1,21 +1,12 @@
import { field, logger } from "@coder/logger"
import * as cp from "child_process"
import * as fs from "fs-extra"
import * as http from "http"
import * as https from "https"
import * as os from "os"
import * as path from "path"
import * as semver from "semver"
import { Readable, Writable } from "stream"
import * as tar from "tar-fs"
import * as url from "url"
import * as util from "util"
import * as zlib from "zlib"
import { HttpCode, HttpError } from "../../common/http"
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "../settings"
import { tmpdir } from "../util"
import { ipcMain } from "../wrapper"
export interface Update {
checked: number
@@ -27,7 +18,7 @@ export interface LatestResponse {
}
/**
* Update HTTP provider.
* HTTP provider for checking updates (does not download/install them).
*/
export class UpdateHttpProvider extends HttpProvider {
private update?: Promise<Update>
@@ -41,12 +32,6 @@ export class UpdateHttpProvider extends HttpProvider {
* that fulfills `LatestResponse`.
*/
private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest",
/**
* The URL for downloading a version of code-server. {{VERSION}} and
* {{RELEASE_NAME}} will be replaced (for example 2.1.0 and
* code-server-2.1.0-linux-x86_64.tar.gz).
*/
private readonly downloadUrl = "https://github.com/cdr/code-server/releases/download/{{VERSION}}/{{RELEASE_NAME}}",
/**
* Update information will be stored here. If not provided, the global
* settings will be used.
@@ -64,66 +49,30 @@ export class UpdateHttpProvider extends HttpProvider {
throw new HttpError("Not found", HttpCode.NotFound)
}
switch (route.base) {
case "/check":
this.getUpdate(true)
if (route.query && route.query.to) {
return {
redirect: Array.isArray(route.query.to) ? route.query.to[0] : route.query.to,
query: { to: undefined },
}
}
return this.getRoot(route, request)
case "/apply":
return this.tryUpdate(route, request)
case "/":
return this.getRoot(route, request)
if (!this.enabled) {
throw new Error("update checks are disabled")
}
throw new HttpError("Not found", HttpCode.NotFound)
}
public async getRoot(
route: Route,
request: http.IncomingMessage,
errorOrUpdate?: Update | Error,
): Promise<HttpResponse> {
if (request.headers["content-type"] === "application/json") {
if (!this.enabled) {
switch (route.base) {
case "/check":
case "/": {
const update = await this.getUpdate(route.base === "/check")
return {
content: {
isLatest: true,
...update,
isLatest: this.isLatestVersion(update),
},
}
}
const update = await this.getUpdate()
return {
content: {
...update,
isLatest: this.isLatestVersion(update),
},
}
}
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/update.html")
response.content = response.content
.replace(
/{{UPDATE_STATUS}}/,
errorOrUpdate && !(errorOrUpdate instanceof Error)
? `Updated to ${errorOrUpdate.version}`
: await this.getUpdateHtml(),
)
.replace(/{{ERROR}}/, errorOrUpdate instanceof Error ? `<div class="error">${errorOrUpdate.message}</div>` : "")
return this.replaceTemplates(route, response)
throw new HttpError("Not found", HttpCode.NotFound)
}
/**
* Query for and return the latest update.
*/
public async getUpdate(force?: boolean): Promise<Update> {
if (!this.enabled) {
throw new Error("updates are not enabled")
}
// Don't run multiple requests at a time.
if (!this.update) {
this.update = this._getUpdate(force)
@@ -171,128 +120,6 @@ export class UpdateHttpProvider extends HttpProvider {
}
}
private async getUpdateHtml(): Promise<string> {
if (!this.enabled) {
return "Updates are disabled"
}
const update = await this.getUpdate()
if (this.isLatestVersion(update)) {
return "No update available"
}
return `<button type="submit" class="apply -button">Update to ${update.version}</button>`
}
public async tryUpdate(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
try {
const update = await this.getUpdate()
if (!this.isLatestVersion(update)) {
await this.downloadAndApplyUpdate(update)
return this.getRoot(route, request, update)
}
return this.getRoot(route, request)
} catch (error) {
// For JSON requests propagate the error. Otherwise catch it so we can
// show the error inline with the update button instead of an error page.
if (request.headers["content-type"] === "application/json") {
throw error
}
return this.getRoot(route, error)
}
}
public async downloadAndApplyUpdate(update: Update, targetPath?: string): Promise<void> {
const releaseName = await this.getReleaseName(update)
const url = this.downloadUrl.replace("{{VERSION}}", update.version).replace("{{RELEASE_NAME}}", releaseName)
let downloadPath = path.join(tmpdir, "updates", releaseName)
fs.mkdirp(path.dirname(downloadPath))
const response = await this.requestResponse(url)
try {
downloadPath = await this.extractTar(response, downloadPath)
logger.debug("Downloaded update", field("path", downloadPath))
// The archive should have a directory inside at the top level with the
// same name as the archive.
const directoryPath = path.join(downloadPath, path.basename(downloadPath))
await fs.stat(directoryPath)
if (!targetPath) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
targetPath = path.resolve(__dirname, "../../../")
}
// Move the old directory to prevent potential data loss.
const backupPath = path.resolve(targetPath, `../${path.basename(targetPath)}.${Date.now().toString()}`)
logger.debug("Replacing files", field("target", targetPath), field("backup", backupPath))
await fs.move(targetPath, backupPath)
// Move the new directory.
await fs.move(directoryPath, targetPath)
await fs.remove(downloadPath)
if (process.send) {
ipcMain().relaunch(update.version)
}
} catch (error) {
response.destroy(error)
throw error
}
}
private async extractTar(response: Readable, downloadPath: string): Promise<string> {
downloadPath = downloadPath.replace(/\.tar\.gz$/, "")
logger.debug("Extracting tar", field("path", downloadPath))
response.pause()
await fs.remove(downloadPath)
const decompress = zlib.createGunzip()
response.pipe(decompress as Writable)
response.on("error", (error) => decompress.destroy(error))
response.on("close", () => decompress.end())
const destination = tar.extract(downloadPath)
decompress.pipe(destination)
decompress.on("error", (error) => destination.destroy(error))
decompress.on("close", () => destination.end())
await new Promise((resolve, reject) => {
destination.on("finish", resolve)
destination.on("error", reject)
response.resume()
})
return downloadPath
}
/**
* Given an update return the name for the packaged archived.
*/
public async getReleaseName(update: Update): Promise<string> {
let target: string = os.platform()
if (target === "linux") {
const result = await util
.promisify(cp.exec)("ldd --version")
.catch((error) => ({
stderr: error.message,
stdout: "",
}))
if (/musl/.test(result.stderr) || /musl/.test(result.stdout)) {
target = "alpine"
}
}
let arch = os.arch()
if (arch === "x64") {
arch = "x86_64"
}
return `code-server-${update.version}-${target}-${arch}.tar.gz`
}
private async request(uri: string): Promise<Buffer> {
const response = await this.requestResponse(uri)
return new Promise((resolve, reject) => {

View File

@@ -14,7 +14,7 @@ import {
WorkbenchOptions,
} from "../../../lib/vscode/src/vs/server/ipc"
import { HttpCode, HttpError } from "../../common/http"
import { generateUuid } from "../../common/util"
import { arrayify, generateUuid } from "../../common/util"
import { Args } from "../cli"
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
import { settings } from "../settings"
@@ -131,7 +131,7 @@ export class VscodeHttpProvider extends HttpProvider {
if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound)
} else if (!this.authenticated(request)) {
return { redirect: "/login", query: { to: this.options.base } }
return { redirect: "/login", query: { to: route.providerBase } }
}
try {
return await this.getRoot(request, route)
@@ -183,11 +183,10 @@ export class VscodeHttpProvider extends HttpProvider {
}),
])
if (startPath) {
settings.write({
lastVisited: startPath,
})
}
settings.write({
lastVisited: startPath || lastVisited, // If startpath is undefined, then fallback to lastVisited
query: route.query,
})
if (!this.isDev) {
response.content = response.content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "")
@@ -201,8 +200,6 @@ export class VscodeHttpProvider extends HttpProvider {
.replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`)
.replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`)
return this.replaceTemplates<Options>(route, response, {
base: this.base(route),
commit: this.options.commit,
disableTelemetry: !!this.args["disable-telemetry"],
})
}
@@ -224,8 +221,7 @@ export class VscodeHttpProvider extends HttpProvider {
}
for (let i = 0; i < startPaths.length; ++i) {
const startPath = startPaths[i]
const url =
startPath && (typeof startPath.url === "string" ? [startPath.url] : startPath.url || []).find((p) => !!p)
const url = arrayify(startPath && startPath.url).find((p) => !!p)
if (startPath && url) {
return {
url,

View File

@@ -5,7 +5,7 @@ import * as os from "os"
import * as path from "path"
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
import { AuthType } from "./http"
import { generatePassword, humanPath, paths } from "./util"
import { canConnect, generatePassword, humanPath, paths } from "./util"
export class Optional<T> {
public constructor(public readonly value?: T) {}
@@ -45,6 +45,10 @@ export interface Args extends VsArgs {
readonly "proxy-domain"?: string[]
readonly locale?: string
readonly _: string[]
readonly "reuse-window"?: boolean
readonly "new-window"?: boolean
readonly link?: OptionalString
}
interface Option<T> {
@@ -61,6 +65,11 @@ interface Option<T> {
* Description of the option. Leave blank to hide the option.
*/
description?: string
/**
* If marked as beta, the option is not printed unless $CS_BETA is set.
*/
beta?: boolean
}
type OptionType<T> = T extends boolean
@@ -125,14 +134,47 @@ const options: Options<Required<Args>> = {
"extra-builtin-extensions-dir": { type: "string[]", path: true },
"list-extensions": { type: "boolean", description: "List installed VS Code extensions." },
force: { type: "boolean", description: "Avoid prompts when installing VS Code extensions." },
"install-extension": { type: "string[]", description: "Install or update a VS Code extension by id or vsix." },
"install-extension": {
type: "string[]",
description:
"Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`.\n" +
"To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.",
},
"enable-proposed-api": {
type: "string[]",
description:
"Enable proposed API features for extensions. Can receive one or more extension IDs to enable individually.",
},
"uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." },
"show-versions": { type: "boolean", description: "Show VS Code extension versions." },
"proxy-domain": { type: "string[]", description: "Domain used for proxying ports." },
"new-window": {
type: "boolean",
short: "n",
description: "Force to open a new window.",
},
"reuse-window": {
type: "boolean",
short: "r",
description: "Force to open a file or folder in an already opened window.",
},
locale: { type: "string" },
log: { type: LogLevel },
verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." },
link: {
type: OptionalString,
description: `
Securely bind code-server via Coder Cloud with the passed name. You'll get a URL like
https://myname.coder-cloud.com at which you can easily access your code-server instance.
Authorization is done via GitHub.
This is presently beta and requires being accepted for testing.
See https://github.com/cdr/code-server/discussions/2137
`,
beta: true,
},
}
export const optionDescriptions = (): string[] => {
@@ -144,12 +186,32 @@ export const optionDescriptions = (): string[] => {
}),
{ short: 0, long: 0 },
)
return entries.map(
([k, v]) =>
`${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat(
widths.long - k.length,
)} ${v.description}${typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : ""}`,
)
return entries
.filter(([, v]) => {
// If CS_BETA is set, we show beta options but if not, then we do not want
// to show beta options.
return process.env.CS_BETA || !v.beta
})
.map(([k, v]) => {
const help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${
v.short ? `-${v.short}` : " "
} --${k} `
return (
help +
v.description
?.trim()
.split(/\n/)
.map((line, i) => {
line = line.trim()
if (i === 0) {
return " ".repeat(widths.long - k.length) + line
}
return " ".repeat(widths.long + widths.short + 6) + line
})
.join("\n") +
(typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : "")
)
})
}
export const parse = (
@@ -172,7 +234,7 @@ export const parse = (
const arg = argv[i]
// -- signals the end of option parsing.
if (!ended && arg == "--") {
if (!ended && arg === "--") {
ended = true
continue
}
@@ -220,7 +282,7 @@ export const parse = (
throw error(`--${key} requires a value`)
}
if (option.type == OptionalString && value == "false") {
if (option.type === OptionalString && value === "false") {
continue
}
@@ -265,6 +327,21 @@ export const parse = (
logger.debug("parsed command line", field("args", args))
return args
}
export async function setDefaults(args: Args): Promise<Args> {
args = { ...args }
if (!args["user-data-dir"]) {
await copyOldMacOSDataDir()
args["user-data-dir"] = paths.data
}
if (!args["extensions-dir"]) {
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
}
// --verbose takes priority over --log and --log takes priority over the
// environment variable.
if (args.verbose) {
@@ -307,21 +384,6 @@ export const parse = (
return args
}
export async function setDefaults(args: Args): Promise<Args> {
args = { ...args }
if (!args["user-data-dir"]) {
await copyOldMacOSDataDir()
args["user-data-dir"] = paths.data
}
if (!args["extensions-dir"]) {
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
}
return args
}
async function defaultConfigFile(): Promise<string> {
return `bind-addr: 127.0.0.1:8080
auth: password
@@ -348,14 +410,13 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
logger.info(`Wrote default config file to ${humanPath(configPath)}`)
}
if (!process.env.CODE_SERVER_PARENT_PID) {
logger.info(`Using config file ${humanPath(configPath)}`)
}
const configFile = await fs.readFile(configPath)
const config = yaml.safeLoad(configFile.toString(), {
filename: configPath,
})
if (!config || typeof config === "string") {
throw new Error(`invalid config: ${config}`)
}
// We convert the config file into a set of flags.
// This is a temporary measure until we add a proper CLI library.
@@ -376,7 +437,10 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
function parseBindAddr(bindAddr: string): [string, number] {
const u = new URL(`http://${bindAddr}`)
return [u.hostname, parseInt(u.port, 10)]
// With the http scheme 80 will be dropped so assume it's 80 if missing. This
// means --bind-addr <addr> without a port will default to 80 as well and not
// the code-server default.
return [u.hostname, u.port ? parseInt(u.port, 10) : 80]
}
interface Addr {
@@ -428,3 +492,52 @@ async function copyOldMacOSDataDir(): Promise<void> {
await fs.copy(oldDataDir, paths.data)
}
}
export const shouldRunVsCodeCli = (args: Args): boolean => {
return !!args["list-extensions"] || !!args["install-extension"] || !!args["uninstall-extension"]
}
/**
* Determine if it looks like the user is trying to open a file or folder in an
* existing instance. The arguments here should be the arguments the user
* explicitly passed on the command line, not defaults or the configuration.
*/
export const shouldOpenInExistingInstance = async (args: Args): Promise<string | undefined> => {
// Always use the existing instance if we're running from VS Code's terminal.
if (process.env.VSCODE_IPC_HOOK_CLI) {
return process.env.VSCODE_IPC_HOOK_CLI
}
const readSocketPath = async (): Promise<string | undefined> => {
try {
return await fs.readFile(path.join(os.tmpdir(), "vscode-ipc"), "utf8")
} catch (error) {
if (error.code !== "ENOENT") {
throw error
}
}
return undefined
}
// If these flags are set then assume the user is trying to open in an
// existing instance since these flags have no effect otherwise.
const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => {
return args[cur as keyof Args] ? prev + 1 : prev
}, 0)
if (openInFlagCount > 0) {
return readSocketPath()
}
// It's possible the user is trying to spawn another instance of code-server.
// Check if any unrelated flags are set (check against one because `_` always
// exists), that a file or directory was passed, and that the socket is
// active.
if (Object.keys(args).length === 1 && args._.length > 0) {
const socketPath = await readSocketPath()
if (socketPath && (await canConnect(socketPath))) {
return socketPath
}
}
return undefined
}

43
src/node/coder-cloud.ts Normal file
View File

@@ -0,0 +1,43 @@
import { logger } from "@coder/logger"
import { spawn } from "child_process"
import path from "path"
import split2 from "split2"
// https://github.com/cdr/coder-cloud
const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent")
function runAgent(...args: string[]): Promise<void> {
logger.debug(`running agent with ${args}`)
const agent = spawn(coderCloudAgent, args, {
stdio: ["inherit", "inherit", "pipe"],
})
agent.stderr.pipe(split2()).on("data", (line) => {
line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "")
logger.info(line)
})
return new Promise((res, rej) => {
agent.on("error", rej)
agent.on("close", (code) => {
if (code !== 0) {
rej({
message: `coder cloud agent exited with ${code}`,
})
return
}
res()
})
})
}
export function coderCloudBind(csAddr: string, serverName = ""): Promise<void> {
logger.info("Remember --link is a beta feature and requires being accepted for testing")
logger.info("See https://github.com/cdr/code-server/discussions/2137")
// addr needs to be in host:port format.
// So we trim the protocol.
csAddr = csAddr.replace(/^https?:\/\//, "")
return runAgent("bind", `--code-server-addr=${csAddr}`, serverName)
}

View File

@@ -1,25 +1,31 @@
import { field, logger } from "@coder/logger"
import * as cp from "child_process"
import { promises as fs } from "fs"
import http from "http"
import * as path from "path"
import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
import { ApiHttpProvider } from "./app/api"
import { DashboardHttpProvider } from "./app/dashboard"
import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc"
import { plural } from "../common/util"
import { HealthHttpProvider } from "./app/health"
import { LoginHttpProvider } from "./app/login"
import { ProxyHttpProvider } from "./app/proxy"
import { StaticHttpProvider } from "./app/static"
import { UpdateHttpProvider } from "./app/update"
import { VscodeHttpProvider } from "./app/vscode"
import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli"
import {
Args,
bindAddrFromAllSources,
optionDescriptions,
parse,
readConfigFile,
setDefaults,
shouldOpenInExistingInstance,
shouldRunVsCodeCli,
} from "./cli"
import { coderCloudBind } from "./coder-cloud"
import { AuthType, HttpServer, HttpServerOptions } from "./http"
import { generateCertificate, hash, open, humanPath } from "./util"
import { ipcMain, wrap } from "./wrapper"
process.on("uncaughtException", (error) => {
logger.error(`Uncaught exception: ${error.message}`)
if (typeof error.stack !== "undefined") {
logger.error(error.stack)
}
})
import { loadPlugins } from "./plugin"
import { generateCertificate, hash, humanPath, open } from "./util"
import { ipcMain, WrapperProcess } from "./wrapper"
let pkg: { version?: string; commit?: string } = {}
try {
@@ -31,7 +37,100 @@ try {
const version = pkg.version || "development"
const commit = pkg.commit || "development"
const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void> => {
export const runVsCodeCli = (args: Args): void => {
logger.debug("forking vs code cli...")
const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], {
env: {
...process.env,
CODE_SERVER_PARENT_PID: process.pid.toString(),
},
})
vscode.once("message", (message: any) => {
logger.debug("got message from VS Code", field("message", message))
if (message.type !== "ready") {
logger.error("Unexpected response waiting for ready response", field("type", message.type))
process.exit(1)
}
const send: CliMessage = { type: "cli", args }
vscode.send(send)
})
vscode.once("error", (error) => {
logger.error("Got error from VS Code", field("error", error))
process.exit(1)
})
vscode.on("exit", (code) => process.exit(code || 0))
}
export const openInExistingInstance = async (args: Args, socketPath: string): Promise<void> => {
const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = {
type: "open",
folderURIs: [],
fileURIs: [],
forceReuseWindow: args["reuse-window"],
forceNewWindow: args["new-window"],
}
const isDir = async (path: string): Promise<boolean> => {
try {
const st = await fs.stat(path)
return st.isDirectory()
} catch (error) {
return false
}
}
for (let i = 0; i < args._.length; i++) {
const fp = path.resolve(args._[i])
if (await isDir(fp)) {
pipeArgs.folderURIs.push(fp)
} else {
pipeArgs.fileURIs.push(fp)
}
}
if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) {
logger.error("--new-window can only be used with folder paths")
process.exit(1)
}
if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) {
logger.error("Please specify at least one file or folder")
process.exit(1)
}
const vscode = http.request(
{
path: "/",
method: "POST",
socketPath,
},
(response) => {
response.on("data", (message) => {
logger.debug("got message from VS Code", field("message", message.toString()))
})
},
)
vscode.on("error", (error: unknown) => {
logger.error("got error from VS Code", field("error", error))
})
vscode.write(JSON.stringify(pipeArgs))
vscode.end()
}
const main = async (args: Args, configArgs: Args): Promise<void> => {
if (args.link) {
// If we're being exposed to the cloud, we listen on a random address and disable auth.
args = {
...args,
host: "localhost",
port: 0,
auth: AuthType.None,
socket: undefined,
cert: undefined,
}
logger.info("link: disabling auth and listening on random localhost port for cloud agent")
}
if (!args.auth) {
args = {
...args,
@@ -48,7 +147,7 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
if (args.auth === AuthType.Password && !password) {
throw new Error("Please pass in a password via the config file or $PASSWORD")
}
const [host, port] = bindAddrFromAllSources(cliArgs, configArgs)
const [host, port] = bindAddrFromAllSources(args, configArgs)
// Spawn the main HTTP server.
const options: HttpServerOptions = {
@@ -73,17 +172,24 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
}
const httpServer = new HttpServer(options)
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
httpServer.registerHttpProvider(["/", "/vscode"], VscodeHttpProvider, args)
httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
httpServer.registerHttpProvider("/static", StaticHttpProvider)
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart)
ipcMain().onDispose(() => httpServer.dispose())
await loadPlugins(httpServer, args)
ipcMain.onDispose(() => {
httpServer.dispose().then((errors) => {
errors.forEach((error) => logger.error(error.message))
})
})
logger.info(`code-server ${version} ${commit}`)
logger.info(`Using config file ${humanPath(args.config)}`)
const serverAddress = await httpServer.listen()
logger.info(`HTTP server listening on ${serverAddress}`)
@@ -110,34 +216,45 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
}
if (httpServer.proxyDomains.size > 0) {
logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`)
logger.info(` - ${plural(httpServer.proxyDomains.size, "Proxying the following domain")}:`)
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
}
if (serverAddress && !options.socket && args.open) {
// The web socket doesn't seem to work if browsing with 0.0.0.0.
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
await open(openAddress).catch(console.error)
await open(openAddress).catch((error: Error) => {
logger.error("Failed to open", field("address", openAddress), field("error", error))
})
logger.info(`Opened ${openAddress}`)
}
if (args.link) {
try {
await coderCloudBind(serverAddress!, args.link.value)
} catch (err) {
logger.error(err.message)
ipcMain.exit(1)
}
}
}
async function entry(): Promise<void> {
const tryParse = async (): Promise<[Args, Args, Args]> => {
try {
const cliArgs = parse(process.argv.slice(2))
const configArgs = await readConfigFile(cliArgs.config)
// This prioritizes the flags set in args over the ones in the config file.
let args = Object.assign(configArgs, cliArgs)
args = await setDefaults(args)
return [args, cliArgs, configArgs]
} catch (error) {
console.error(error.message)
process.exit(1)
}
const cliArgs = parse(process.argv.slice(2))
const configArgs = await readConfigFile(cliArgs.config)
// This prioritizes the flags set in args over the ones in the config file.
let args = Object.assign(configArgs, cliArgs)
args = await setDefaults(args)
// There's no need to check flags like --help or to spawn in an existing
// instance for the child process because these would have already happened in
// the parent and the child wouldn't have been spawned.
if (ipcMain.isChild) {
await ipcMain.handshake()
ipcMain.preventExit()
return main(args, configArgs)
}
const [args, cliArgs, configArgs] = await tryParse()
if (args.help) {
console.log("code-server", version, commit)
console.log("")
@@ -147,7 +264,10 @@ async function entry(): Promise<void> {
optionDescriptions().forEach((description) => {
console.log("", description)
})
} else if (args.version) {
return
}
if (args.version) {
if (args.json) {
console.log({
codeServer: version,
@@ -157,32 +277,23 @@ async function entry(): Promise<void> {
} else {
console.log(version, commit)
}
process.exit(0)
} else if (args["list-extensions"] || args["install-extension"] || args["uninstall-extension"]) {
logger.debug("forking vs code cli...")
const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], {
env: {
...process.env,
CODE_SERVER_PARENT_PID: process.pid.toString(),
},
})
vscode.once("message", (message) => {
logger.debug("Got message from VS Code", field("message", message))
if (message.type !== "ready") {
logger.error("Unexpected response waiting for ready response")
process.exit(1)
}
const send: CliMessage = { type: "cli", args }
vscode.send(send)
})
vscode.once("error", (error) => {
logger.error(error.message)
process.exit(1)
})
vscode.on("exit", (code) => process.exit(code || 0))
} else {
wrap(() => main(args, cliArgs, configArgs))
return
}
if (shouldRunVsCodeCli(args)) {
return runVsCodeCli(args)
}
const socketPath = await shouldOpenInExistingInstance(cliArgs)
if (socketPath) {
return openInExistingInstance(args, socketPath)
}
const wrapper = new WrapperProcess(require("../../package.json").version)
return wrapper.start()
}
entry()
entry().catch((error) => {
logger.error(error.message)
ipcMain.exit(error)
})

View File

@@ -12,7 +12,7 @@ import { Readable } from "stream"
import * as tls from "tls"
import * as url from "url"
import { HttpCode, HttpError } from "../common/http"
import { normalize, Options, plural, split } from "../common/util"
import { arrayify, normalize, Options, plural, split, trimSlashes } from "../common/util"
import { SocketProxyProvider } from "./socket"
import { getMediaMime, paths } from "./util"
@@ -36,9 +36,13 @@ export type Query = { [key: string]: string | string[] | undefined }
export interface ProxyOptions {
/**
* A base path to strip from from the request before proxying if necessary.
* A path to strip from from the beginning of the request before proxying
*/
base?: string
strip?: string
/**
* A path to add to the beginning of the request before proxying.
*/
prepend?: string
/**
* The port to proxy.
*/
@@ -79,9 +83,8 @@ export interface HttpResponse<T = string | Buffer | object> {
*/
mime?: string
/**
* Redirect to this path. Will rewrite against the base path but NOT the
* provider endpoint so you must include it. This allows redirecting outside
* of your endpoint.
* Redirect to this path. This is constructed against the site base (not the
* provider's base).
*/
redirect?: string
/**
@@ -133,12 +136,16 @@ export interface HttpServerOptions {
export interface Route {
/**
* Base path part (in /test/path it would be "/test").
* Provider base path part (for /provider/base/path it would be /provider).
*/
providerBase: string
/**
* Base path part (for /provider/base/path it would be /base).
*/
base: string
/**
* Remaining part of the route (in /test/path it would be "/path"). It can be
* blank.
* Remaining part of the route after factoring out the base and provider base
* (for /provider/base/path it would be /path). It can be blank.
*/
requestPath: string
/**
@@ -161,7 +168,6 @@ interface ProviderRoute extends Route {
export interface HttpProviderOptions {
readonly auth: AuthType
readonly base: string
readonly commit: string
readonly password?: string
}
@@ -175,7 +181,7 @@ export abstract class HttpProvider {
public constructor(protected readonly options: HttpProviderOptions) {}
public dispose(): void {
public async dispose(): Promise<void> {
// No default behavior.
}
@@ -203,11 +209,11 @@ export abstract class HttpProvider {
/**
* Get the base relative to the provided route. For each slash we need to go
* up a directory. For example:
* / => ./
* /foo => ./
* /foo/ => ./../
* /foo/bar => ./../
* /foo/bar/ => ./../../
* / => .
* /foo => .
* /foo/ => ./..
* /foo/bar => ./..
* /foo/bar/ => ./../..
*/
public base(route: Route): string {
const depth = (route.originalPath.match(/\//g) || []).length
@@ -229,30 +235,23 @@ export abstract class HttpProvider {
/**
* Replace common templates strings.
*/
protected replaceTemplates(route: Route, response: HttpStringFileResponse, sessionId?: string): HttpStringFileResponse
protected replaceTemplates<T extends object>(
route: Route,
response: HttpStringFileResponse,
options: T,
): HttpStringFileResponse
protected replaceTemplates(
route: Route,
response: HttpStringFileResponse,
sessionIdOrOptions?: string | object,
extraOptions?: Omit<T, "base" | "csStaticBase" | "logLevel">,
): HttpStringFileResponse {
if (typeof sessionIdOrOptions === "undefined" || typeof sessionIdOrOptions === "string") {
sessionIdOrOptions = {
base: this.base(route),
commit: this.options.commit,
logLevel: logger.level,
sessionID: sessionIdOrOptions,
} as Options
const base = this.base(route)
const options: Options = {
base,
csStaticBase: base + "/static/" + this.options.commit + this.rootPath,
logLevel: logger.level,
...extraOptions,
}
response.content = response.content
.replace(/{{COMMIT}}/g, this.options.commit)
.replace(/{{TO}}/g, Array.isArray(route.query.to) ? route.query.to[0] : route.query.to || "/dashboard")
.replace(/{{BASE}}/g, this.base(route))
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(sessionIdOrOptions)}'`)
.replace(/{{BASE}}/g, options.base)
.replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase)
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
return response
}
@@ -281,7 +280,7 @@ export abstract class HttpProvider {
* Helper to error on invalid methods (default GET).
*/
protected ensureMethod(request: http.IncomingMessage, method?: string | string[]): void {
const check = Array.isArray(method) ? method : [method || "GET"]
const check = arrayify(method || "GET")
if (!request.method || !check.includes(request.method)) {
throw new HttpError(`Unsupported method ${request.method}`, HttpCode.BadRequest)
}
@@ -290,7 +289,7 @@ export abstract class HttpProvider {
/**
* Helper to error if not authorized.
*/
protected ensureAuthenticated(request: http.IncomingMessage): void {
public ensureAuthenticated(request: http.IncomingMessage): void {
if (!this.authenticated(request)) {
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
}
@@ -397,23 +396,26 @@ export abstract class HttpProvider {
export class Heart {
private heartbeatTimer?: NodeJS.Timeout
private heartbeatInterval = 60000
private lastHeartbeat = 0
public lastHeartbeat = 0
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {}
public alive(): boolean {
const now = Date.now()
return now - this.lastHeartbeat < this.heartbeatInterval
}
/**
* Write to the heartbeat file if we haven't already done so within the
* timeout and start or reset a timer that keeps running as long as there is
* activity. Failures are logged as warnings.
*/
public beat(): void {
const now = Date.now()
if (now - this.lastHeartbeat >= this.heartbeatInterval) {
if (!this.alive()) {
logger.trace("heartbeat")
fs.outputFile(this.heartbeatPath, "").catch((error) => {
logger.warn(error.message)
})
this.lastHeartbeat = now
this.lastHeartbeat = Date.now()
if (typeof this.heartbeatTimer !== "undefined") {
clearTimeout(this.heartbeatTimer)
}
@@ -458,7 +460,7 @@ export class HttpServer {
private listenPromise: Promise<string | null> | undefined
public readonly protocol: "http" | "https"
private readonly providers = new Map<string, HttpProvider>()
private readonly heart: Heart
public readonly heart: Heart
private readonly socketProvider = new SocketProxyProvider()
/**
@@ -475,7 +477,7 @@ export class HttpServer {
this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, "")))
this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
const connections = await this.getConnections()
logger.trace(`${connections} active connection${plural(connections)}`)
logger.trace(plural(connections, `${connections} active connection`))
return connections !== 0
})
this.protocol = this.options.cert ? "https" : "http"
@@ -502,9 +504,15 @@ export class HttpServer {
})
}
public dispose(): void {
/**
* Stop and dispose everything. Return an array of disposal errors.
*/
public async dispose(): Promise<Error[]> {
this.socketProvider.stop()
this.providers.forEach((p) => p.dispose())
const providers = Array.from(this.providers.values())
// Catch so all the errors can be seen rather than just the first one.
const responses = await Promise.all<Error | undefined>(providers.map((p) => p.dispose().catch((e) => e)))
return responses.filter<Error>((r): r is Error => typeof r !== "undefined")
}
public async getConnections(): Promise<number> {
@@ -518,41 +526,51 @@ export class HttpServer {
/**
* Register a provider for a top-level endpoint.
*/
public registerHttpProvider<T extends HttpProvider>(endpoint: string, provider: HttpProvider0<T>): T
public registerHttpProvider<A1, T extends HttpProvider>(endpoint: string, provider: HttpProvider1<A1, T>, a1: A1): T
public registerHttpProvider<T extends HttpProvider>(endpoint: string | string[], provider: HttpProvider0<T>): T
public registerHttpProvider<A1, T extends HttpProvider>(
endpoint: string | string[],
provider: HttpProvider1<A1, T>,
a1: A1,
): T
public registerHttpProvider<A1, A2, T extends HttpProvider>(
endpoint: string,
endpoint: string | string[],
provider: HttpProvider2<A1, A2, T>,
a1: A1,
a2: A2,
): T
public registerHttpProvider<A1, A2, A3, T extends HttpProvider>(
endpoint: string,
endpoint: string | string[],
provider: HttpProvider3<A1, A2, A3, T>,
a1: A1,
a2: A2,
a3: A3,
): T
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public registerHttpProvider(endpoint: string, provider: any, ...args: any[]): any {
endpoint = endpoint.replace(/^\/+|\/+$/g, "")
if (this.providers.has(`/${endpoint}`)) {
throw new Error(`${endpoint} is already registered`)
}
if (/\//.test(endpoint)) {
throw new Error(`Only top-level endpoints are supported (got ${endpoint})`)
}
public registerHttpProvider(endpoint: string | string[], provider: any, ...args: any[]): void {
const p = new provider(
{
auth: this.options.auth || AuthType.None,
base: `/${endpoint}`,
commit: this.options.commit,
password: this.options.password,
},
...args,
)
this.providers.set(`/${endpoint}`, p)
return p
const endpoints = arrayify(endpoint).map(trimSlashes)
endpoints.forEach((endpoint) => {
if (/\//.test(endpoint)) {
throw new Error(`Only top-level endpoints are supported (got ${endpoint})`)
}
const existingProvider = this.providers.get(`/${endpoint}`)
this.providers.set(`/${endpoint}`, p)
if (existingProvider) {
logger.debug(`Overridding existing /${endpoint} provider`)
// If the existing provider isn't registered elsewhere we can dispose.
if (!Array.from(this.providers.values()).find((p) => p === existingProvider)) {
logger.debug(`Disposing existing /${endpoint} provider`)
existingProvider.dispose()
}
}
})
}
/**
@@ -560,14 +578,24 @@ export class HttpServer {
*/
public listen(): Promise<string | null> {
if (!this.listenPromise) {
this.listenPromise = new Promise((resolve, reject) => {
this.listenPromise = new Promise(async (resolve, reject) => {
this.server.on("error", reject)
this.server.on("upgrade", this.onUpgrade)
const onListen = (): void => resolve(this.address())
if (this.options.socket) {
try {
await fs.unlink(this.options.socket)
} catch (err) {
if (err.code !== "ENOENT") {
logger.warn(err.message)
}
}
this.server.listen(this.options.socket, onListen)
} else if (this.options.host) {
// [] is the correct format when using :: but Node errors with them.
this.server.listen(this.options.port, this.options.host.replace(/^\[|\]$/g, ""), onListen)
} else {
this.server.listen(this.options.port, this.options.host, onListen)
this.server.listen(this.options.port, onListen)
}
})
}
@@ -587,8 +615,10 @@ export class HttpServer {
}
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
this.heart.beat()
const route = this.parseUrl(request)
if (route.providerBase !== "/healthz") {
this.heart.beat()
}
const write = (payload: HttpResponse): void => {
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
"Content-Type": payload.mime || getMediaMime(payload.filePath),
@@ -627,10 +657,7 @@ export class HttpServer {
}
try {
const payload =
this.maybeRedirect(request, route) ||
(route.provider.authenticated(request) && this.maybeProxy(request)) ||
(await route.provider.handleRequest(route, request))
const payload = (await this.handleRequest(route, request)) || (await route.provider.handleRequest(route, request))
if (payload.proxy) {
this.doProxy(route, request, response, payload.proxy)
} else {
@@ -642,15 +669,17 @@ export class HttpServer {
e = new HttpError("Not found", HttpCode.NotFound)
}
const code = typeof e.code === "number" ? e.code : HttpCode.ServerError
logger.debug("Request error", field("url", request.url), field("code", code))
logger.debug("Request error", field("url", request.url), field("code", code), field("error", error))
if (code >= HttpCode.ServerError) {
logger.error(error.stack)
}
if (request.headers["content-type"] === "application/json") {
write({
code,
mime: "application/json",
content: {
error: e.message,
...(e.details || {}),
},
})
} else {
@@ -663,15 +692,23 @@ export class HttpServer {
}
/**
* Return any necessary redirection before delegating to a provider.
* Handle requests that are always in effect no matter what provider is
* registered at the route.
*/
private maybeRedirect(request: http.IncomingMessage, route: ProviderRoute): RedirectResponse | undefined {
private async handleRequest(route: ProviderRoute, request: http.IncomingMessage): Promise<HttpResponse | undefined> {
// If we're handling TLS ensure all requests are redirected to HTTPS.
if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) {
return { redirect: route.fullPath }
}
return undefined
// Return robots.txt.
if (route.fullPath === "/robots.txt") {
const filePath = path.resolve(__dirname, "../../src/browser/robots.txt")
return { content: await fs.readFile(filePath), filePath }
}
// Handle proxy domains.
return this.maybeProxy(route, request)
}
/**
@@ -722,7 +759,7 @@ export class HttpServer {
// can't be transferred so we need an in-between).
const socketProxy = await this.socketProvider.createProxy(socket)
const payload =
this.maybeProxy(request) || (await route.provider.handleWebSocket(route, request, socketProxy, head))
this.maybeProxy(route, request) || (await route.provider.handleWebSocket(route, request, socketProxy, head))
if (payload && payload.proxy) {
this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy)
}
@@ -759,7 +796,7 @@ export class HttpServer {
// that by shifting the next base out of the request path.
let provider = this.providers.get(base)
if (base !== "/" && provider) {
return { ...parse(requestPath), fullPath, query: parsedUrl.query, provider, originalPath }
return { ...parse(requestPath), providerBase: base, fullPath, query: parsedUrl.query, provider, originalPath }
}
// Fall back to the top-level provider.
@@ -767,7 +804,7 @@ export class HttpServer {
if (!provider) {
throw new Error(`No provider for ${base}`)
}
return { base, fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
return { base, providerBase: "/", fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
}
/**
@@ -806,10 +843,11 @@ export class HttpServer {
// sure how best to get this information to the `proxyRes` event handler.
// For now I'm sticking it on the request object which is passed through to
// the event.
;(request as ProxyRequest).base = options.base
;(request as ProxyRequest).base = options.strip
const isHttp = response instanceof http.ServerResponse
const path = options.base ? route.fullPath.replace(options.base, "") : route.fullPath
const base = options.strip ? route.fullPath.replace(options.strip, "") : route.fullPath
const path = normalize("/" + (options.prepend || "") + "/" + base, true)
const proxyOptions: proxy.ServerOptions = {
changeOrigin: true,
ignorePath: true,
@@ -850,6 +888,7 @@ export class HttpServer {
// isn't setting the host header to match the access domain.
host === "localhost"
) {
logger.debug("no valid cookie doman", field("host", host))
return undefined
}
@@ -859,6 +898,7 @@ export class HttpServer {
}
})
logger.debug("got cookie doman", field("host", host))
return host ? `Domain=${host}` : undefined
}
@@ -869,8 +909,10 @@ export class HttpServer {
*
* For example if `coder.com` is specified `8080.coder.com` will be proxied
* but `8080.test.coder.com` and `test.8080.coder.com` will not.
*
* Throw an error if proxying but the user isn't authenticated.
*/
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
public maybeProxy(route: ProviderRoute, request: http.IncomingMessage): HttpResponse | undefined {
// Split into parts.
const host = request.headers.host || ""
const idx = host.indexOf(":")
@@ -884,6 +926,9 @@ export class HttpServer {
return undefined
}
// Must be authenticated to use the proxy.
route.provider.ensureAuthenticated(request)
return {
proxy: {
port,

92
src/node/plugin.ts Normal file
View File

@@ -0,0 +1,92 @@
import { field, logger } from "@coder/logger"
import * as fs from "fs"
import * as path from "path"
import * as util from "util"
import { Args } from "./cli"
import { HttpServer } from "./http"
import { paths } from "./util"
/* eslint-disable @typescript-eslint/no-var-requires */
export type Activate = (httpServer: HttpServer, args: Args) => void
/**
* Plugins must implement this interface.
*/
export interface Plugin {
activate: Activate
}
/**
* Intercept imports so we can inject code-server when the plugin tries to
* import it.
*/
const originalLoad = require("module")._load
// eslint-disable-next-line @typescript-eslint/no-explicit-any
require("module")._load = function (request: string, parent: object, isMain: boolean): any {
return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain])
}
/**
* Load a plugin and run its activation function.
*/
const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise<void> => {
try {
const plugin: Plugin = require(pluginPath)
plugin.activate(httpServer, args)
const packageJson = require(path.join(pluginPath, "package.json"))
logger.debug(
"Loaded plugin",
field("name", packageJson.name || path.basename(pluginPath)),
field("path", pluginPath),
field("version", packageJson.version || "n/a"),
)
} catch (error) {
logger.error(error.message)
}
}
/**
* Load all plugins in the specified directory.
*/
const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Args): Promise<void> => {
try {
const files = await util.promisify(fs.readdir)(pluginDir, {
withFileTypes: true,
})
await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), httpServer, args)))
} catch (error) {
if (error.code !== "ENOENT") {
logger.warn(error.message)
}
}
}
/**
* Load all plugins from the `plugins` directory, directories specified by
* `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by
* `CS_PLUGIN` (also colon-separated).
*/
export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/usr/share/code-server/plugins`
const plugin = process.env.CS_PLUGIN || ""
await Promise.all([
// Built-in plugins.
_loadPlugins(path.resolve(__dirname, "../../plugins"), httpServer, args),
// User-added plugins.
...pluginPath
.split(":")
.filter((p) => !!p)
.map((dir) => _loadPlugins(path.resolve(dir), httpServer, args)),
// Individual plugins so you don't have to symlink or move them into a
// directory specifically for plugins. This lets you load plugins that are
// on the same level as other directories that are not plugins (if you tried
// to use CS_PLUGIN_PATH code-server would try to load those other
// directories as plugins). Intended for development.
...plugin
.split(":")
.filter((p) => !!p)
.map((dir) => loadPlugin(path.resolve(dir), httpServer, args)),
])
}

View File

@@ -1,7 +1,8 @@
import { logger } from "@coder/logger"
import * as fs from "fs-extra"
import * as path from "path"
import { extend, paths } from "./util"
import { logger } from "@coder/logger"
import { Route } from "./http"
import { paths } from "./util"
export type Settings = { [key: string]: Settings | string | boolean | number }
@@ -29,11 +30,13 @@ export class SettingsProvider<T> {
/**
* Write settings combined with current settings. On failure log a warning.
* Objects will be merged and everything else will be replaced.
* Settings will be merged shallowly.
*/
public async write(settings: Partial<T>): Promise<void> {
try {
await fs.writeFile(this.settingsPath, JSON.stringify(extend(await this.read(), settings), null, 2))
const oldSettings = await this.read()
const nextSettings = { ...oldSettings, ...settings }
await fs.writeFile(this.settingsPath, JSON.stringify(nextSettings, null, 2))
} catch (error) {
logger.warn(error.message)
}
@@ -55,6 +58,7 @@ export interface CoderSettings extends UpdateSettings {
url: string
workspace: boolean
}
query: Route["query"]
}
/**

View File

@@ -4,7 +4,7 @@ import * as path from "path"
import * as tls from "tls"
import { Emitter } from "../common/emitter"
import { generateUuid } from "../common/util"
import { tmpdir } from "./util"
import { canConnect, tmpdir } from "./util"
/**
* Provides a way to proxy a TLS socket. Can be used when you need to pass a
@@ -89,17 +89,6 @@ export class SocketProxyProvider {
}
public async findFreeSocketPath(basePath: string, maxTries = 100): Promise<string> {
const canConnect = (path: string): Promise<boolean> => {
return new Promise((resolve) => {
const socket = net.connect(path)
socket.once("error", () => resolve(false))
socket.once("connect", () => {
socket.destroy()
resolve(true)
})
})
}
let i = 0
let path = basePath
while ((await canConnect(path)) && i < maxTries) {

View File

@@ -1,10 +1,11 @@
import * as cp from "child_process"
import * as crypto from "crypto"
import envPaths from "env-paths"
import * as fs from "fs-extra"
import * as net from "net"
import * as os from "os"
import * as path from "path"
import * as util from "util"
import envPaths from "env-paths"
import xdgBasedir from "xdg-basedir"
export const tmpdir = path.join(os.tmpdir(), "code-server")
@@ -199,25 +200,6 @@ export const isObject = <T extends object>(obj: T): obj is T => {
return !Array.isArray(obj) && typeof obj === "object" && obj !== null
}
/**
* Extend a with b and return a new object. Properties with objects will be
* recursively merged while all other properties are just overwritten.
*/
export function extend<A, B>(a: A, b: B): A & B
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function extend(...args: any[]): any {
const c = {} as any // eslint-disable-line @typescript-eslint/no-explicit-any
for (const obj of args) {
if (!isObject(obj)) {
continue
}
for (const key in obj) {
c[key] = isObject(obj[key]) ? extend(c[key], obj[key]) : obj[key]
}
}
return c
}
/**
* Taken from vs/base/common/charCode.ts. Copied for now instead of importing so
* we don't have to set up a `vs` alias to be able to import with types (since
@@ -265,3 +247,17 @@ export function pathToFsPath(path: string, keepDriveLetterCasing = false): strin
}
return value
}
/**
* Return a promise that resolves with whether the socket path is active.
*/
export function canConnect(path: string): Promise<boolean> {
return new Promise((resolve) => {
const socket = net.connect(path)
socket.once("error", () => resolve(false))
socket.once("connect", () => {
socket.destroy()
resolve(true)
})
})
}

View File

@@ -1,6 +1,9 @@
import { logger, field } from "@coder/logger"
import { field, logger } from "@coder/logger"
import * as cp from "child_process"
import * as path from "path"
import * as rfs from "rotating-file-stream"
import { Emitter } from "../common/emitter"
import { paths } from "./util"
interface HandshakeMessage {
type: "handshake"
@@ -29,19 +32,13 @@ export class IpcMain {
public readonly onMessage = this._onMessage.event
private readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
public readonly onDispose = this._onDispose.event
public readonly exit: (code?: number) => never
public readonly processExit: (code?: number) => never = process.exit
public constructor(public readonly parentPid?: number) {
public constructor(private readonly parentPid?: number) {
process.on("SIGINT", () => this._onDispose.emit("SIGINT"))
process.on("SIGTERM", () => this._onDispose.emit("SIGTERM"))
process.on("exit", () => this._onDispose.emit(undefined))
// Ensure we control when the process exits.
this.exit = process.exit
process.exit = function (code?: number) {
logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
} as (code?: number) => never
this.onDispose((signal) => {
// Remove listeners to avoid possibly triggering disposal again.
process.removeAllListeners()
@@ -68,6 +65,27 @@ export class IpcMain {
}
}
/**
* Ensure we control when the process exits.
*/
public preventExit(): void {
process.exit = function (code?: number) {
logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
} as (code?: number) => never
}
public get isChild(): boolean {
return typeof this.parentPid !== "undefined"
}
public exit(error?: number | ProcessError): never {
if (error && typeof error !== "number") {
this.processExit(typeof error.code === "number" ? error.code : 1)
} else {
this.processExit(error)
}
}
public handshake(child?: cp.ChildProcess): Promise<void> {
return new Promise((resolve, reject) => {
const target = child || process
@@ -116,17 +134,12 @@ export class IpcMain {
}
}
let _ipcMain: IpcMain
export const ipcMain = (): IpcMain => {
if (!_ipcMain) {
_ipcMain = new IpcMain(
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined"
? parseInt(process.env.CODE_SERVER_PARENT_PID)
: undefined,
)
}
return _ipcMain
}
/**
* Channel for communication between the child and parent processes.
*/
export const ipcMain = new IpcMain(
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" ? parseInt(process.env.CODE_SERVER_PARENT_PID) : undefined,
)
export interface WrapperOptions {
maxMemory?: number
@@ -140,31 +153,27 @@ export interface WrapperOptions {
export class WrapperProcess {
private process?: cp.ChildProcess
private started?: Promise<void>
private readonly logStdoutStream: rfs.RotatingFileStream
private readonly logStderrStream: rfs.RotatingFileStream
public constructor(private currentVersion: string, private readonly options?: WrapperOptions) {
ipcMain().onDispose(() => {
if (this.process) {
this.process.removeAllListeners()
this.process.kill()
}
const opts = {
size: "10M",
maxFiles: 10,
}
this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts)
this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts)
ipcMain.onDispose(() => {
this.disposeChild()
})
ipcMain().onMessage(async (message) => {
ipcMain.onMessage((message) => {
switch (message.type) {
case "relaunch":
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
this.currentVersion = message.version
this.started = undefined
if (this.process) {
this.process.removeAllListeners()
this.process.kill()
}
try {
await this.start()
} catch (error) {
logger.error(error.message)
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
}
this.relaunch()
break
default:
logger.error(`Unrecognized message ${message}`)
@@ -173,25 +182,63 @@ export class WrapperProcess {
})
}
private disposeChild(): void {
this.started = undefined
if (this.process) {
this.process.removeAllListeners()
this.process.kill()
}
}
private async relaunch(): Promise<void> {
this.disposeChild()
try {
await this.start()
} catch (error) {
logger.error(error.message)
ipcMain.exit(typeof error.code === "number" ? error.code : 1)
}
}
public start(): Promise<void> {
if (!this.started) {
this.started = this.spawn().then((child) => {
logger.debug(`spawned inner process ${child.pid}`)
ipcMain()
.handshake(child)
.then(() => {
child.once("exit", (code) => {
logger.debug(`inner process ${child.pid} exited unexpectedly`)
ipcMain().exit(code || 0)
})
})
this.process = child
// If we have a process then we've already bound this.
if (!this.process) {
process.on("SIGUSR1", async () => {
logger.info("Received SIGUSR1; hotswapping")
this.relaunch()
})
}
if (!this.started) {
this.started = this._start()
}
return this.started
}
private async spawn(): Promise<cp.ChildProcess> {
private async _start(): Promise<void> {
const child = this.spawn()
this.process = child
// Log both to stdout and to the log directory.
if (child.stdout) {
child.stdout.pipe(this.logStdoutStream)
child.stdout.pipe(process.stdout)
}
if (child.stderr) {
child.stderr.pipe(this.logStderrStream)
child.stderr.pipe(process.stderr)
}
logger.debug(`spawned inner process ${child.pid}`)
await ipcMain.handshake(child)
child.once("exit", (code) => {
logger.debug(`inner process ${child.pid} exited unexpectedly`)
ipcMain.exit(code || 0)
})
}
private spawn(): cp.ChildProcess {
// Flags to pass along to the Node binary.
let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${(this.options && this.options.nodeOptions) || ""}`
if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) {
@@ -205,7 +252,7 @@ export class WrapperProcess {
CODE_SERVER_PARENT_PID: process.pid.toString(),
NODE_OPTIONS: nodeOptions,
},
stdio: ["inherit", "inherit", "inherit", "ipc"],
stdio: ["ipc"],
})
}
}
@@ -213,23 +260,13 @@ export class WrapperProcess {
// It's possible that the pipe has closed (for example if you run code-server
// --version | head -1). Assume that means we're done.
if (!process.stdout.isTTY) {
process.stdout.on("error", () => ipcMain().exit())
process.stdout.on("error", () => ipcMain.exit())
}
export const wrap = (fn: () => Promise<void>): void => {
if (ipcMain().parentPid) {
ipcMain()
.handshake()
.then(() => fn())
.catch((error: ProcessError): void => {
logger.error(error.message)
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
})
} else {
const wrapper = new WrapperProcess(require("../../package.json").version)
wrapper.start().catch((error) => {
logger.error(error.message)
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
})
// Don't let uncaught exceptions crash the process.
process.on("uncaughtException", (error) => {
logger.error(`Uncaught exception: ${error.message}`)
if (typeof error.stack !== "undefined") {
logger.error(error.stack)
}
}
})

View File

@@ -1,20 +1,31 @@
import { logger, Level } from "@coder/logger"
import { Level, logger } from "@coder/logger"
import * as assert from "assert"
import * as fs from "fs-extra"
import * as net from "net"
import * as os from "os"
import * as path from "path"
import { parse } from "../src/node/cli"
import { Args, parse, setDefaults, shouldOpenInExistingInstance } from "../src/node/cli"
import { paths, tmpdir } from "../src/node/util"
describe("cli", () => {
type Mutable<T> = {
-readonly [P in keyof T]: T[P]
}
describe("parser", () => {
beforeEach(() => {
delete process.env.LOG_LEVEL
})
// The parser will always fill these out.
// The parser should not set any defaults so the caller can determine what
// values the user actually set. These are only set after explicitly calling
// `setDefaults`.
const defaults = {
_: [],
"extensions-dir": path.join(paths.data, "extensions"),
"user-data-dir": paths.data,
}
it("should set defaults", () => {
assert.deepEqual(parse([]), defaults)
assert.deepEqual(parse([]), { _: [] })
})
it("should parse all available options", () => {
@@ -69,7 +80,7 @@ describe("cli", () => {
help: true,
host: "0.0.0.0",
json: true,
log: "trace",
log: "error",
open: true,
port: 8081,
socket: path.resolve("mumble"),
@@ -83,19 +94,20 @@ describe("cli", () => {
it("should work with short options", () => {
assert.deepEqual(parse(["-vvv", "-v"]), {
...defaults,
log: "trace",
_: [],
verbose: true,
version: true,
})
assert.equal(process.env.LOG_LEVEL, "trace")
assert.equal(logger.level, Level.Trace)
})
it("should use log level env var", () => {
it("should use log level env var", async () => {
const args = parse([])
assert.deepEqual(args, { _: [] })
process.env.LOG_LEVEL = "debug"
assert.deepEqual(parse([]), {
assert.deepEqual(await setDefaults(args), {
...defaults,
_: [],
log: "debug",
verbose: false,
})
@@ -103,8 +115,9 @@ describe("cli", () => {
assert.equal(logger.level, Level.Debug)
process.env.LOG_LEVEL = "trace"
assert.deepEqual(parse([]), {
assert.deepEqual(await setDefaults(args), {
...defaults,
_: [],
log: "trace",
verbose: true,
})
@@ -113,9 +126,16 @@ describe("cli", () => {
})
it("should prefer --log to env var and --verbose to --log", async () => {
let args = parse(["--log", "info"])
assert.deepEqual(args, {
_: [],
log: "info",
})
process.env.LOG_LEVEL = "debug"
assert.deepEqual(parse(["--log", "info"]), {
assert.deepEqual(await setDefaults(args), {
...defaults,
_: [],
log: "info",
verbose: false,
})
@@ -123,17 +143,26 @@ describe("cli", () => {
assert.equal(logger.level, Level.Info)
process.env.LOG_LEVEL = "trace"
assert.deepEqual(parse(["--log", "info"]), {
assert.deepEqual(await setDefaults(args), {
...defaults,
_: [],
log: "info",
verbose: false,
})
assert.equal(process.env.LOG_LEVEL, "info")
assert.equal(logger.level, Level.Info)
args = parse(["--log", "info", "--verbose"])
assert.deepEqual(args, {
_: [],
log: "info",
verbose: true,
})
process.env.LOG_LEVEL = "warn"
assert.deepEqual(parse(["--log", "info", "--verbose"]), {
assert.deepEqual(await setDefaults(args), {
...defaults,
_: [],
log: "trace",
verbose: true,
})
@@ -141,9 +170,12 @@ describe("cli", () => {
assert.equal(logger.level, Level.Trace)
})
it("should ignore invalid log level env var", () => {
it("should ignore invalid log level env var", async () => {
process.env.LOG_LEVEL = "bogus"
assert.deepEqual(parse([]), defaults)
assert.deepEqual(await setDefaults(parse([])), {
_: [],
...defaults,
})
})
it("should error if value isn't provided", () => {
@@ -166,7 +198,7 @@ describe("cli", () => {
it("should not error if the value is optional", () => {
assert.deepEqual(parse(["--cert"]), {
...defaults,
_: [],
cert: {
value: undefined,
},
@@ -177,7 +209,7 @@ describe("cli", () => {
assert.throws(() => parse(["--socket", "--socket-path-value"]), /--socket requires a value/)
// If you actually had a path like this you would do this instead:
assert.deepEqual(parse(["--socket", "./--socket-path-value"]), {
...defaults,
_: [],
socket: path.resolve("--socket-path-value"),
})
assert.throws(() => parse(["--cert", "--socket-path-value"]), /Unknown option --socket-path-value/)
@@ -185,7 +217,6 @@ describe("cli", () => {
it("should allow positional arguments before options", () => {
assert.deepEqual(parse(["foo", "test", "--auth", "none"]), {
...defaults,
_: ["foo", "test"],
auth: "none",
})
@@ -193,12 +224,85 @@ describe("cli", () => {
it("should support repeatable flags", () => {
assert.deepEqual(parse(["--proxy-domain", "*.coder.com"]), {
...defaults,
_: [],
"proxy-domain": ["*.coder.com"],
})
assert.deepEqual(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), {
...defaults,
_: [],
"proxy-domain": ["*.coder.com", "test.com"],
})
})
})
describe("cli", () => {
let args: Mutable<Args> = { _: [] }
const testDir = path.join(tmpdir, "tests/cli")
const vscodeIpcPath = path.join(os.tmpdir(), "vscode-ipc")
before(async () => {
await fs.remove(testDir)
await fs.mkdirp(testDir)
})
beforeEach(async () => {
delete process.env.VSCODE_IPC_HOOK_CLI
args = { _: [] }
await fs.remove(vscodeIpcPath)
})
it("should use existing if inside code-server", async () => {
process.env.VSCODE_IPC_HOOK_CLI = "test"
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
args.port = 8081
args._.push("./file")
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
})
it("should use existing if --reuse-window is set", async () => {
args["reuse-window"] = true
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
await fs.writeFile(vscodeIpcPath, "test")
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
args.port = 8081
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
})
it("should use existing if --new-window is set", async () => {
args["new-window"] = true
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
await fs.writeFile(vscodeIpcPath, "test")
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
args.port = 8081
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
})
it("should use existing if no unrelated flags are set, has positional, and socket is active", async () => {
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
args._.push("./file")
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
const socketPath = path.join(testDir, "socket")
await fs.writeFile(vscodeIpcPath, socketPath)
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
await new Promise((resolve) => {
const server = net.createServer(() => {
// Close after getting the first connection.
server.close()
})
server.once("listening", () => resolve(server))
server.listen(socketPath)
})
assert.strictEqual(await shouldOpenInExistingInstance(args), socketPath)
args.port = 8081
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
})
})

View File

@@ -6,8 +6,8 @@ import * as net from "net"
import * as path from "path"
import * as tls from "tls"
import { Emitter } from "../src/common/emitter"
import { generateCertificate, tmpdir } from "../src/node/util"
import { SocketProxyProvider } from "../src/node/socket"
import { generateCertificate, tmpdir } from "../src/node/util"
describe("SocketProxyProvider", () => {
const provider = new SocketProxyProvider()

View File

@@ -2,40 +2,34 @@ import * as assert from "assert"
import * as fs from "fs-extra"
import * as http from "http"
import * as path from "path"
import * as tar from "tar-fs"
import * as zlib from "zlib"
import { LatestResponse, UpdateHttpProvider } from "../src/node/app/update"
import { AuthType } from "../src/node/http"
import { SettingsProvider, UpdateSettings } from "../src/node/settings"
import { tmpdir } from "../src/node/util"
describe("update", () => {
const archivePath = path.join(tmpdir, "tests/updates/code-server-loose-source")
return
let version = "1.0.0"
let spy: string[] = []
const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => {
if (!request.url) {
throw new Error("no url")
}
spy.push(request.url)
response.writeHead(200)
// Return the latest version.
if (request.url === "/latest") {
const latest: LatestResponse = {
name: version,
}
response.writeHead(200)
return response.end(JSON.stringify(latest))
}
const path = archivePath + (request.url.endsWith(".tar.gz") ? ".tar.gz" : ".zip")
const stream = fs.createReadStream(path)
stream.on("error", (error: NodeJS.ErrnoException) => {
response.writeHead(500)
response.end(error.message)
})
response.writeHead(200)
stream.on("close", () => response.end())
stream.pipe(response)
// Anything else is a 404.
response.writeHead(404)
response.end("not found")
})
const jsonPath = path.join(tmpdir, "tests/updates/update.json")
@@ -51,12 +45,10 @@ describe("update", () => {
_provider = new UpdateHttpProvider(
{
auth: AuthType.None,
base: "/update",
commit: "test",
},
true,
`http://${address.address}:${address.port}/latest`,
`http://${address.address}:${address.port}/download/{{VERSION}}/{{RELEASE_NAME}}`,
settings,
)
}
@@ -72,32 +64,8 @@ describe("update", () => {
host: "localhost",
})
})
const p = provider()
const archiveName = (await p.getReleaseName({ version: "9999999.99999.9999", checked: 0 })).replace(
/.tar.gz$|.zip$/,
"",
)
await fs.remove(path.join(tmpdir, "tests/updates"))
await fs.mkdirp(path.join(archivePath, archiveName))
await Promise.all([
fs.writeFile(path.join(archivePath, archiveName, "code-server"), `console.log("UPDATED")`),
fs.writeFile(path.join(archivePath, archiveName, "node"), `NODE BINARY`),
])
await new Promise((resolve, reject) => {
const write = fs.createWriteStream(archivePath + ".tar.gz")
const compress = zlib.createGzip()
compress.pipe(write)
compress.on("error", (error) => compress.destroy(error))
compress.on("close", () => write.end())
tar.pack(archivePath).pipe(compress)
write.on("close", reject)
write.on("finish", () => {
resolve()
})
})
await fs.mkdirp(path.join(tmpdir, "tests/updates"))
})
after(() => {
@@ -185,53 +153,15 @@ describe("update", () => {
assert.equal(p.isLatestVersion(update), true)
})
it("should download and apply an update", async () => {
version = "9999999.99999.9999"
const p = provider()
const update = await p.getUpdate(true)
// Create an existing version.
const destination = path.join(tmpdir, "tests/updates/code-server")
await fs.mkdirp(destination)
const entry = path.join(destination, "code-server")
await fs.writeFile(entry, `console.log("OLD")`)
assert.equal(`console.log("OLD")`, await fs.readFile(entry, "utf8"))
// Updating should replace the existing version.
await p.downloadAndApplyUpdate(update, destination)
assert.equal(`console.log("UPDATED")`, await fs.readFile(entry, "utf8"))
// There should be a backup.
const dir = (await fs.readdir(path.join(tmpdir, "tests/updates"))).filter((dir) => {
return dir.startsWith("code-server.")
})
assert.equal(dir.length, 1)
assert.equal(
`console.log("OLD")`,
await fs.readFile(path.join(tmpdir, "tests/updates", dir[0], "code-server"), "utf8"),
)
const archiveName = await p.getReleaseName(update)
assert.deepEqual(spy, ["/latest", `/download/${version}/${archiveName}`])
})
it("should not reject if unable to fetch", async () => {
const options = {
auth: AuthType.None,
base: "/update",
commit: "test",
}
let provider = new UpdateHttpProvider(options, true, "invalid", "invalid", settings)
let provider = new UpdateHttpProvider(options, true, "invalid", settings)
await assert.doesNotReject(() => provider.getUpdate(true))
provider = new UpdateHttpProvider(
options,
true,
"http://probably.invalid.dev.localhost/latest",
"http://probably.invalid.dev.localhost/download",
settings,
)
provider = new UpdateHttpProvider(options, true, "http://probably.invalid.dev.localhost/latest", settings)
await assert.doesNotReject(() => provider.getUpdate(true))
})
})

View File

@@ -1,43 +1,7 @@
import * as assert from "assert"
import { normalize } from "../src/common/util"
import { extend } from "../src/node/util"
describe("util", () => {
describe("extend", () => {
it("should extend", () => {
const a = { foo: { bar: 0, baz: 2 }, garply: 4, waldo: 6 }
const b = { foo: { bar: 1, qux: 3 }, garply: "5", fred: 7 }
const extended = extend(a, b)
assert.deepEqual(extended, {
foo: { bar: 1, baz: 2, qux: 3 },
garply: "5",
waldo: 6,
fred: 7,
})
})
it("should make deep copies of the original objects", () => {
const a = { foo: 0, bar: { frobnozzle: 2 }, mumble: { qux: { thud: 4 } } }
const b = { foo: 1, bar: { chad: 3 } }
const extended = extend(a, b)
assert.notEqual(a.bar, extended.bar)
assert.notEqual(b.bar, extended.bar)
assert.notEqual(a.mumble, extended.mumble)
assert.notEqual(a.mumble.qux, extended.mumble.qux)
})
it("should handle mismatch in type", () => {
const a = { foo: { bar: 0, baz: 2, qux: { mumble: 11 } }, garply: 4, waldo: { thud: 10 } }
const b = { foo: { bar: [1], baz: { plugh: 8 }, qux: 12 }, garply: { nox: 9 }, waldo: 7 }
const extended = extend(a, b)
assert.deepEqual(extended, {
foo: { bar: [1], baz: { plugh: 8 }, qux: 12 },
garply: { nox: 9 },
waldo: 7,
})
})
})
describe("normalize", () => {
it("should remove multiple slashes", () => {
assert.equal(normalize("//foo//bar//baz///mumble"), "/foo/bar/baz/mumble")

View File

@@ -8,17 +8,15 @@
"noUnusedLocals": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./out",
"allowJs": false,
"jsx": "react",
"declaration": true,
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"tsBuildInfoFile": "./.tsbuildinfo",
"tsBuildInfoFile": "./.cache/tsbuildinfo",
"incremental": true,
"rootDir": "./src",
"typeRoots": ["./node_modules/@types", "./typings"]
},
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
"include": ["./src/**/*.ts"]
}

2961
yarn.lock

File diff suppressed because it is too large Load Diff