Compare commits

...

8 Commits

Author SHA1 Message Date
dependabot[bot]
6e2bf55346 Bump github/codeql-action/analyze from 4.38.2 to 4.39.1
Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.38.2 to 4.39.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](2892aa5e19...9b32540854)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-01 12:05:02 +00:00
cdrci
68810a6bd6 Update Helm chart and changelog with 4.144.0 (#8088) 2026-10-29 13:35:10 -08:00
cdrci
ee31cdbe02 Update Code to 1.144.0 (#8087) 2026-10-28 17:38:07 -08:00
Andy Piper
27808460b1 Fix --socket-fd when code-server is socket-activated (#8083)
The --socket-fd flag never worked. The wrapper forks the process that
listens, but only the IPC channel was inherited as fd 3, so the socket
passed by the caller was not available to the child. Node then tried to
listen on the IPC pipe and failed with "listen EEXIST".

Pass the socket to the child as stdio entry 4 and tell the child to use
that fd in the handshake.

Node also reports no address for a Unix socket adopted by fd, which made
startup fail with "Server has no address". Log the listener as fd:<n>
in that case.

Add an integration test that starts the code-server binary with a Unix
or TCP socket as fd 3 and checks that /healthz answers over it.
2026-10-28 15:11:06 -08:00
Harsh Bhanushali
70fc911358 Override basic-ftp to ^6.2.1 (#8086)
get-uri still depends on basic-ftp ^5.x, which has no patched release.
2026-10-28 14:25:45 -08:00
Raphael Fakhri
761b0d2a19 Fix cookie domain not being set against root host (#8076) 2026-10-27 13:05:56 -08:00
Vincent Coffey
09012065c9 Support load balancer service in Helm chart (#8081) 2026-10-27 11:58:13 -08:00
dependabot[bot]
e999549684 Bump follow-redirects from 1.16.0 to 1.16.2 (#8065) 2026-10-27 11:51:39 -08:00
24 changed files with 214 additions and 30 deletions

View File

@@ -89,4 +89,4 @@ jobs:
uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1

View File

@@ -22,6 +22,28 @@ Code v99.99.999
## Unreleased ## Unreleased
## [4.144.0](https://github.com/coder/code-server/releases/tag/v4.144.0) - 2026-10-29
Code v1.144.0
### Changed
- Update to Code 1.144.0
### Fixed
- `--socket-fd` now works: the inherited socket is passed on to the process
that listens, and the address is logged as `fd:<n>`. Previously code-server
failed with `listen EEXIST` or `Server has no address`.
- The cookie domain was not being set against the shared root host of proxy
domains when proxy domains contained template variables like `{{port}}`,
resulting in having to sign in multiple times per domain instead of only once
as originally intended.
- When doing the web socket origin check, if the host does not already contain a
port, fall back to `X-Forwarded-Port`.
- Fix default ports not matching in the web socket origin check (for example
`https://coder.com` will now match `https://coder.com:443`).
## [4.143.1](https://github.com/coder/code-server/releases/tag/v4.143.1) - 2026-10-23 ## [4.143.1](https://github.com/coder/code-server/releases/tag/v4.143.1) - 2026-10-23
Code v1.143.1 Code v1.143.1

View File

@@ -101,6 +101,14 @@ function update_csp() {
echo "Calculated hash as $hash" echo "Calculated hash as $hash"
# Use octothorpe as a delimiter since the hash may contain a slash. # Use octothorpe as a delimiter since the hash may contain a slash.
sed -i.bak "s#'sha256-[^']\+'#'sha256-$hash'#" "$file" sed -i.bak "s#'sha256-[^']\+'#'sha256-$hash'#" "$file"
# For the web worker extension host iframe, there is a matching sha in the
# web server.
if [[ $file == */webWorkerExtensionHostIframe.html ]] ; then
quilt add ./lib/vscode/src/vs/server/node/webClientServer.ts
sed -i.bak \
"s#webWorkerExtensionHostIframeScriptSHA = 'sha256-[^']\+'#webWorkerExtensionHostIframeScriptSHA = 'sha256-$hash'#" \
./lib/vscode/src/vs/server/node/webClientServer.ts
fi
done done
quilt refresh quilt refresh

View File

@@ -15,9 +15,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 3.57.0 version: 3.58.0
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
appVersion: 4.143.1 appVersion: 4.144.0

View File

@@ -4,8 +4,13 @@ metadata:
name: {{ include "code-server.fullname" . }} name: {{ include "code-server.fullname" . }}
labels: labels:
{{- include "code-server.labels" . | nindent 4 }} {{- include "code-server.labels" . | nindent 4 }}
annotations:
{{- with .Values.service.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec: spec:
type: {{ .Values.service.type }} type: {{ .Values.service.type }}
loadBalancerClass: {{ .Values.service.loadBalancerClass }}
ports: ports:
- port: {{ .Values.service.port }} - port: {{ .Values.service.port }}
targetPort: http targetPort: http

View File

@@ -6,7 +6,7 @@ replicaCount: 1
image: image:
repository: codercom/code-server repository: codercom/code-server
tag: '4.143.1' tag: '4.144.0'
pullPolicy: Always pullPolicy: Always
# Specifies one or more secrets to be used when pulling images from a # Specifies one or more secrets to be used when pulling images from a
@@ -48,6 +48,8 @@ priorityClassName: ""
service: service:
type: ClusterIP type: ClusterIP
port: 8080 port: 8080
loadBalancerClass: null
annotations: {}
ingress: ingress:
enabled: false enabled: false

12
package-lock.json generated
View File

@@ -1726,9 +1726,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/basic-ftp": { "node_modules/basic-ftp": {
"version": "5.3.1", "version": "6.2.3",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-6.2.3.tgz",
"integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", "integrity": "sha512-T7GYtQ2eKJUN5YORG7A8u2WfB9JMN0a4/wl3MI64qq2qTRfa3FXzoNq0DDojZDkDyStcL+TkfKsz4JHEZx5kGQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
@@ -3245,9 +3245,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.16.0", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.2.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "integrity": "sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",

View File

@@ -90,6 +90,9 @@
"resolutions": { "resolutions": {
"@types/node": "24.x" "@types/node": "24.x"
}, },
"overrides": {
"basic-ftp": "^6.2.1"
},
"bin": { "bin": {
"code-server": "out/node/entry.js" "code-server": "out/node/entry.js"
}, },

View File

@@ -10,7 +10,7 @@ Index: code-server/lib/vscode/src/vs/base/common/network.ts
=================================================================== ===================================================================
--- code-server.orig/lib/vscode/src/vs/base/common/network.ts --- code-server.orig/lib/vscode/src/vs/base/common/network.ts
+++ code-server/lib/vscode/src/vs/base/common/network.ts +++ code-server/lib/vscode/src/vs/base/common/network.ts
@@ -256,7 +256,9 @@ class RemoteAuthoritiesImpl { @@ -262,7 +262,9 @@ class RemoteAuthoritiesImpl {
return URI.from({ return URI.from({
scheme: platform.isWeb ? this._preferredWebSchema : Schemas.vscodeRemoteResource, scheme: platform.isWeb ? this._preferredWebSchema : Schemas.vscodeRemoteResource,
authority: `${host}:${port}`, authority: `${host}:${port}`,

View File

@@ -2,7 +2,7 @@ Index: code-server/lib/vscode/build/gulpfile.extensions.ts
=================================================================== ===================================================================
--- code-server.orig/lib/vscode/build/gulpfile.extensions.ts --- code-server.orig/lib/vscode/build/gulpfile.extensions.ts
+++ code-server/lib/vscode/build/gulpfile.extensions.ts +++ code-server/lib/vscode/build/gulpfile.extensions.ts
@@ -294,6 +294,29 @@ export const compileCopilotExtensionBuil @@ -283,6 +283,29 @@ export const compileCopilotExtensionBuil
task.task(compileCopilotExtensionBuildTask); task.task(compileCopilotExtensionBuildTask);
/** /**

View File

@@ -6,8 +6,8 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/webview/browser/pre/index
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" <meta http-equiv="Content-Security-Policy"
- content="default-src 'none'; script-src 'sha256-7cKy9iQfWRXcgRS74x6I4q5cRDkv8nQz51jecqMmQ5w=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> - content="default-src 'none'; script-src 'sha256-Q3Tfcw0dGu/YD4dGBrRqBeOnmRMAH8HbKxArK4YSXBc=' 'self'; frame-src 'self'; style-src 'unsafe-inline';">
+ content="default-src 'none'; script-src 'sha256-FYdHnaDkSHwSK3TmWDrs/tT+X4bK+aaoa9yy432BONk=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> + content="default-src 'none'; script-src 'sha256-BmcTt6YZr75RujSQ3gF/UQL77e1RrOx62jNeikpwtxc=' 'self'; frame-src 'self'; style-src 'unsafe-inline';">
<!-- Disable pinch zooming --> <!-- Disable pinch zooming -->
<meta name="viewport" <meta name="viewport"
@@ -24,3 +24,16 @@ Index: code-server/lib/vscode/src/vs/workbench/services/extensions/worker/webWor
connect-src 'self' https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*;"/> connect-src 'self' https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*;"/>
</head> </head>
<body> <body>
Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts
+++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts
@@ -114,7 +114,7 @@ const APP_ROOT = dirname(FileAccess.asFi
const STATIC_PATH = `/static`;
const CALLBACK_PATH = `/callback`;
const WEB_EXTENSION_PATH = `/web-extension-resource`;
-const webWorkerExtensionHostIframeScriptSHA = 'sha256-daEgfo2VIXpx2Np71KqCCbkeQwv+68vPrx54XRcbdcs=';
+const webWorkerExtensionHostIframeScriptSHA = 'sha256-gfjiOVjAlxqrivZAfTq2d83mOaz0fnAxbGaFpj5IjD8=';
/**
* Substitutes the `{{...}}` placeholders of a workbench template. Placeholders must only ever

View File

@@ -19,7 +19,7 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/welcomeGettingStarted/bro
import { renderFormattedText } from '../../../../base/browser/formattedTextRenderer.js'; import { renderFormattedText } from '../../../../base/browser/formattedTextRenderer.js';
import { status } from '../../../../base/browser/ui/aria/aria.js'; import { status } from '../../../../base/browser/ui/aria/aria.js';
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
@@ -55,7 +55,7 @@ import { IRecentFolder, IRecentWorkspace @@ -56,7 +56,7 @@ import { IRecentFolder, IRecentWorkspace
import { OpenRecentAction } from '../../../browser/actions/windowActions.js'; import { OpenRecentAction } from '../../../browser/actions/windowActions.js';
import { OpenFileFolderAction, OpenFolderAction, OpenFolderViaWorkspaceAction } from '../../../browser/actions/workspaceActions.js'; import { OpenFileFolderAction, OpenFolderAction, OpenFolderViaWorkspaceAction } from '../../../browser/actions/workspaceActions.js';
import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
@@ -28,7 +28,7 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/welcomeGettingStarted/bro
import { IEditorOpenContext, IEditorSerializer } from '../../../common/editor.js'; import { IEditorOpenContext, IEditorSerializer } from '../../../common/editor.js';
import { IWebviewElement, IWebviewService } from '../../webview/browser/webview.js'; import { IWebviewElement, IWebviewService } from '../../webview/browser/webview.js';
import './gettingStartedColors.js'; import './gettingStartedColors.js';
@@ -930,6 +930,72 @@ export class GettingStartedPage extends @@ -944,6 +944,72 @@ export class GettingStartedPage extends
$('p.subtitle.description', {}, localize({ key: 'gettingStarted.editingEvolved', comment: ['Shown as subtitle on the Welcome page.'] }, "Editing evolved")) $('p.subtitle.description', {}, localize({ key: 'gettingStarted.editingEvolved', comment: ['Shown as subtitle on the Welcome page.'] }, "Editing evolved"))
); );
@@ -101,7 +101,7 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/welcomeGettingStarted/bro
const leftColumn = $('.categories-column.categories-column-left', {},); const leftColumn = $('.categories-column.categories-column-left', {},);
const rightColumn = $('.categories-column.categories-column-right', {},); const rightColumn = $('.categories-column.categories-column-right', {},);
@@ -980,6 +1046,9 @@ export class GettingStartedPage extends @@ -1000,6 +1066,9 @@ export class GettingStartedPage extends
recentList.setLimit(5); recentList.setLimit(5);
reset(leftColumn, startList.getDomElement(), recentList.getDomElement()); reset(leftColumn, startList.getDomElement(), recentList.getDomElement());
} }

View File

@@ -169,7 +169,7 @@ Index: code-server/lib/vscode/src/vs/workbench/browser/web.main.ts
=================================================================== ===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.main.ts --- code-server.orig/lib/vscode/src/vs/workbench/browser/web.main.ts
+++ code-server/lib/vscode/src/vs/workbench/browser/web.main.ts +++ code-server/lib/vscode/src/vs/workbench/browser/web.main.ts
@@ -64,6 +64,7 @@ import { IOpenerService } from '../../pl @@ -65,6 +65,7 @@ import { IOpenerService } from '../../pl
import { mixin, safeStringify } from '../../base/common/objects.js'; import { mixin, safeStringify } from '../../base/common/objects.js';
import { IndexedDB } from '../../base/browser/indexedDB.js'; import { IndexedDB } from '../../base/browser/indexedDB.js';
import { WebFileSystemAccess } from '../../platform/files/browser/webFileSystemAccess.js'; import { WebFileSystemAccess } from '../../platform/files/browser/webFileSystemAccess.js';
@@ -177,7 +177,7 @@ Index: code-server/lib/vscode/src/vs/workbench/browser/web.main.ts
import { IProgressService } from '../../platform/progress/common/progress.js'; import { IProgressService } from '../../platform/progress/common/progress.js';
import { DelayedLogChannel } from '../services/output/common/delayedLogChannel.js'; import { DelayedLogChannel } from '../services/output/common/delayedLogChannel.js';
import { dirname, joinPath } from '../../base/common/resources.js'; import { dirname, joinPath } from '../../base/common/resources.js';
@@ -140,6 +141,9 @@ export class BrowserMain extends Disposa @@ -141,6 +142,9 @@ export class BrowserMain extends Disposa
// Startup // Startup
const instantiationService = workbench.startup(); const instantiationService = workbench.startup();

View File

@@ -77,7 +77,7 @@ Index: code-server/lib/vscode/src/vs/platform/extensionResourceLoader/common/ext
import { TargetPlatform } from '../../extensions/common/extensions.js'; import { TargetPlatform } from '../../extensions/common/extensions.js';
import { ExtensionGalleryResourceType, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../extensionManagement/common/extensionGalleryManifest.js'; import { ExtensionGalleryResourceType, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../extensionManagement/common/extensionGalleryManifest.js';
import { ILogService } from '../../log/common/log.js'; import { ILogService } from '../../log/common/log.js';
@@ -163,9 +162,9 @@ export abstract class AbstractExtensionR @@ -167,9 +166,9 @@ export abstract class AbstractExtensionR
} }
protected _isWebExtensionResourceEndPoint(uri: URI): boolean { protected _isWebExtensionResourceEndPoint(uri: URI): boolean {

View File

@@ -10,7 +10,7 @@ Index: code-server/lib/vscode/src/vs/workbench/services/extensions/common/extens
=================================================================== ===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/services/extensions/common/extensions.ts --- code-server.orig/lib/vscode/src/vs/workbench/services/extensions/common/extensions.ts
+++ code-server/lib/vscode/src/vs/workbench/services/extensions/common/extensions.ts +++ code-server/lib/vscode/src/vs/workbench/services/extensions/common/extensions.ts
@@ -321,6 +321,10 @@ function extensionDescriptionArrayToMap( @@ -324,6 +324,10 @@ function extensionDescriptionArrayToMap(
} }
export function isProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): boolean { export function isProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): boolean {

View File

@@ -179,7 +179,7 @@ Index: code-server/lib/vscode/src/vs/workbench/browser/web.main.ts
=================================================================== ===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.main.ts --- code-server.orig/lib/vscode/src/vs/workbench/browser/web.main.ts
+++ code-server/lib/vscode/src/vs/workbench/browser/web.main.ts +++ code-server/lib/vscode/src/vs/workbench/browser/web.main.ts
@@ -33,6 +33,7 @@ import { ISignService } from '../../plat @@ -34,6 +34,7 @@ import { ISignService } from '../../plat
import { SignService } from '../../platform/sign/browser/signService.js'; import { SignService } from '../../platform/sign/browser/signService.js';
import { IWorkbenchConstructionOptions, IWorkbench, IWorkspace, ITunnel } from './web.api.js'; import { IWorkbenchConstructionOptions, IWorkbench, IWorkspace, ITunnel } from './web.api.js';
import { BrowserStorageService } from '../services/storage/browser/storageService.js'; import { BrowserStorageService } from '../services/storage/browser/storageService.js';
@@ -187,7 +187,7 @@ Index: code-server/lib/vscode/src/vs/workbench/browser/web.main.ts
import { IStorageService } from '../../platform/storage/common/storage.js'; import { IStorageService } from '../../platform/storage/common/storage.js';
import { toLocalISOString } from '../../base/common/date.js'; import { toLocalISOString } from '../../base/common/date.js';
import { isWorkspaceToOpen, isFolderToOpen } from '../../platform/window/common/window.js'; import { isWorkspaceToOpen, isFolderToOpen } from '../../platform/window/common/window.js';
@@ -482,7 +483,7 @@ export class BrowserMain extends Disposa @@ -488,7 +489,7 @@ export class BrowserMain extends Disposa
serviceCollection.set(IWorkbenchConfigurationService, service); serviceCollection.set(IWorkbenchConfigurationService, service);
return service; return service;
}), }),
@@ -196,7 +196,7 @@ Index: code-server/lib/vscode/src/vs/workbench/browser/web.main.ts
serviceCollection.set(IStorageService, service); serviceCollection.set(IStorageService, service);
return service; return service;
}) })
@@ -604,7 +605,21 @@ export class BrowserMain extends Disposa @@ -610,7 +611,21 @@ export class BrowserMain extends Disposa
})); }));
} }

View File

@@ -111,9 +111,14 @@ export const createApp = async (args: DefaultedArgs): Promise<App> => {
* *
* The address might be a URL or it might be a pipe or socket path. * The address might be a URL or it might be a pipe or socket path.
*/ */
export const ensureAddress = (server: http.Server, protocol: string): URL | string => { export const ensureAddress = (server: http.Server, protocol: string, socketFd?: number): URL | string => {
const addr = server.address() const addr = server.address()
// Node reports no address for a Unix socket adopted by fd.
if (!addr && typeof socketFd === "number") {
return `fd:${socketFd}`
}
if (!addr) { if (!addr) {
throw new Error("Server has no address") throw new Error("Server has no address")
} }

View File

@@ -235,7 +235,16 @@ export const getCookieDomain = (host: string, proxyDomains: string[]): string |
return undefined return undefined
} }
proxyDomains.forEach((domain) => { proxyDomains.forEach((proxyDomain) => {
// Proxy domains are patterns like `{{port}}.domain.tld` or
// `prefix-{{port}}-suffix.domain.tld` so use the part after the label that
// contains the port.
const portIndex = proxyDomain.indexOf("{{port}}")
const domain = portIndex === -1 ? proxyDomain : proxyDomain.substring(proxyDomain.indexOf(".", portIndex) + 1)
// Skip patterns we cannot turn into a domain, like `{{port}}.{{host}}`.
if (!domain || domain.includes("{{")) {
return
}
if (host.endsWith(domain) && domain.length < host.length) { if (host.endsWith(domain) && domain.length < host.length) {
host = domain host = domain
} }

View File

@@ -141,7 +141,7 @@ export const runCodeServer = async (
const app = await createApp(args) const app = await createApp(args)
const protocol = args.cert ? "https" : "http" const protocol = args.cert ? "https" : "http"
const serverAddress = ensureAddress(app.server, protocol) const serverAddress = ensureAddress(app.server, protocol, args["socket-fd"])
const { disposeRoutes, heart } = await register(app, args) const { disposeRoutes, heart } = await register(app, args)
logger.info(`Using config file ${args.config}`) logger.info(`Using config file ${args.config}`)

View File

@@ -8,6 +8,9 @@ import { paths } from "./util"
const timeoutInterval = 10000 // 10s, matches VS Code's timeouts. const timeoutInterval = 10000 // 10s, matches VS Code's timeouts.
// Fd 3 in the child is the IPC channel, so the inherited socket goes to fd 4.
const CHILD_SOCKET_FD = 4
/** /**
* Listen to a single message from a process. Reject if the process errors, * Listen to a single message from a process. Reject if the process errors,
* exits, or times out. * exits, or times out.
@@ -334,13 +337,14 @@ export class ParentProcess extends Process {
} }
private spawn(): cp.ChildProcess { private spawn(): cp.ChildProcess {
const socketFd = this.args?.["socket-fd"]
return cp.fork(path.join(__dirname, "entry"), { return cp.fork(path.join(__dirname, "entry"), {
env: { env: {
...process.env, ...process.env,
CODE_SERVER_PARENT_PID: process.pid.toString(), CODE_SERVER_PARENT_PID: process.pid.toString(),
NODE_EXEC_PATH: process.execPath, NODE_EXEC_PATH: process.execPath,
}, },
stdio: ["pipe", "pipe", "pipe", "ipc"], stdio: typeof socketFd === "number" ? ["pipe", "pipe", "pipe", "ipc", socketFd] : ["pipe", "pipe", "pipe", "ipc"],
}) })
} }
@@ -359,7 +363,8 @@ export class ParentProcess extends Process {
this.logger, this.logger,
) )
this.logger.debug("got message", field("message", message)) this.logger.debug("got message", field("message", message))
this.send(child, { type: "handshake", args: this.args }) const args = typeof this.args["socket-fd"] === "number" ? { ...this.args, "socket-fd": CHILD_SOCKET_FD } : this.args
this.send(child, { type: "handshake", args })
} }
/** /**

View File

@@ -0,0 +1,80 @@
import { ChildProcess, spawn } from "child_process"
import { promises as fs } from "fs"
import * as http from "http"
import * as net from "net"
import * as path from "path"
import { clean, tmpdir } from "../utils/helpers"
describe("--socket-fd", () => {
const testName = "socketFd"
const codeServerPath = process.env.CODE_SERVER_PATH || path.resolve("../../release-standalone/bin/code-server")
let tempDir: string
let socketDir: string | undefined
let child: ChildProcess | undefined
beforeEach(async () => {
await clean(testName)
tempDir = await tmpdir(testName)
})
afterEach(async () => {
if (child?.pid && child.exitCode === null) {
// Kill the process group so that the wrapper's inner process also stops.
process.kill(-child.pid, "SIGKILL")
}
if (socketDir) {
await fs.rm(socketDir, { force: true, recursive: true })
socketDir = undefined
}
})
// Give the listener to code-server as fd 3, as systemd socket activation does.
const getHealthzThroughFd = async (server: net.Server, target: http.RequestOptions): Promise<number> => {
const fd: number = (server as any)._handle.fd
let output = ""
child = spawn(
codeServerPath,
[
"--socket-fd=3",
"--auth=none",
`--user-data-dir=${path.join(tempDir, "data")}`,
`--config=${path.join(tempDir, "config.yaml")}`,
],
{ detached: true, stdio: ["ignore", "pipe", "pipe", fd] },
)
child.stdout?.on("data", (chunk) => (output += chunk))
child.stderr?.on("data", (chunk) => (output += chunk))
await new Promise((resolve) => server.close(resolve))
const exited = new Promise<never>((_, reject) => {
child?.once("exit", (code) => reject(new Error(`code-server exited with code ${code}:\n${output}`)))
})
const status = new Promise<number>((resolve, reject) => {
http
.get({ ...target, path: "/healthz" }, (res) => {
res.resume()
resolve(res.statusCode ?? 0)
})
.once("error", (error) => reject(new Error(`${error.message}:\n${output}`)))
})
return Promise.race([status, exited])
}
it("should serve requests on a Unix socket passed as a file descriptor", async () => {
// macOS limits socket paths to 104 bytes, and os.tmpdir() can be too long.
socketDir = await fs.mkdtemp("/tmp/cs-fd-")
const server = net.createServer()
await new Promise<void>((resolve) => server.listen(path.join(socketDir!, "listen.sock"), resolve))
// server.close() unlinks the socket path, so connect through a hard link.
const socketPath = path.join(socketDir, "connect.sock")
await fs.link(path.join(socketDir, "listen.sock"), socketPath)
expect(await getHealthzThroughFd(server, { socketPath })).toBe(200)
}, 60000)
it("should serve requests on a TCP socket passed as a file descriptor", async () => {
const server = net.createServer()
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve))
const { port } = server.address() as net.AddressInfo
expect(await getHealthzThroughFd(server, { host: "127.0.0.1", port })).toBe(200)
}, 60000)
})

View File

@@ -152,6 +152,9 @@ describe("ensureAddress", () => {
it("should throw and error if no address", () => { it("should throw and error if no address", () => {
expect(() => ensureAddress(mockServer, "http")).toThrow("Server has no address") expect(() => ensureAddress(mockServer, "http")).toThrow("Server has no address")
}) })
it("should describe a --socket-fd listener by its fd when node reports no address", () => {
expect(ensureAddress(mockServer, "http", 4)).toBe("fd:4")
})
it("should return the address if it's a string", async () => { it("should return the address if it's a string", async () => {
mockServer.address = () => "/path/to/unix.sock" mockServer.address = () => "/path/to/unix.sock"
const address = ensureAddress(mockServer, "http") const address = ensureAddress(mockServer, "http")

View File

@@ -230,6 +230,35 @@ describe("http", () => {
}) })
}) })
describe("getCookieDomain", () => {
it("should use the proxy domain for the cookie", () => {
const proxyDomains = ["{{port}}.example.com", "code-{{port}}.test.com"]
expect(http.getCookieDomain("example.com", proxyDomains)).toBe("example.com")
expect(http.getCookieDomain("8080.example.com", proxyDomains)).toBe("example.com")
expect(http.getCookieDomain("8080.example.com:443", proxyDomains)).toBe("example.com")
expect(http.getCookieDomain("code-8080.test.com", proxyDomains)).toBe("test.com")
})
it("should use the highest level proxy domain", () => {
const proxyDomains = ["{{port}}.test.example.com", "{{port}}.example.com"]
expect(http.getCookieDomain("test.example.com", proxyDomains)).toBe("example.com")
expect(http.getCookieDomain("8080.test.example.com", proxyDomains)).toBe("example.com")
})
it("should use the host when no proxy domain matches", () => {
expect(http.getCookieDomain("code.other.com", ["{{port}}.example.com"])).toBe("code.other.com")
expect(http.getCookieDomain("8080.example.com", ["{{port}}.{{host}}"])).toBe("8080.example.com")
expect(http.getCookieDomain("8080.example.com", [])).toBe("8080.example.com")
})
it("should not set a domain for localhost or IP addresses", () => {
const proxyDomains = ["{{port}}.example.com"]
expect(http.getCookieDomain("localhost:8080", proxyDomains)).toBe(undefined)
expect(http.getCookieDomain("127.0.0.1:8080", proxyDomains)).toBe(undefined)
expect(http.getCookieDomain("8080.dev.localhost", proxyDomains)).toBe(undefined)
})
})
describe("constructRedirectPath", () => { describe("constructRedirectPath", () => {
it("should preserve slashes in queryString so they are human-readable", () => { it("should preserve slashes in queryString so they are human-readable", () => {
const mockReq = getMockReq({ const mockReq = getMockReq({