Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1995ede5d | ||
|
|
c87d955bad | ||
|
|
7407189cf5 | ||
|
|
a84fb3602d | ||
|
|
84598e03fa | ||
|
|
8fa7457bbf | ||
|
|
00af8379a2 | ||
|
|
6692e6a590 | ||
|
|
cacb25d2ed | ||
|
|
c6ca9f6edb | ||
|
|
fad1253513 | ||
|
|
45debbd528 | ||
|
|
43e5211996 | ||
|
|
4a587ada27 |
@@ -4,7 +4,7 @@
|
|||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
||||||
"ghcr.io/devcontainers/features/dotnet": {
|
"ghcr.io/devcontainers/features/dotnet": {
|
||||||
"version": "8.0.419"
|
"version": "8.0.420"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": "20"
|
"version": "20"
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ ARG TARGETOS
|
|||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG RUNNER_VERSION
|
ARG RUNNER_VERSION
|
||||||
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
||||||
ARG DOCKER_VERSION=29.3.1
|
ARG DOCKER_VERSION=29.4.0
|
||||||
ARG BUILDX_VERSION=0.33.0
|
ARG BUILDX_VERSION=0.33.0
|
||||||
|
|
||||||
RUN apt update -y && apt install curl unzip -y
|
RUN apt update -y && apt install curl unzip -y
|
||||||
|
|||||||
+29
-26
@@ -1,33 +1,36 @@
|
|||||||
## What's Changed
|
## What's Changed
|
||||||
* Log inner exception message. by @TingluoHuang in https://github.com/actions/runner/pull/4265
|
* Bump flatted from 3.2.7 to 3.4.2 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4307
|
||||||
* Fix composite post-step marker display names by @ericsciple in https://github.com/actions/runner/pull/4267
|
* Add DAP server by @rentziass in https://github.com/actions/runner/pull/4298
|
||||||
* Bump actions/download-artifact from 7 to 8 by @dependabot[bot] in https://github.com/actions/runner/pull/4269
|
* Bump @typescript-eslint/eslint-plugin from 8.57.1 to 8.57.2 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4310
|
||||||
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4272
|
* Remove AllowCaseFunction feature flag by @ericsciple in https://github.com/actions/runner/pull/4316
|
||||||
* Avoid throw in SelfUpdaters. by @TingluoHuang in https://github.com/actions/runner/pull/4274
|
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4319
|
||||||
* Fix parser comparison mismatches by @ericsciple in https://github.com/actions/runner/pull/4273
|
* Batch and deduplicate action resolution across composite depths by @stefanpenner in https://github.com/actions/runner/pull/4296
|
||||||
* Devcontainer: bump base image Ubuntu version by @MaxHorstmann in https://github.com/actions/runner/pull/4277
|
* Add support for Bearer token in action archive downloads by @TingluoHuang in https://github.com/actions/runner/pull/4321
|
||||||
* Support `entrypoint` and `command` for service containers by @ericsciple in https://github.com/actions/runner/pull/4276
|
* Bump brace-expansion in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4318
|
||||||
* Bump actions/upload-artifact from 6 to 7 by @dependabot[bot] in https://github.com/actions/runner/pull/4270
|
* Add devtunnel connection for debugger jobs by @rentziass in https://github.com/actions/runner/pull/4317
|
||||||
* Bump docker/login-action from 3 to 4 by @dependabot[bot] in https://github.com/actions/runner/pull/4278
|
* Update Docker to v29.3.1 and Buildx to v0.33.0 by @github-actions[bot] in https://github.com/actions/runner/pull/4324
|
||||||
* Fix positional arg bug in ExpressionParser.CreateTree by @ericsciple in https://github.com/actions/runner/pull/4279
|
* Bump @typescript-eslint/eslint-plugin from 8.57.2 to 8.58.1 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4327
|
||||||
* Bump docker/build-push-action from 6 to 7 by @dependabot[bot] in https://github.com/actions/runner/pull/4283
|
* Bump actions/github-script from 8 to 9 by @dependabot[bot] in https://github.com/actions/runner/pull/4331
|
||||||
* Bump docker/setup-buildx-action from 3 to 4 by @dependabot[bot] in https://github.com/actions/runner/pull/4282
|
* Bump typescript from 5.9.3 to 6.0.2 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4329
|
||||||
* Bump actions/attest-build-provenance from 3 to 4 by @dependabot[bot] in https://github.com/actions/runner/pull/4266
|
* fix: only show changed versions in node upgrade PR description by @salmanmkc in https://github.com/actions/runner/pull/4332
|
||||||
* Bump @stylistic/eslint-plugin from 5.9.0 to 5.10.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4281
|
* Bump System.Formats.Asn1, Cryptography.Pkcs, ProtectedData, ServiceController, CodePages, Threading.Channels, @actions/glob, @typescript-eslint/parser, lint-staged, picomatch by @Copilot in https://github.com/actions/runner/pull/4333
|
||||||
* Update Docker to v29.3.0 and Buildx to v0.32.1 by @github-actions[bot] in https://github.com/actions/runner/pull/4286
|
* feat: add `job.workflow_*` typed accessors to JobContext by @salmanmkc in https://github.com/actions/runner/pull/4335
|
||||||
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4287
|
* Add WS bridge over DAP TCP server by @rentziass in https://github.com/actions/runner/pull/4328
|
||||||
* Fix cancellation token race during parser comparison by @ericsciple in https://github.com/actions/runner/pull/4280
|
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4355
|
||||||
* Bump @typescript-eslint/eslint-plugin from 8.47.0 to 8.54.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4230
|
* Bump Docker version to 29.4.0 by @Copilot in https://github.com/actions/runner/pull/4352
|
||||||
* Exit with specified exit code when runner is outdated by @nikola-jokic in https://github.com/actions/runner/pull/4285
|
* Update dotnet sdk to latest version @8.0.420 by @github-actions[bot] in https://github.com/actions/runner/pull/4356
|
||||||
* Report infra_error for action download failures. by @TingluoHuang in https://github.com/actions/runner/pull/4294
|
* Bump @typescript-eslint/parser from 8.58.1 to 8.59.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4360
|
||||||
* Update dotnet sdk to latest version @8.0.419 by @github-actions[bot] in https://github.com/actions/runner/pull/4301
|
* Bump System.Formats.Asn1 and System.Security.Cryptography.Pkcs by @dependabot[bot] in https://github.com/actions/runner/pull/4362
|
||||||
* Node 24 enforcement + Linux ARM32 deprecation support by @salmanmkc in https://github.com/actions/runner/pull/4303
|
* Add vulnerability-alerts permission by @salmanmkc in https://github.com/actions/runner/pull/4350
|
||||||
* Bump @typescript-eslint/eslint-plugin from 8.54.0 to 8.57.1 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4304
|
* Bump @typescript-eslint/eslint-plugin from 8.58.1 to 8.59.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4359
|
||||||
|
* Bump System.ServiceProcess.ServiceController from 10.0.3 to 10.0.6 by @dependabot[bot] in https://github.com/actions/runner/pull/4358
|
||||||
|
* Bump typescript from 6.0.2 to 6.0.3 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4353
|
||||||
|
* Bump Microsoft.DevTunnels.Connections from 1.3.16 to 1.3.39 by @dependabot[bot] in https://github.com/actions/runner/pull/4339
|
||||||
|
|
||||||
## New Contributors
|
## New Contributors
|
||||||
* @MaxHorstmann made their first contribution in https://github.com/actions/runner/pull/4277
|
* @stefanpenner made their first contribution in https://github.com/actions/runner/pull/4296
|
||||||
|
|
||||||
**Full Changelog**: https://github.com/actions/runner/compare/v2.332.0...v2.333.0
|
**Full Changelog**: https://github.com/actions/runner/compare/v2.333.1...v2.334.0
|
||||||
|
|
||||||
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
|
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
|
||||||
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.
|
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
<Update to ./src/runnerversion when creating release>
|
2.334.0
|
||||||
|
|||||||
+134
-125
@@ -14,8 +14,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
"@stylistic/eslint-plugin": "^5.10.0",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.58.1",
|
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
||||||
"@typescript-eslint/parser": "^8.58.1",
|
"@typescript-eslint/parser": "^8.59.0",
|
||||||
"@vercel/ncc": "^0.38.3",
|
"@vercel/ncc": "^0.38.3",
|
||||||
"eslint": "^8.47.0",
|
"eslint": "^8.47.0",
|
||||||
"eslint-plugin-github": "^4.10.2",
|
"eslint-plugin-github": "^4.10.2",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.4.0",
|
"lint-staged": "^16.4.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"typescript": "^6.0.2"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
@@ -327,16 +327,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz",
|
||||||
"integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
|
"integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.12.2",
|
"@eslint-community/regexpp": "^4.12.2",
|
||||||
"@typescript-eslint/scope-manager": "8.58.1",
|
"@typescript-eslint/scope-manager": "8.59.0",
|
||||||
"@typescript-eslint/type-utils": "8.58.1",
|
"@typescript-eslint/type-utils": "8.59.0",
|
||||||
"@typescript-eslint/utils": "8.58.1",
|
"@typescript-eslint/utils": "8.59.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.58.1",
|
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"ts-api-utils": "^2.5.0"
|
"ts-api-utils": "^2.5.0"
|
||||||
@@ -349,7 +349,7 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.58.1",
|
"@typescript-eslint/parser": "^8.59.0",
|
||||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
"typescript": ">=4.8.4 <6.1.0"
|
"typescript": ">=4.8.4 <6.1.0"
|
||||||
}
|
}
|
||||||
@@ -377,15 +377,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz",
|
||||||
"integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
|
"integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.58.1",
|
"@typescript-eslint/scope-manager": "8.59.0",
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.58.1",
|
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.58.1",
|
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||||
"debug": "^4.4.3"
|
"debug": "^4.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -401,13 +401,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz",
|
||||||
"integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==",
|
"integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.58.1",
|
"@typescript-eslint/tsconfig-utils": "^8.59.0",
|
||||||
"@typescript-eslint/types": "^8.58.1",
|
"@typescript-eslint/types": "^8.59.0",
|
||||||
"debug": "^4.4.3"
|
"debug": "^4.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -422,13 +422,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz",
|
||||||
"integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==",
|
"integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.58.1"
|
"@typescript-eslint/visitor-keys": "8.59.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -439,9 +439,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz",
|
||||||
"integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==",
|
"integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -455,14 +455,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz",
|
||||||
"integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==",
|
"integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.58.1",
|
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||||
"@typescript-eslint/utils": "8.58.1",
|
"@typescript-eslint/utils": "8.59.0",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"ts-api-utils": "^2.5.0"
|
"ts-api-utils": "^2.5.0"
|
||||||
},
|
},
|
||||||
@@ -491,9 +491,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz",
|
||||||
"integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==",
|
"integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -504,15 +504,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz",
|
||||||
"integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==",
|
"integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.58.1",
|
"@typescript-eslint/project-service": "8.59.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.58.1",
|
"@typescript-eslint/tsconfig-utils": "8.59.0",
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.58.1",
|
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"minimatch": "^10.2.2",
|
"minimatch": "^10.2.2",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.7.3",
|
||||||
@@ -579,15 +579,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz",
|
||||||
"integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==",
|
"integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.9.1",
|
"@eslint-community/eslint-utils": "^4.9.1",
|
||||||
"@typescript-eslint/scope-manager": "8.58.1",
|
"@typescript-eslint/scope-manager": "8.59.0",
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.58.1"
|
"@typescript-eslint/typescript-estree": "8.59.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -602,12 +602,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz",
|
||||||
"integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==",
|
"integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"eslint-visitor-keys": "^5.0.0"
|
"eslint-visitor-keys": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4408,9 +4408,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -4887,16 +4887,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/eslint-plugin": {
|
"@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz",
|
||||||
"integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
|
"integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@eslint-community/regexpp": "^4.12.2",
|
"@eslint-community/regexpp": "^4.12.2",
|
||||||
"@typescript-eslint/scope-manager": "8.58.1",
|
"@typescript-eslint/scope-manager": "8.59.0",
|
||||||
"@typescript-eslint/type-utils": "8.58.1",
|
"@typescript-eslint/type-utils": "8.59.0",
|
||||||
"@typescript-eslint/utils": "8.58.1",
|
"@typescript-eslint/utils": "8.59.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.58.1",
|
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"ts-api-utils": "^2.5.0"
|
"ts-api-utils": "^2.5.0"
|
||||||
@@ -4912,59 +4912,61 @@
|
|||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||||
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
|
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/parser": {
|
"@typescript-eslint/parser": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz",
|
||||||
"integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
|
"integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/scope-manager": "8.58.1",
|
"@typescript-eslint/scope-manager": "8.59.0",
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.58.1",
|
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.58.1",
|
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||||
"debug": "^4.4.3"
|
"debug": "^4.4.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/project-service": {
|
"@typescript-eslint/project-service": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz",
|
||||||
"integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==",
|
"integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.58.1",
|
"@typescript-eslint/tsconfig-utils": "^8.59.0",
|
||||||
"@typescript-eslint/types": "^8.58.1",
|
"@typescript-eslint/types": "^8.59.0",
|
||||||
"debug": "^4.4.3"
|
"debug": "^4.4.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/scope-manager": {
|
"@typescript-eslint/scope-manager": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz",
|
||||||
"integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==",
|
"integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.58.1"
|
"@typescript-eslint/visitor-keys": "8.59.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/tsconfig-utils": {
|
"@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz",
|
||||||
"integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==",
|
"integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/type-utils": {
|
"@typescript-eslint/type-utils": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz",
|
||||||
"integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==",
|
"integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.58.1",
|
"@typescript-eslint/typescript-estree": "8.59.0",
|
||||||
"@typescript-eslint/utils": "8.58.1",
|
"@typescript-eslint/utils": "8.59.0",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"ts-api-utils": "^2.5.0"
|
"ts-api-utils": "^2.5.0"
|
||||||
},
|
},
|
||||||
@@ -4973,26 +4975,27 @@
|
|||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||||
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
|
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/types": {
|
"@typescript-eslint/types": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz",
|
||||||
"integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==",
|
"integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@typescript-eslint/typescript-estree": {
|
"@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz",
|
||||||
"integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==",
|
"integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/project-service": "8.58.1",
|
"@typescript-eslint/project-service": "8.59.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.58.1",
|
"@typescript-eslint/tsconfig-utils": "8.59.0",
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.58.1",
|
"@typescript-eslint/visitor-keys": "8.59.0",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"minimatch": "^10.2.2",
|
"minimatch": "^10.2.2",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.7.3",
|
||||||
@@ -5028,29 +5031,30 @@
|
|||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||||
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
|
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/utils": {
|
"@typescript-eslint/utils": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz",
|
||||||
"integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==",
|
"integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@eslint-community/eslint-utils": "^4.9.1",
|
"@eslint-community/eslint-utils": "^4.9.1",
|
||||||
"@typescript-eslint/scope-manager": "8.58.1",
|
"@typescript-eslint/scope-manager": "8.59.0",
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.58.1"
|
"@typescript-eslint/typescript-estree": "8.59.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/visitor-keys": {
|
"@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.58.1",
|
"version": "8.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz",
|
||||||
"integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==",
|
"integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.59.0",
|
||||||
"eslint-visitor-keys": "^5.0.0"
|
"eslint-visitor-keys": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5084,7 +5088,8 @@
|
|||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||||
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"ajv": {
|
"ajv": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
@@ -5638,7 +5643,8 @@
|
|||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
|
||||||
"integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
|
"integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"eslint-import-resolver-node": {
|
"eslint-import-resolver-node": {
|
||||||
"version": "0.3.9",
|
"version": "0.3.9",
|
||||||
@@ -5872,7 +5878,8 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz",
|
||||||
"integrity": "sha512-3G3UetST6rdqhqW9SfcfzNYMpQXS7wNkJvp6dsXnjzGiku6Iu5hl3B0kmk6lIcFPwYjhQIY+tXVRtK9TlGT7RA==",
|
"integrity": "sha512-3G3UetST6rdqhqW9SfcfzNYMpQXS7wNkJvp6dsXnjzGiku6Iu5hl3B0kmk6lIcFPwYjhQIY+tXVRtK9TlGT7RA==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"eslint-plugin-import": {
|
"eslint-plugin-import": {
|
||||||
"version": "2.28.1",
|
"version": "2.28.1",
|
||||||
@@ -7507,7 +7514,8 @@
|
|||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"picomatch": {
|
"picomatch": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
@@ -7536,7 +7544,8 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
|
||||||
"integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
|
"integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"tsconfig-paths": {
|
"tsconfig-paths": {
|
||||||
"version": "3.14.2",
|
"version": "3.14.2",
|
||||||
@@ -7624,9 +7633,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"unbox-primitive": {
|
"unbox-primitive": {
|
||||||
|
|||||||
@@ -37,8 +37,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
"@stylistic/eslint-plugin": "^5.10.0",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.58.1",
|
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
||||||
"@typescript-eslint/parser": "^8.58.1",
|
"@typescript-eslint/parser": "^8.59.0",
|
||||||
"@vercel/ncc": "^0.38.3",
|
"@vercel/ncc": "^0.38.3",
|
||||||
"eslint": "^8.47.0",
|
"eslint": "^8.47.0",
|
||||||
"eslint-plugin-github": "^4.10.2",
|
"eslint-plugin-github": "^4.10.2",
|
||||||
@@ -46,6 +46,6 @@
|
|||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.4.0",
|
"lint-staged": "^16.4.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"typescript": "^6.0.2"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
|
|||||||
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
|
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
|
||||||
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
|
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
|
||||||
NODE20_VERSION="20.20.2"
|
NODE20_VERSION="20.20.2"
|
||||||
NODE24_VERSION="24.14.1"
|
NODE24_VERSION="24.15.0"
|
||||||
|
|
||||||
get_abs_path() {
|
get_abs_path() {
|
||||||
# exploits the fact that pwd will print abs path when no args
|
# exploits the fact that pwd will print abs path when no args
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
|
||||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.3" />
|
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
|
|
||||||
// Dev Tunnel relay host for remote debugging
|
// Dev Tunnel relay host for remote debugging
|
||||||
private TunnelRelayTunnelHost _tunnelRelayHost;
|
private TunnelRelayTunnelHost _tunnelRelayHost;
|
||||||
|
private IWebSocketDapBridge _webSocketBridge;
|
||||||
|
|
||||||
// Cancellation source for the connection loop, cancelled in StopAsync
|
// Cancellation source for the connection loop, cancelled in StopAsync
|
||||||
// so AcceptTcpClientAsync unblocks cleanly without relying on listener disposal.
|
// so AcceptTcpClientAsync unblocks cleanly without relying on listener disposal.
|
||||||
@@ -74,6 +75,10 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
// When true, skip tunnel relay startup (unit tests only)
|
// When true, skip tunnel relay startup (unit tests only)
|
||||||
internal bool SkipTunnelRelay { get; set; }
|
internal bool SkipTunnelRelay { get; set; }
|
||||||
|
|
||||||
|
// When true, skip the public websocket bridge and expose the raw DAP
|
||||||
|
// listener directly on the configured tunnel port (unit tests only).
|
||||||
|
internal bool SkipWebSocketBridge { get; set; }
|
||||||
|
|
||||||
// Synchronization for step execution
|
// Synchronization for step execution
|
||||||
private TaskCompletionSource<DapCommand> _commandTcs;
|
private TaskCompletionSource<DapCommand> _commandTcs;
|
||||||
private readonly object _stateLock = new object();
|
private readonly object _stateLock = new object();
|
||||||
@@ -108,6 +113,7 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
_state == DapSessionState.Running;
|
_state == DapSessionState.Running;
|
||||||
|
|
||||||
internal DapSessionState State => _state;
|
internal DapSessionState State => _state;
|
||||||
|
internal int InternalDapPort => (_listener?.LocalEndpoint as IPEndPoint)?.Port ?? 0;
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
public override void Initialize(IHostContext hostContext)
|
||||||
{
|
{
|
||||||
@@ -133,9 +139,19 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
_jobContext = jobContext;
|
_jobContext = jobContext;
|
||||||
_readyTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
_readyTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
_listener = new TcpListener(IPAddress.Loopback, debuggerConfig.Tunnel.Port);
|
var dapPort = SkipWebSocketBridge ? debuggerConfig.Tunnel.Port : 0;
|
||||||
|
_listener = new TcpListener(IPAddress.Loopback, dapPort);
|
||||||
_listener.Start();
|
_listener.Start();
|
||||||
Trace.Info($"DAP debugger listening on {_listener.LocalEndpoint}");
|
if (SkipWebSocketBridge)
|
||||||
|
{
|
||||||
|
Trace.Info($"DAP debugger listening on {_listener.LocalEndpoint}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Info($"Internal DAP debugger listening on {_listener.LocalEndpoint}");
|
||||||
|
_webSocketBridge = HostContext.CreateService<IWebSocketDapBridge>();
|
||||||
|
_webSocketBridge.Start(debuggerConfig.Tunnel.Port, InternalDapPort);
|
||||||
|
}
|
||||||
|
|
||||||
// Start Dev Tunnel relay so remote clients reach the local DAP port.
|
// Start Dev Tunnel relay so remote clients reach the local DAP port.
|
||||||
// The relay is torn down explicitly in StopAsync (after the DAP session
|
// The relay is torn down explicitly in StopAsync (after the DAP session
|
||||||
@@ -274,6 +290,25 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
_tunnelRelayHost = null;
|
_tunnelRelayHost = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_webSocketBridge != null)
|
||||||
|
{
|
||||||
|
Trace.Info("Stopping WebSocket DAP bridge");
|
||||||
|
var shutdownTask = _webSocketBridge.ShutdownAsync();
|
||||||
|
if (await Task.WhenAny(shutdownTask, Task.Delay(5_000)) != shutdownTask)
|
||||||
|
{
|
||||||
|
Trace.Warning("WebSocket DAP bridge shutdown timed out after 5s");
|
||||||
|
_ = shutdownTask.ContinueWith(
|
||||||
|
t => Trace.Error($"WebSocket DAP bridge shutdown faulted: {t.Exception?.GetBaseException().Message}"),
|
||||||
|
TaskContinuationOptions.OnlyOnFaulted);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Info("WebSocket DAP bridge stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
_webSocketBridge = null;
|
||||||
|
}
|
||||||
|
|
||||||
CleanupConnection();
|
CleanupConnection();
|
||||||
|
|
||||||
// Cancel the connection loop first so AcceptTcpClientAsync unblocks
|
// Cancel the connection loop first so AcceptTcpClientAsync unblocks
|
||||||
@@ -315,6 +350,7 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
_connectionLoopTask = null;
|
_connectionLoopTask = null;
|
||||||
_loopCts?.Dispose();
|
_loopCts?.Dispose();
|
||||||
_loopCts = null;
|
_loopCts = null;
|
||||||
|
_webSocketBridge = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task OnStepStartingAsync(IStep step)
|
public async Task OnStepStartingAsync(IStep step)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.Runner.Common;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Worker.Dap
|
||||||
|
{
|
||||||
|
[ServiceLocator(Default = typeof(WebSocketDapBridge))]
|
||||||
|
public interface IWebSocketDapBridge : IRunnerService
|
||||||
|
{
|
||||||
|
void Start(int listenPort, int targetPort);
|
||||||
|
Task ShutdownAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,839 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.Runner.Common;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Worker.Dap
|
||||||
|
{
|
||||||
|
internal sealed class WebSocketDapBridge : RunnerService, IWebSocketDapBridge
|
||||||
|
{
|
||||||
|
internal enum IncomingStreamPrefixKind
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
HttpWebSocketUpgrade,
|
||||||
|
PreUpgradedWebSocket,
|
||||||
|
WebSocketReservedBits,
|
||||||
|
Http2Preface,
|
||||||
|
TlsClientHello,
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int _bufferSize = 32 * 1024;
|
||||||
|
private const int _maxHeaderLineLength = 8 * 1024;
|
||||||
|
private const int _defaultMaxInboundMessageSize = 10 * 1024 * 1024; // 10 MB
|
||||||
|
private static readonly TimeSpan _keepAliveInterval = TimeSpan.FromSeconds(30);
|
||||||
|
private static readonly TimeSpan _closeTimeout = TimeSpan.FromSeconds(5);
|
||||||
|
private static readonly TimeSpan _handshakeTimeout = TimeSpan.FromSeconds(10);
|
||||||
|
private const string _webSocketAcceptMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
|
private const int _maxHeaderCount = 64;
|
||||||
|
private static readonly byte[] _headerEndMarker = new byte[] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
|
||||||
|
|
||||||
|
private int _listenPort;
|
||||||
|
private int _targetPort;
|
||||||
|
|
||||||
|
private TcpListener _listener;
|
||||||
|
private CancellationTokenSource _loopCts;
|
||||||
|
private Task _acceptLoopTask;
|
||||||
|
|
||||||
|
public int MaxInboundMessageSize { get; set; } = _defaultMaxInboundMessageSize;
|
||||||
|
|
||||||
|
internal int ListenPort => (_listener?.LocalEndpoint as IPEndPoint)?.Port ?? 0;
|
||||||
|
|
||||||
|
public void Start(int listenPort, int targetPort)
|
||||||
|
{
|
||||||
|
if (_listener != null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("WebSocket DAP bridge already started.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_listenPort = listenPort;
|
||||||
|
_targetPort = targetPort;
|
||||||
|
|
||||||
|
_listener = new TcpListener(IPAddress.Loopback, _listenPort);
|
||||||
|
_listener.Start();
|
||||||
|
_loopCts = new CancellationTokenSource();
|
||||||
|
_acceptLoopTask = AcceptLoopAsync(_loopCts.Token);
|
||||||
|
|
||||||
|
Trace.Info($"WebSocket DAP bridge listening on {_listener.LocalEndpoint} -> 127.0.0.1:{_targetPort}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ShutdownAsync()
|
||||||
|
{
|
||||||
|
_loopCts?.Cancel();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_listener?.Stop();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Warning($"Error stopping listener during shutdown ({ex.GetType().Name})");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_acceptLoopTask != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _acceptLoopTask;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// expected on shutdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_loopCts?.Dispose();
|
||||||
|
_loopCts = null;
|
||||||
|
_listener = null;
|
||||||
|
_acceptLoopTask = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AcceptLoopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
TcpClient client = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
client = await _listener.AcceptTcpClientAsync(cancellationToken);
|
||||||
|
client.NoDelay = true;
|
||||||
|
await HandleClientAsync(client, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
client?.Dispose();
|
||||||
|
Trace.Error($"WebSocket DAP bridge connection error");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
client?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Info("WebSocket DAP bridge accept loop ended");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleClientAsync(TcpClient incomingClient, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using (var incomingStream = incomingClient.GetStream())
|
||||||
|
{
|
||||||
|
Trace.Info($"WebSocket DAP bridge accepted client {incomingClient.Client.RemoteEndPoint}");
|
||||||
|
|
||||||
|
WebSocket webSocket;
|
||||||
|
using (var handshakeCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||||
|
{
|
||||||
|
handshakeCts.CancelAfter(_handshakeTimeout);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
webSocket = await AcceptWebSocketAsync(incomingStream, handshakeCts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Trace.Warning("WebSocket handshake timed out");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (webSocket == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (webSocket)
|
||||||
|
using (var dapClient = new TcpClient())
|
||||||
|
{
|
||||||
|
dapClient.NoDelay = true;
|
||||||
|
await dapClient.ConnectAsync(IPAddress.Loopback, _targetPort, cancellationToken);
|
||||||
|
|
||||||
|
using (var dapStream = dapClient.GetStream())
|
||||||
|
using (var sessionCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||||
|
{
|
||||||
|
var proxyToken = sessionCts.Token;
|
||||||
|
var wsToTcpTask = PumpWebSocketToTcpAsync(webSocket, dapStream, proxyToken);
|
||||||
|
var tcpToWsTask = PumpTcpToWebSocketAsync(dapStream, webSocket, proxyToken);
|
||||||
|
|
||||||
|
await Task.WhenAny(wsToTcpTask, tcpToWsTask);
|
||||||
|
sessionCts.Cancel();
|
||||||
|
|
||||||
|
await CloseWebSocketAsync(webSocket);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.WhenAll(wsToTcpTask, tcpToWsTask);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (proxyToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// expected during shutdown
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Warning($"DAP protocol error: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<WebSocket> AcceptWebSocketAsync(NetworkStream stream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var initialBytes = await ReadInitialBytesAsync(stream, cancellationToken);
|
||||||
|
if (initialBytes == null || initialBytes.Length == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefixKind = ClassifyIncomingStreamPrefix(initialBytes);
|
||||||
|
if (prefixKind == IncomingStreamPrefixKind.PreUpgradedWebSocket)
|
||||||
|
{
|
||||||
|
Trace.Info($"Treating incoming tunnel stream as an already-upgraded websocket connection ({DescribeInitialBytes(initialBytes)})");
|
||||||
|
return WebSocket.CreateFromStream(
|
||||||
|
new ReplayableStream(stream, initialBytes),
|
||||||
|
isServer: true,
|
||||||
|
subProtocol: null,
|
||||||
|
keepAliveInterval: _keepAliveInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefixKind != IncomingStreamPrefixKind.HttpWebSocketUpgrade)
|
||||||
|
{
|
||||||
|
Trace.Warning($"Unsupported debugger tunnel stream prefix ({prefixKind}): {DescribeInitialBytes(initialBytes)}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handshakeStream = new ReplayableStream(stream, initialBytes);
|
||||||
|
var requestLine = await ReadLineAsync(handshakeStream, cancellationToken);
|
||||||
|
if (string.IsNullOrEmpty(requestLine))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (headers.Count >= _maxHeaderCount)
|
||||||
|
{
|
||||||
|
Trace.Warning($"Rejected WebSocket request with too many headers (>{_maxHeaderCount})");
|
||||||
|
await WriteHttpErrorAsync(stream, HttpStatusCode.BadRequest, "Too many headers.", cancellationToken);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var line = await ReadLineAsync(handshakeStream, cancellationToken);
|
||||||
|
if (line == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.Length == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var separatorIndex = line.IndexOf(':');
|
||||||
|
if (separatorIndex <= 0)
|
||||||
|
{
|
||||||
|
await WriteHttpErrorAsync(stream, HttpStatusCode.BadRequest, "Invalid HTTP header.", cancellationToken);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerName = line.Substring(0, separatorIndex).Trim();
|
||||||
|
var headerValue = line.Substring(separatorIndex + 1).Trim();
|
||||||
|
|
||||||
|
if (headers.TryGetValue(headerName, out var existingValue))
|
||||||
|
{
|
||||||
|
headers[headerName] = $"{existingValue}, {headerValue}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
headers[headerName] = headerValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsValidWebSocketRequest(requestLine, headers))
|
||||||
|
{
|
||||||
|
var method = requestLine.Split(' ')[0];
|
||||||
|
Trace.Info($"Rejected non-websocket request (method={method})");
|
||||||
|
await WriteHttpErrorAsync(stream, HttpStatusCode.BadRequest, "Expected a websocket upgrade request.", cancellationToken);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!headers.TryGetValue("Sec-WebSocket-Version", out var webSocketVersion) ||
|
||||||
|
!string.Equals(webSocketVersion.Trim(), "13", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Trace.Warning("Rejected WebSocket request with unsupported version");
|
||||||
|
await WriteHttpErrorAsync(stream, (HttpStatusCode)426, "Unsupported WebSocket version. Expected: 13.", cancellationToken);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var webSocketKey = headers["Sec-WebSocket-Key"];
|
||||||
|
if (!IsValidWebSocketKey(webSocketKey))
|
||||||
|
{
|
||||||
|
Trace.Warning("Rejected WebSocket request with invalid Sec-WebSocket-Key");
|
||||||
|
await WriteHttpErrorAsync(stream, HttpStatusCode.BadRequest, "Invalid Sec-WebSocket-Key.", cancellationToken);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var acceptValue = ComputeAcceptValue(webSocketKey);
|
||||||
|
var responseBytes = Encoding.ASCII.GetBytes(
|
||||||
|
"HTTP/1.1 101 Switching Protocols\r\n" +
|
||||||
|
"Connection: Upgrade\r\n" +
|
||||||
|
"Upgrade: websocket\r\n" +
|
||||||
|
$"Sec-WebSocket-Accept: {acceptValue}\r\n" +
|
||||||
|
"\r\n");
|
||||||
|
|
||||||
|
await handshakeStream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken);
|
||||||
|
await handshakeStream.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
|
Trace.Info("WebSocket DAP bridge completed websocket handshake");
|
||||||
|
return WebSocket.CreateFromStream(handshakeStream, isServer: true, subProtocol: null, keepAliveInterval: _keepAliveInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PumpWebSocketToTcpAsync(WebSocket source, NetworkStream destination, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var buffer = new byte[_bufferSize];
|
||||||
|
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
using (var messageStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
WebSocketReceiveResult result;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
result = await source.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.MessageType != WebSocketMessageType.Binary &&
|
||||||
|
result.MessageType != WebSocketMessageType.Text)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Count > 0)
|
||||||
|
{
|
||||||
|
if (messageStream.Length + result.Count > MaxInboundMessageSize)
|
||||||
|
{
|
||||||
|
Trace.Warning($"WebSocket message exceeds maximum allowed size of {MaxInboundMessageSize} bytes, closing connection");
|
||||||
|
await source.CloseAsync(
|
||||||
|
WebSocketCloseStatus.MessageTooBig,
|
||||||
|
$"Message exceeds {MaxInboundMessageSize} byte limit",
|
||||||
|
CancellationToken.None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageStream.Write(buffer, 0, result.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!result.EndOfMessage && !cancellationToken.IsCancellationRequested);
|
||||||
|
|
||||||
|
if (result.MessageType != WebSocketMessageType.Binary &&
|
||||||
|
result.MessageType != WebSocketMessageType.Text)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageBytes = messageStream.ToArray();
|
||||||
|
if (messageBytes.Length == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentLengthHeader = Encoding.ASCII.GetBytes($"Content-Length: {messageBytes.Length}\r\n\r\n");
|
||||||
|
await destination.WriteAsync(contentLengthHeader, 0, contentLengthHeader.Length, cancellationToken);
|
||||||
|
await destination.WriteAsync(messageBytes, 0, messageBytes.Length, cancellationToken);
|
||||||
|
await destination.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task PumpTcpToWebSocketAsync(NetworkStream source, WebSocket destination, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var readBuffer = new byte[_bufferSize];
|
||||||
|
var dapBuffer = new List<byte>();
|
||||||
|
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var bytesRead = await source.ReadAsync(readBuffer, 0, readBuffer.Length, cancellationToken);
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
dapBuffer.AddRange(new ArraySegment<byte>(readBuffer, 0, bytesRead));
|
||||||
|
|
||||||
|
while (TryParseDapMessage(dapBuffer, out var messageBody))
|
||||||
|
{
|
||||||
|
await destination.SendAsync(
|
||||||
|
new ArraySegment<byte>(messageBody),
|
||||||
|
WebSocketMessageType.Text,
|
||||||
|
endOfMessage: true,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseDapMessage(List<byte> buffer, out byte[] messageBody)
|
||||||
|
{
|
||||||
|
messageBody = null;
|
||||||
|
|
||||||
|
var headerEndIndex = FindSequence(buffer, _headerEndMarker);
|
||||||
|
if (headerEndIndex == -1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerBytes = buffer.GetRange(0, headerEndIndex).ToArray();
|
||||||
|
var headerText = Encoding.ASCII.GetString(headerBytes);
|
||||||
|
|
||||||
|
var contentLength = -1;
|
||||||
|
foreach (var line in headerText.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var valueStart = line.IndexOf(':') + 1;
|
||||||
|
if (int.TryParse(line.Substring(valueStart).Trim(), out var parsedLength))
|
||||||
|
{
|
||||||
|
contentLength = parsedLength;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentLength < 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("DAP message missing or unparseable Content-Length header; tearing down session.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageStart = headerEndIndex + 4;
|
||||||
|
var messageEnd = messageStart + contentLength;
|
||||||
|
|
||||||
|
if (buffer.Count < messageEnd)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageBody = buffer.GetRange(messageStart, contentLength).ToArray();
|
||||||
|
buffer.RemoveRange(0, messageEnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindSequence(List<byte> buffer, byte[] sequence)
|
||||||
|
{
|
||||||
|
if (buffer.Count < sequence.Length)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i <= buffer.Count - sequence.Length; i++)
|
||||||
|
{
|
||||||
|
var match = true;
|
||||||
|
for (int j = 0; j < sequence.Length; j++)
|
||||||
|
{
|
||||||
|
if (buffer[i + j] != sequence[j])
|
||||||
|
{
|
||||||
|
match = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidWebSocketRequest(string requestLine, IDictionary<string, string> headers)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(requestLine))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestLineParts = requestLine.Split(' ');
|
||||||
|
if (requestLineParts.Length < 3 || !string.Equals(requestLineParts[0], "GET", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HeaderContainsToken(headers, "Connection", "Upgrade") &&
|
||||||
|
HeaderContainsToken(headers, "Upgrade", "websocket") &&
|
||||||
|
headers.ContainsKey("Sec-WebSocket-Key");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HeaderContainsToken(IDictionary<string, string> headers, string headerName, string expectedToken)
|
||||||
|
{
|
||||||
|
if (!headers.TryGetValue(headerName, out var headerValue) || string.IsNullOrWhiteSpace(headerValue))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerValue
|
||||||
|
.Split(',')
|
||||||
|
.Select(token => token.Trim())
|
||||||
|
.Any(token => string.Equals(token, expectedToken, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ComputeAcceptValue(string webSocketKey)
|
||||||
|
{
|
||||||
|
using (var sha1 = SHA1.Create())
|
||||||
|
{
|
||||||
|
var inputBytes = Encoding.ASCII.GetBytes($"{webSocketKey}{_webSocketAcceptMagic}");
|
||||||
|
var hashBytes = sha1.ComputeHash(inputBytes);
|
||||||
|
return Convert.ToBase64String(hashBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidWebSocketKey(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key) || key.IndexOfAny(new[] { '\r', '\n' }) >= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var decoded = Convert.FromBase64String(key);
|
||||||
|
return decoded.Length == 16;
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadLineAsync(Stream stream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var lineBuilder = new StringBuilder();
|
||||||
|
var buffer = new byte[1];
|
||||||
|
var previousWasCarriageReturn = false;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var bytesRead = await stream.ReadAsync(buffer, 0, 1, cancellationToken);
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
return lineBuilder.Length > 0 ? lineBuilder.ToString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentChar = (char)buffer[0];
|
||||||
|
if (currentChar == '\n' && previousWasCarriageReturn)
|
||||||
|
{
|
||||||
|
if (lineBuilder.Length > 0 && lineBuilder[lineBuilder.Length - 1] == '\r')
|
||||||
|
{
|
||||||
|
lineBuilder.Length--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lineBuilder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
previousWasCarriageReturn = currentChar == '\r';
|
||||||
|
lineBuilder.Append(currentChar);
|
||||||
|
|
||||||
|
if (lineBuilder.Length > _maxHeaderLineLength)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"HTTP header line exceeds maximum length of {_maxHeaderLineLength}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<byte[]> ReadInitialBytesAsync(NetworkStream stream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var buffer = new byte[4];
|
||||||
|
var totalRead = 0;
|
||||||
|
|
||||||
|
while (totalRead < buffer.Length)
|
||||||
|
{
|
||||||
|
var bytesRead = await stream.ReadAsync(buffer, totalRead, buffer.Length - totalRead, cancellationToken);
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalRead += bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalRead == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<byte>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalRead == buffer.Length)
|
||||||
|
{
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialBytes = new byte[totalRead];
|
||||||
|
Array.Copy(buffer, initialBytes, totalRead);
|
||||||
|
return initialBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IncomingStreamPrefixKind ClassifyIncomingStreamPrefix(byte[] initialBytes)
|
||||||
|
{
|
||||||
|
if (LooksLikeHttpUpgrade(initialBytes))
|
||||||
|
{
|
||||||
|
return IncomingStreamPrefixKind.HttpWebSocketUpgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LooksLikeHttp2Preface(initialBytes))
|
||||||
|
{
|
||||||
|
return IncomingStreamPrefixKind.Http2Preface;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LooksLikeTlsClientHello(initialBytes))
|
||||||
|
{
|
||||||
|
return IncomingStreamPrefixKind.TlsClientHello;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LooksLikeWebSocketFramePrefix(initialBytes, requireReservedBitsClear: false))
|
||||||
|
{
|
||||||
|
return HasReservedBitsSet(initialBytes[0])
|
||||||
|
? IncomingStreamPrefixKind.WebSocketReservedBits
|
||||||
|
: IncomingStreamPrefixKind.PreUpgradedWebSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IncomingStreamPrefixKind.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string DescribeInitialBytes(byte[] initialBytes)
|
||||||
|
{
|
||||||
|
if (initialBytes == null || initialBytes.Length == 0)
|
||||||
|
{
|
||||||
|
return "no bytes read";
|
||||||
|
}
|
||||||
|
|
||||||
|
var hex = BitConverter.ToString(initialBytes);
|
||||||
|
var ascii = new string(initialBytes.Select(value => value >= 32 && value <= 126 ? (char)value : '.').ToArray());
|
||||||
|
return $"hex={hex}, ascii=\"{ascii}\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeHttpUpgrade(byte[] initialBytes)
|
||||||
|
{
|
||||||
|
if (initialBytes == null || initialBytes.Length < 4)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialBytes[0] == (byte)'G' &&
|
||||||
|
initialBytes[1] == (byte)'E' &&
|
||||||
|
initialBytes[2] == (byte)'T' &&
|
||||||
|
initialBytes[3] == (byte)' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeHttp2Preface(byte[] initialBytes)
|
||||||
|
{
|
||||||
|
if (initialBytes == null || initialBytes.Length < 4)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialBytes[0] == (byte)'P' &&
|
||||||
|
initialBytes[1] == (byte)'R' &&
|
||||||
|
initialBytes[2] == (byte)'I' &&
|
||||||
|
initialBytes[3] == (byte)' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeTlsClientHello(byte[] initialBytes)
|
||||||
|
{
|
||||||
|
if (initialBytes == null || initialBytes.Length < 3)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialBytes[0] == 0x16 &&
|
||||||
|
initialBytes[1] == 0x03 &&
|
||||||
|
initialBytes[2] >= 0x00 &&
|
||||||
|
initialBytes[2] <= 0x04;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeWebSocketFramePrefix(byte[] initialBytes, bool requireReservedBitsClear)
|
||||||
|
{
|
||||||
|
if (initialBytes == null || initialBytes.Length < 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstByte = initialBytes[0];
|
||||||
|
var secondByte = initialBytes[1];
|
||||||
|
var opcode = firstByte & 0x0F;
|
||||||
|
var isMasked = (secondByte & 0x80) != 0;
|
||||||
|
|
||||||
|
if (!isMasked || !IsSupportedWebSocketOpcode(opcode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !requireReservedBitsClear || !HasReservedBitsSet(firstByte);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasReservedBitsSet(byte firstByte)
|
||||||
|
{
|
||||||
|
return (firstByte & 0x70) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSupportedWebSocketOpcode(int opcode)
|
||||||
|
{
|
||||||
|
switch (opcode)
|
||||||
|
{
|
||||||
|
case 0x0:
|
||||||
|
case 0x1:
|
||||||
|
case 0x2:
|
||||||
|
case 0x8:
|
||||||
|
case 0x9:
|
||||||
|
case 0xA:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task WriteHttpErrorAsync(
|
||||||
|
NetworkStream stream,
|
||||||
|
HttpStatusCode statusCode,
|
||||||
|
string message,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var bodyBytes = Encoding.UTF8.GetBytes(message);
|
||||||
|
var responseBytes = Encoding.ASCII.GetBytes(
|
||||||
|
$"HTTP/1.1 {(int)statusCode} {statusCode}\r\n" +
|
||||||
|
"Connection: close\r\n" +
|
||||||
|
"Content-Type: text/plain; charset=utf-8\r\n" +
|
||||||
|
$"Content-Length: {bodyBytes.Length}\r\n" +
|
||||||
|
"Sec-WebSocket-Version: 13\r\n" +
|
||||||
|
"\r\n");
|
||||||
|
|
||||||
|
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken);
|
||||||
|
await stream.WriteAsync(bodyBytes, 0, bodyBytes.Length, cancellationToken);
|
||||||
|
await stream.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task CloseWebSocketAsync(WebSocket webSocket)
|
||||||
|
{
|
||||||
|
if (webSocket == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webSocket.State != WebSocketState.Open &&
|
||||||
|
webSocket.State != WebSocketState.CloseReceived)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource(_closeTimeout);
|
||||||
|
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Graceful close timed out, abort the connection.
|
||||||
|
webSocket.Abort();
|
||||||
|
}
|
||||||
|
catch (WebSocketException)
|
||||||
|
{
|
||||||
|
// Peer already disconnected.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ReplayableStream : Stream
|
||||||
|
{
|
||||||
|
private readonly Stream _innerStream;
|
||||||
|
private readonly byte[] _prefixBytes;
|
||||||
|
private int _prefixOffset;
|
||||||
|
|
||||||
|
public ReplayableStream(Stream innerStream, byte[] prefixBytes)
|
||||||
|
{
|
||||||
|
_innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream));
|
||||||
|
_prefixBytes = prefixBytes ?? Array.Empty<byte>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanRead => _innerStream.CanRead;
|
||||||
|
public override bool CanSeek => false;
|
||||||
|
public override bool CanWrite => _innerStream.CanWrite;
|
||||||
|
public override long Length => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public override long Position
|
||||||
|
{
|
||||||
|
get => throw new NotSupportedException();
|
||||||
|
set => throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Flush() => _innerStream.Flush();
|
||||||
|
|
||||||
|
public override Task FlushAsync(CancellationToken cancellationToken) => _innerStream.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
if (TryReadPrefix(buffer, offset, count, out var bytesRead))
|
||||||
|
{
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _innerStream.Read(buffer, offset, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (TryReadPrefix(buffer, offset, count, out var bytesRead))
|
||||||
|
{
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _innerStream.ReadAsync(buffer, offset, count, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (_prefixOffset < _prefixBytes.Length)
|
||||||
|
{
|
||||||
|
var bytesToCopy = Math.Min(buffer.Length, _prefixBytes.Length - _prefixOffset);
|
||||||
|
new ReadOnlySpan<byte>(_prefixBytes, _prefixOffset, bytesToCopy).CopyTo(buffer.Span);
|
||||||
|
_prefixOffset += bytesToCopy;
|
||||||
|
return bytesToCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _innerStream.ReadAsync(buffer, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public override void SetLength(long value) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count);
|
||||||
|
|
||||||
|
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
|
||||||
|
_innerStream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||||
|
|
||||||
|
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) =>
|
||||||
|
_innerStream.WriteAsync(buffer, cancellationToken);
|
||||||
|
|
||||||
|
private bool TryReadPrefix(byte[] buffer, int offset, int count, out int bytesRead)
|
||||||
|
{
|
||||||
|
if (_prefixOffset >= _prefixBytes.Length)
|
||||||
|
{
|
||||||
|
bytesRead = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesRead = Math.Min(count, _prefixBytes.Length - _prefixOffset);
|
||||||
|
Array.Copy(_prefixBytes, _prefixOffset, buffer, offset, bytesRead);
|
||||||
|
_prefixOffset += bytesRead;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -900,10 +900,6 @@ namespace GitHub.Runner.Worker
|
|||||||
jobContext[pair.Key] = pair.Value;
|
jobContext[pair.Key] = pair.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive workflow_repository and workflow_file_path from workflow_ref
|
|
||||||
// if the server sent workflow_ref but not the decomposed fields
|
|
||||||
jobContext.DeriveWorkflowRefComponents();
|
|
||||||
ExpressionValues["job"] = jobContext;
|
ExpressionValues["job"] = jobContext;
|
||||||
|
|
||||||
Trace.Info("Initialize GitHub context");
|
Trace.Info("Initialize GitHub context");
|
||||||
|
|||||||
@@ -146,51 +146,5 @@ namespace GitHub.Runner.Worker
|
|||||||
this["workflow_file_path"] = value != null ? new StringContextData(value) : null;
|
this["workflow_file_path"] = value != null ? new StringContextData(value) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses a composite workflow_ref (e.g. "owner/repo/.github/workflows/file.yml@refs/heads/main")
|
|
||||||
/// and populates workflow_repository and workflow_file_path if they are not already set.
|
|
||||||
/// </summary>
|
|
||||||
public void DeriveWorkflowRefComponents()
|
|
||||||
{
|
|
||||||
var workflowRef = WorkflowRef;
|
|
||||||
if (string.IsNullOrEmpty(workflowRef))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format: owner/repo/.github/workflows/file.yml@ref
|
|
||||||
var atIndex = workflowRef.IndexOf('@');
|
|
||||||
var pathPart = atIndex >= 0 ? workflowRef.Substring(0, atIndex) : workflowRef;
|
|
||||||
|
|
||||||
// Split at /.github/workflows/ to correctly handle repos named ".github"
|
|
||||||
// e.g. "octo-org/.github/.github/workflows/ci.yml" → repo="octo-org/.github"
|
|
||||||
var marker = "/.github/workflows/";
|
|
||||||
var markerIndex = pathPart.IndexOf(marker);
|
|
||||||
if (markerIndex < 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var repo = pathPart.Substring(0, markerIndex);
|
|
||||||
var filePath = pathPart.Substring(markerIndex + 1); // skip leading '/'
|
|
||||||
|
|
||||||
// Validate repo is owner/repo format (must have at least one slash with non-empty segments)
|
|
||||||
var slashIndex = repo.IndexOf('/');
|
|
||||||
if (slashIndex <= 0 || slashIndex >= repo.Length - 1)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WorkflowRepository == null || WorkflowRepository == "")
|
|
||||||
{
|
|
||||||
WorkflowRepository = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WorkflowFilePath == null || WorkflowFilePath == "")
|
|
||||||
{
|
|
||||||
WorkflowFilePath = filePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.3" />
|
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.3" />
|
||||||
<PackageReference Include="System.Threading.Channels" Version="10.0.3" />
|
<PackageReference Include="System.Threading.Channels" Version="10.0.3" />
|
||||||
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
||||||
<PackageReference Include="Microsoft.DevTunnels.Connections" Version="1.3.16" />
|
<PackageReference Include="Microsoft.DevTunnels.Connections" Version="1.3.39" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
+2
-2
@@ -23,14 +23,14 @@
|
|||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
||||||
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
|
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.3" />
|
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.6" />
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
|
||||||
<PackageReference Include="Minimatch" Version="2.0.0" />
|
<PackageReference Include="Minimatch" Version="2.0.0" />
|
||||||
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
||||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||||
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
|
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
|
||||||
<PackageReference Include="System.Formats.Asn1" Version="10.0.3" />
|
<PackageReference Include="System.Formats.Asn1" Version="10.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace GitHub.Actions.WorkflowParser.Conversion
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var effectiveMax = explicitMax ?? CreatePermissionsFromPolicy(context, permissionsPolicy, includeIdToken: isTrusted, includeModels: context.GetFeatures().AllowModelsPermission);
|
var effectiveMax = explicitMax ?? CreatePermissionsFromPolicy(context, permissionsPolicy, includeIdToken: isTrusted, includeModels: context.GetFeatures().AllowModelsPermission, includeVulnerabilityAlerts: context.GetFeatures().AllowVulnerabilityAlertsPermission);
|
||||||
|
|
||||||
if (requested.ViolatesMaxPermissions(effectiveMax, out var permissionLevelViolations))
|
if (requested.ViolatesMaxPermissions(effectiveMax, out var permissionLevelViolations))
|
||||||
{
|
{
|
||||||
@@ -59,18 +59,19 @@ namespace GitHub.Actions.WorkflowParser.Conversion
|
|||||||
TemplateContext context,
|
TemplateContext context,
|
||||||
string permissionsPolicy,
|
string permissionsPolicy,
|
||||||
bool includeIdToken,
|
bool includeIdToken,
|
||||||
bool includeModels)
|
bool includeModels,
|
||||||
|
bool includeVulnerabilityAlerts)
|
||||||
{
|
{
|
||||||
switch (permissionsPolicy)
|
switch (permissionsPolicy)
|
||||||
{
|
{
|
||||||
case WorkflowConstants.PermissionsPolicy.LimitedRead:
|
case WorkflowConstants.PermissionsPolicy.LimitedRead:
|
||||||
return new Permissions(PermissionLevel.NoAccess, includeIdToken: false, includeAttestations: false, includeModels: false)
|
return new Permissions(PermissionLevel.NoAccess, includeIdToken: false, includeAttestations: false, includeModels: false, includeVulnerabilityAlerts: false)
|
||||||
{
|
{
|
||||||
Contents = PermissionLevel.Read,
|
Contents = PermissionLevel.Read,
|
||||||
Packages = PermissionLevel.Read,
|
Packages = PermissionLevel.Read,
|
||||||
};
|
};
|
||||||
case WorkflowConstants.PermissionsPolicy.Write:
|
case WorkflowConstants.PermissionsPolicy.Write:
|
||||||
return new Permissions(PermissionLevel.Write, includeIdToken: includeIdToken, includeAttestations: true, includeModels: includeModels);
|
return new Permissions(PermissionLevel.Write, includeIdToken: includeIdToken, includeAttestations: true, includeModels: includeModels, includeVulnerabilityAlerts: includeVulnerabilityAlerts);
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException($"Unexpected permission policy: '{permissionsPolicy}'");
|
throw new ArgumentException($"Unexpected permission policy: '{permissionsPolicy}'");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1877,7 +1877,7 @@ namespace GitHub.Actions.WorkflowParser.Conversion
|
|||||||
permissionsStr.AssertUnexpectedValue(permissionsStr.Value);
|
permissionsStr.AssertUnexpectedValue(permissionsStr.Value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return new Permissions(permissionLevel, includeIdToken: true, includeAttestations: true, includeModels: context.GetFeatures().AllowModelsPermission);
|
return new Permissions(permissionLevel, includeIdToken: true, includeAttestations: true, includeModels: context.GetFeatures().AllowModelsPermission, includeVulnerabilityAlerts: context.GetFeatures().AllowVulnerabilityAlertsPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
var mapping = token.AssertMapping("permissions");
|
var mapping = token.AssertMapping("permissions");
|
||||||
@@ -1957,6 +1957,23 @@ namespace GitHub.Actions.WorkflowParser.Conversion
|
|||||||
context.Error(key, $"The permission 'models' is not allowed");
|
context.Error(key, $"The permission 'models' is not allowed");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "vulnerability-alerts":
|
||||||
|
if (context.GetFeatures().AllowVulnerabilityAlertsPermission)
|
||||||
|
{
|
||||||
|
if (permissionLevel == PermissionLevel.Write)
|
||||||
|
{
|
||||||
|
permissions.VulnerabilityAlerts = PermissionLevel.Read;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
permissions.VulnerabilityAlerts = permissionLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Error(key, $"The permission 'vulnerability-alerts' is not allowed");
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ namespace GitHub.Actions.WorkflowParser
|
|||||||
SecurityEvents = copy.SecurityEvents;
|
SecurityEvents = copy.SecurityEvents;
|
||||||
IdToken = copy.IdToken;
|
IdToken = copy.IdToken;
|
||||||
Models = copy.Models;
|
Models = copy.Models;
|
||||||
|
VulnerabilityAlerts = copy.VulnerabilityAlerts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Permissions(
|
public Permissions(
|
||||||
@@ -61,6 +62,19 @@ namespace GitHub.Actions.WorkflowParser
|
|||||||
: PermissionLevel.NoAccess;
|
: PermissionLevel.NoAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Permissions(
|
||||||
|
PermissionLevel permissionLevel,
|
||||||
|
bool includeIdToken,
|
||||||
|
bool includeAttestations,
|
||||||
|
bool includeModels,
|
||||||
|
bool includeVulnerabilityAlerts)
|
||||||
|
: this(permissionLevel, includeIdToken, includeAttestations, includeModels)
|
||||||
|
{
|
||||||
|
VulnerabilityAlerts = includeVulnerabilityAlerts
|
||||||
|
? (permissionLevel == PermissionLevel.Write ? PermissionLevel.Read : permissionLevel)
|
||||||
|
: PermissionLevel.NoAccess;
|
||||||
|
}
|
||||||
|
|
||||||
private static KeyValuePair<string, (PermissionLevel, PermissionLevel)>[] ComparisonKeyMapping(Permissions left, Permissions right)
|
private static KeyValuePair<string, (PermissionLevel, PermissionLevel)>[] ComparisonKeyMapping(Permissions left, Permissions right)
|
||||||
{
|
{
|
||||||
return new[]
|
return new[]
|
||||||
@@ -81,6 +95,7 @@ namespace GitHub.Actions.WorkflowParser
|
|||||||
new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("security-events", (left.SecurityEvents, right.SecurityEvents)),
|
new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("security-events", (left.SecurityEvents, right.SecurityEvents)),
|
||||||
new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("id-token", (left.IdToken, right.IdToken)),
|
new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("id-token", (left.IdToken, right.IdToken)),
|
||||||
new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("models", (left.Models, right.Models)),
|
new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("models", (left.Models, right.Models)),
|
||||||
|
new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("vulnerability-alerts", (left.VulnerabilityAlerts, right.VulnerabilityAlerts)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +169,13 @@ namespace GitHub.Actions.WorkflowParser
|
|||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DataMember(Name = "vulnerability-alerts", EmitDefaultValue = false)]
|
||||||
|
public PermissionLevel VulnerabilityAlerts
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
[DataMember(Name = "packages", EmitDefaultValue = false)]
|
[DataMember(Name = "packages", EmitDefaultValue = false)]
|
||||||
public PermissionLevel Packages
|
public PermissionLevel Packages
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ namespace GitHub.Actions.WorkflowParser
|
|||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public bool AllowModelsPermission { get; set; }
|
public bool AllowModelsPermission { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether users may use the "vulnerability-alerts" permission.
|
||||||
|
/// Used during parsing only.
|
||||||
|
/// </summary>
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public bool AllowVulnerabilityAlertsPermission { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the expression function fromJson performs strict JSON parsing.
|
/// Gets or sets a value indicating whether the expression function fromJson performs strict JSON parsing.
|
||||||
/// Used during evaluation only.
|
/// Used during evaluation only.
|
||||||
@@ -67,6 +74,7 @@ namespace GitHub.Actions.WorkflowParser
|
|||||||
Snapshot = false, // Default to false since this feature is still in an experimental phase
|
Snapshot = false, // Default to false since this feature is still in an experimental phase
|
||||||
StrictJsonParsing = false, // Default to false since this is temporary for telemetry purposes only
|
StrictJsonParsing = false, // Default to false since this is temporary for telemetry purposes only
|
||||||
AllowModelsPermission = false, // Default to false since we want this to be disabled for all non-production environments
|
AllowModelsPermission = false, // Default to false since we want this to be disabled for all non-production environments
|
||||||
|
AllowVulnerabilityAlertsPermission = false, // Default to false since we want this to be disabled for all non-production environments
|
||||||
AllowServiceContainerCommand = false, // Default to false since this feature is gated by actions_service_container_command
|
AllowServiceContainerCommand = false, // Default to false since this feature is gated by actions_service_container_command
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -496,8 +496,8 @@
|
|||||||
"check-suite-activity": {
|
"check-suite-activity": {
|
||||||
"description": "The types of check suite activity that trigger the workflow. Supported activity types: `completed`.",
|
"description": "The types of check suite activity that trigger the workflow. Supported activity types: `completed`.",
|
||||||
"one-of": [
|
"one-of": [
|
||||||
"check-suite-activity-type",
|
"check-suite-activity-type",
|
||||||
"check-suite-activity-types"
|
"check-suite-activity-types"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"check-suite-activity-types": {
|
"check-suite-activity-types": {
|
||||||
@@ -1865,11 +1865,15 @@
|
|||||||
},
|
},
|
||||||
"security-events": {
|
"security-events": {
|
||||||
"type": "permission-level-any",
|
"type": "permission-level-any",
|
||||||
"description": "Code scanning and Dependabot alerts."
|
"description": "Code scanning alerts."
|
||||||
},
|
},
|
||||||
"statuses": {
|
"statuses": {
|
||||||
"type": "permission-level-any",
|
"type": "permission-level-any",
|
||||||
"description": "Commit statuses."
|
"description": "Commit statuses."
|
||||||
|
},
|
||||||
|
"vulnerability-alerts": {
|
||||||
|
"type": "permission-level-read-or-no-access",
|
||||||
|
"description": "Dependabot alerts."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
using System.Net.WebSockets;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -19,13 +20,45 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
private const string TimeoutEnvironmentVariable = "ACTIONS_RUNNER_DAP_CONNECTION_TIMEOUT";
|
private const string TimeoutEnvironmentVariable = "ACTIONS_RUNNER_DAP_CONNECTION_TIMEOUT";
|
||||||
private const string TunnelConnectTimeoutVariable = "ACTIONS_RUNNER_DAP_TUNNEL_CONNECT_TIMEOUT_SECONDS";
|
private const string TunnelConnectTimeoutVariable = "ACTIONS_RUNNER_DAP_TUNNEL_CONNECT_TIMEOUT_SECONDS";
|
||||||
private DapDebugger _debugger;
|
private DapDebugger _debugger;
|
||||||
|
private TestWebSocketDapBridge _testWebSocketBridge;
|
||||||
|
|
||||||
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
|
private sealed class TestWebSocketDapBridge : RunnerService, IWebSocketDapBridge
|
||||||
|
{
|
||||||
|
private readonly WebSocketDapBridge _inner = new WebSocketDapBridge();
|
||||||
|
|
||||||
|
public int ListenPort => _inner.ListenPort;
|
||||||
|
|
||||||
|
public override void Initialize(IHostContext hostContext)
|
||||||
|
{
|
||||||
|
base.Initialize(hostContext);
|
||||||
|
_inner.Initialize(hostContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start(int listenPort, int targetPort)
|
||||||
|
{
|
||||||
|
_inner.Start(0, targetPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ShutdownAsync()
|
||||||
|
{
|
||||||
|
return _inner.ShutdownAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestHostContext CreateTestContext(bool enableWebSocketBridge = false, [CallerMemberName] string testName = "")
|
||||||
{
|
{
|
||||||
var hc = new TestHostContext(this, testName);
|
var hc = new TestHostContext(this, testName);
|
||||||
_debugger = new DapDebugger();
|
_debugger = new DapDebugger();
|
||||||
|
_testWebSocketBridge = null;
|
||||||
_debugger.Initialize(hc);
|
_debugger.Initialize(hc);
|
||||||
_debugger.SkipTunnelRelay = true;
|
_debugger.SkipTunnelRelay = true;
|
||||||
|
_debugger.SkipWebSocketBridge = !enableWebSocketBridge;
|
||||||
|
if (enableWebSocketBridge)
|
||||||
|
{
|
||||||
|
_testWebSocketBridge = new TestWebSocketDapBridge();
|
||||||
|
hc.EnqueueInstance<IWebSocketDapBridge>(_testWebSocketBridge);
|
||||||
|
}
|
||||||
|
|
||||||
return hc;
|
return hc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +104,14 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<ClientWebSocket> ConnectWebSocketClientAsync(int port)
|
||||||
|
{
|
||||||
|
var client = new ClientWebSocket();
|
||||||
|
client.Options.Proxy = null;
|
||||||
|
await client.ConnectAsync(new Uri($"ws://127.0.0.1:{port}/"), CancellationToken.None);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task SendRequestAsync(NetworkStream stream, Request request)
|
private static async Task SendRequestAsync(NetworkStream stream, Request request)
|
||||||
{
|
{
|
||||||
var json = JsonConvert.SerializeObject(request);
|
var json = JsonConvert.SerializeObject(request);
|
||||||
@@ -83,6 +124,14 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
await stream.FlushAsync();
|
await stream.FlushAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task SendRequestAsync(WebSocket client, Request request)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(request);
|
||||||
|
var body = Encoding.UTF8.GetBytes(json);
|
||||||
|
|
||||||
|
await client.SendAsync(new ArraySegment<byte>(body), WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads a single DAP-framed message from a stream with a timeout.
|
/// Reads a single DAP-framed message from a stream with a timeout.
|
||||||
/// Parses the Content-Length header, reads exactly that many bytes,
|
/// Parses the Content-Length header, reads exactly that many bytes,
|
||||||
@@ -141,6 +190,52 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
return Encoding.UTF8.GetString(body);
|
return Encoding.UTF8.GetString(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadWebSocketDataUntilAsync(WebSocket client, TimeSpan timeout, params string[] expectedFragments)
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource(timeout);
|
||||||
|
var buffer = new byte[4096];
|
||||||
|
var allMessages = new StringBuilder();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
using var messageStream = new MemoryStream();
|
||||||
|
WebSocketReceiveResult result;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
throw new EndOfStreamException("WebSocket closed before expected DAP messages were received.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Count > 0)
|
||||||
|
{
|
||||||
|
messageStream.Write(buffer, 0, result.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!result.EndOfMessage);
|
||||||
|
|
||||||
|
var messageText = Encoding.UTF8.GetString(messageStream.ToArray());
|
||||||
|
allMessages.Append(messageText);
|
||||||
|
|
||||||
|
var text = allMessages.ToString();
|
||||||
|
var containsAllFragments = true;
|
||||||
|
foreach (var fragment in expectedFragments)
|
||||||
|
{
|
||||||
|
if (!text.Contains(fragment, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
containsAllFragments = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsAllFragments)
|
||||||
|
{
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Mock<IExecutionContext> CreateJobContextWithTunnel(CancellationToken cancellationToken, ushort port, string jobName = null)
|
private static Mock<IExecutionContext> CreateJobContextWithTunnel(CancellationToken cancellationToken, ushort port, string jobName = null)
|
||||||
{
|
{
|
||||||
var tunnel = new GitHub.DistributedTask.Pipelines.DebuggerTunnelInfo
|
var tunnel = new GitHub.DistributedTask.Pipelines.DebuggerTunnelInfo
|
||||||
@@ -208,6 +303,84 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async Task StartAsyncWithWebSocketBridgeAcceptsInitializeOverWebSocket()
|
||||||
|
{
|
||||||
|
using (CreateTestContext(enableWebSocketBridge: true))
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||||
|
var jobContext = CreateJobContextWithTunnel(cts.Token, GetFreePort());
|
||||||
|
await _debugger.StartAsync(jobContext.Object);
|
||||||
|
|
||||||
|
var bridgePort = _testWebSocketBridge.ListenPort;
|
||||||
|
Assert.NotEqual(0, _debugger.InternalDapPort);
|
||||||
|
Assert.NotEqual(0, bridgePort);
|
||||||
|
Assert.NotEqual(bridgePort, _debugger.InternalDapPort);
|
||||||
|
|
||||||
|
using var client = await ConnectWebSocketClientAsync(bridgePort);
|
||||||
|
await SendRequestAsync(client, new Request
|
||||||
|
{
|
||||||
|
Seq = 1,
|
||||||
|
Type = "request",
|
||||||
|
Command = "initialize"
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await ReadWebSocketDataUntilAsync(
|
||||||
|
client,
|
||||||
|
TimeSpan.FromSeconds(5),
|
||||||
|
"\"type\":\"response\"",
|
||||||
|
"\"command\":\"initialize\"",
|
||||||
|
"\"event\":\"initialized\"");
|
||||||
|
|
||||||
|
Assert.Contains("\"success\":true", response);
|
||||||
|
await _debugger.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async Task StartAsyncWithWebSocketBridgeAcceptsPreUpgradedWebSocketStream()
|
||||||
|
{
|
||||||
|
using (CreateTestContext(enableWebSocketBridge: true))
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||||
|
var jobContext = CreateJobContextWithTunnel(cts.Token, GetFreePort());
|
||||||
|
await _debugger.StartAsync(jobContext.Object);
|
||||||
|
|
||||||
|
var bridgePort = _testWebSocketBridge.ListenPort;
|
||||||
|
Assert.NotEqual(0, _debugger.InternalDapPort);
|
||||||
|
Assert.NotEqual(0, bridgePort);
|
||||||
|
Assert.NotEqual(bridgePort, _debugger.InternalDapPort);
|
||||||
|
|
||||||
|
using var tcpClient = await ConnectClientAsync(bridgePort);
|
||||||
|
using var webSocket = WebSocket.CreateFromStream(
|
||||||
|
tcpClient.GetStream(),
|
||||||
|
isServer: false,
|
||||||
|
subProtocol: null,
|
||||||
|
keepAliveInterval: TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
|
await SendRequestAsync(webSocket, new Request
|
||||||
|
{
|
||||||
|
Seq = 1,
|
||||||
|
Type = "request",
|
||||||
|
Command = "initialize"
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await ReadWebSocketDataUntilAsync(
|
||||||
|
webSocket,
|
||||||
|
TimeSpan.FromSeconds(5),
|
||||||
|
"\"type\":\"response\"",
|
||||||
|
"\"command\":\"initialize\"",
|
||||||
|
"\"event\":\"initialized\"");
|
||||||
|
|
||||||
|
Assert.Contains("\"success\":true", response);
|
||||||
|
await _debugger.StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
|
|||||||
@@ -1256,22 +1256,22 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
var ec = new Runner.Worker.ExecutionContext();
|
var ec = new Runner.Worker.ExecutionContext();
|
||||||
ec.Initialize(hc);
|
ec.Initialize(hc);
|
||||||
|
|
||||||
// Arrange: Add workflow identity to the job context
|
// Arrange: Server sends all 4 workflow identity fields
|
||||||
var jobContext = new Pipelines.ContextData.DictionaryContextData();
|
var jobContext = new Pipelines.ContextData.DictionaryContextData();
|
||||||
jobContext["workflow_ref"] = new StringContextData("my-org/my-repo/.github/workflows/reusable.yml@refs/heads/main");
|
jobContext["workflow_ref"] = new StringContextData("my-org/my-repo/.github/workflows/reusable.yml@refs/heads/main");
|
||||||
jobContext["workflow_sha"] = new StringContextData("abc123def456");
|
jobContext["workflow_sha"] = new StringContextData("abc123def456");
|
||||||
|
jobContext["workflow_repository"] = new StringContextData("my-org/my-repo");
|
||||||
|
jobContext["workflow_file_path"] = new StringContextData(".github/workflows/reusable.yml");
|
||||||
jobRequest.ContextData["job"] = jobContext;
|
jobRequest.ContextData["job"] = jobContext;
|
||||||
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
ec.InitializeJob(jobRequest, CancellationToken.None);
|
ec.InitializeJob(jobRequest, CancellationToken.None);
|
||||||
|
|
||||||
// Assert: direct properties from server
|
// Assert: all properties hydrated from server
|
||||||
Assert.NotNull(ec.JobContext);
|
Assert.NotNull(ec.JobContext);
|
||||||
Assert.Equal("my-org/my-repo/.github/workflows/reusable.yml@refs/heads/main", ec.JobContext.WorkflowRef);
|
Assert.Equal("my-org/my-repo/.github/workflows/reusable.yml@refs/heads/main", ec.JobContext.WorkflowRef);
|
||||||
Assert.Equal("abc123def456", ec.JobContext.WorkflowSha);
|
Assert.Equal("abc123def456", ec.JobContext.WorkflowSha);
|
||||||
|
|
||||||
// Assert: derived properties
|
|
||||||
Assert.Equal("my-org/my-repo", ec.JobContext.WorkflowRepository);
|
Assert.Equal("my-org/my-repo", ec.JobContext.WorkflowRepository);
|
||||||
Assert.Equal(".github/workflows/reusable.yml", ec.JobContext.WorkflowFilePath);
|
Assert.Equal(".github/workflows/reusable.yml", ec.JobContext.WorkflowFilePath);
|
||||||
}
|
}
|
||||||
@@ -1310,41 +1310,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
[Trait("Level", "L0")]
|
|
||||||
[Trait("Category", "Worker")]
|
|
||||||
public void InitializeJob_WorkflowIdentityDerived_WhenServerSendsAllFields()
|
|
||||||
{
|
|
||||||
using (TestHostContext hc = CreateTestContext())
|
|
||||||
{
|
|
||||||
// Arrange: Server sends all 4 fields explicitly
|
|
||||||
var variables = new Dictionary<string, VariableValue>();
|
|
||||||
var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
|
|
||||||
var pagingLogger = new Moq.Mock<IPagingLogger>();
|
|
||||||
var jobServerQueue = new Moq.Mock<IJobServerQueue>();
|
|
||||||
hc.EnqueueInstance(pagingLogger.Object);
|
|
||||||
hc.SetSingleton(jobServerQueue.Object);
|
|
||||||
var ec = new Runner.Worker.ExecutionContext();
|
|
||||||
ec.Initialize(hc);
|
|
||||||
|
|
||||||
// Arrange: Server sends all fields, derivation should not overwrite
|
|
||||||
var jobContext = new Pipelines.ContextData.DictionaryContextData();
|
|
||||||
jobContext["workflow_ref"] = new StringContextData("my-org/my-repo/.github/workflows/reusable.yml@refs/heads/main");
|
|
||||||
jobContext["workflow_sha"] = new StringContextData("abc123def456");
|
|
||||||
jobContext["workflow_repository"] = new StringContextData("explicit-org/explicit-repo");
|
|
||||||
jobContext["workflow_file_path"] = new StringContextData(".github/workflows/explicit.yml");
|
|
||||||
jobRequest.ContextData["job"] = jobContext;
|
|
||||||
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
ec.InitializeJob(jobRequest, CancellationToken.None);
|
|
||||||
|
|
||||||
// Assert: explicit values should be preserved, not overwritten by derivation
|
|
||||||
Assert.Equal("explicit-org/explicit-repo", ec.JobContext.WorkflowRepository);
|
|
||||||
Assert.Equal(".github/workflows/explicit.yml", ec.JobContext.WorkflowFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ExpressionValuesAssertEqual(DictionaryContextData expect, DictionaryContextData actual)
|
private bool ExpressionValuesAssertEqual(DictionaryContextData expect, DictionaryContextData actual)
|
||||||
{
|
{
|
||||||
foreach (var key in expect.Keys.ToList())
|
foreach (var key in expect.Keys.ToList())
|
||||||
|
|||||||
@@ -138,120 +138,5 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
ctx.WorkflowFilePath = null;
|
ctx.WorkflowFilePath = null;
|
||||||
Assert.Null(ctx.WorkflowFilePath);
|
Assert.Null(ctx.WorkflowFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DeriveWorkflowRefComponents_PopulatesRepositoryAndFilePath()
|
|
||||||
{
|
|
||||||
var ctx = new JobContext();
|
|
||||||
ctx.WorkflowRef = "my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main";
|
|
||||||
ctx.DeriveWorkflowRefComponents();
|
|
||||||
|
|
||||||
Assert.Equal("my-org/my-repo", ctx.WorkflowRepository);
|
|
||||||
Assert.Equal(".github/workflows/deploy.yml", ctx.WorkflowFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DeriveWorkflowRefComponents_DoesNotOverwriteExistingValues()
|
|
||||||
{
|
|
||||||
var ctx = new JobContext();
|
|
||||||
ctx.WorkflowRef = "my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main";
|
|
||||||
ctx.WorkflowRepository = "explicit/override";
|
|
||||||
ctx.WorkflowFilePath = ".github/workflows/override.yml";
|
|
||||||
ctx.DeriveWorkflowRefComponents();
|
|
||||||
|
|
||||||
Assert.Equal("explicit/override", ctx.WorkflowRepository);
|
|
||||||
Assert.Equal(".github/workflows/override.yml", ctx.WorkflowFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DeriveWorkflowRefComponents_NoOp_WhenWorkflowRefIsNull()
|
|
||||||
{
|
|
||||||
var ctx = new JobContext();
|
|
||||||
ctx.DeriveWorkflowRefComponents();
|
|
||||||
|
|
||||||
Assert.Null(ctx.WorkflowRepository);
|
|
||||||
Assert.Null(ctx.WorkflowFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DeriveWorkflowRefComponents_NoOp_WhenRefHasNoGithubDir()
|
|
||||||
{
|
|
||||||
var ctx = new JobContext();
|
|
||||||
ctx.WorkflowRef = "some/path/without/github/dir@refs/heads/main";
|
|
||||||
ctx.DeriveWorkflowRefComponents();
|
|
||||||
|
|
||||||
Assert.Null(ctx.WorkflowRepository);
|
|
||||||
Assert.Null(ctx.WorkflowFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DeriveWorkflowRefComponents_HandlesRefWithoutAtSign()
|
|
||||||
{
|
|
||||||
var ctx = new JobContext();
|
|
||||||
ctx.WorkflowRef = "my-org/my-repo/.github/workflows/deploy.yml";
|
|
||||||
ctx.DeriveWorkflowRefComponents();
|
|
||||||
|
|
||||||
Assert.Equal("my-org/my-repo", ctx.WorkflowRepository);
|
|
||||||
Assert.Equal(".github/workflows/deploy.yml", ctx.WorkflowFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DeriveWorkflowRefComponents_HandlesDotGithubRepoName()
|
|
||||||
{
|
|
||||||
// Repos can be named ".github" — the marker must be /.github/workflows/ not /.github/
|
|
||||||
var ctx = new JobContext();
|
|
||||||
ctx.WorkflowRef = "octo-org/.github/.github/workflows/ci.yml@refs/heads/main";
|
|
||||||
ctx.DeriveWorkflowRefComponents();
|
|
||||||
|
|
||||||
Assert.Equal("octo-org/.github", ctx.WorkflowRepository);
|
|
||||||
Assert.Equal(".github/workflows/ci.yml", ctx.WorkflowFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DeriveWorkflowRefComponents_TreatsEmptyStringAsUnset()
|
|
||||||
{
|
|
||||||
var ctx = new JobContext();
|
|
||||||
ctx.WorkflowRef = "my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main";
|
|
||||||
ctx.WorkflowRepository = "";
|
|
||||||
ctx.WorkflowFilePath = "";
|
|
||||||
ctx.DeriveWorkflowRefComponents();
|
|
||||||
|
|
||||||
Assert.Equal("my-org/my-repo", ctx.WorkflowRepository);
|
|
||||||
Assert.Equal(".github/workflows/deploy.yml", ctx.WorkflowFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DeriveWorkflowRefComponents_HandlesPRMergeRef()
|
|
||||||
{
|
|
||||||
var ctx = new JobContext();
|
|
||||||
ctx.WorkflowRef = "my-org/my-repo/.github/workflows/ci.yml@refs/pull/42/merge";
|
|
||||||
ctx.DeriveWorkflowRefComponents();
|
|
||||||
|
|
||||||
Assert.Equal("my-org/my-repo", ctx.WorkflowRepository);
|
|
||||||
Assert.Equal(".github/workflows/ci.yml", ctx.WorkflowFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DeriveWorkflowRefComponents_HandlesTagRef()
|
|
||||||
{
|
|
||||||
var ctx = new JobContext();
|
|
||||||
ctx.WorkflowRef = "my-org/my-repo/.github/workflows/release.yml@refs/tags/v1.0.0";
|
|
||||||
ctx.DeriveWorkflowRefComponents();
|
|
||||||
|
|
||||||
Assert.Equal("my-org/my-repo", ctx.WorkflowRepository);
|
|
||||||
Assert.Equal(".github/workflows/release.yml", ctx.WorkflowFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void DeriveWorkflowRefComponents_RejectsInvalidRepoFormat()
|
|
||||||
{
|
|
||||||
// No owner/repo slash — should no-op
|
|
||||||
var ctx = new JobContext();
|
|
||||||
ctx.WorkflowRef = "noslash.github/workflows/ci.yml@refs/heads/main";
|
|
||||||
ctx.DeriveWorkflowRefComponents();
|
|
||||||
|
|
||||||
Assert.Null(ctx.WorkflowRepository);
|
|
||||||
Assert.Null(ctx.WorkflowFilePath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Worker.Dap;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Common.Tests.Worker
|
||||||
|
{
|
||||||
|
public sealed class WebSocketDapBridgeL0
|
||||||
|
{
|
||||||
|
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
|
||||||
|
{
|
||||||
|
return new TestHostContext(this, testName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<byte[]> ReadWebSocketMessageAsync(ClientWebSocket client, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource(timeout);
|
||||||
|
using var buffer = new MemoryStream();
|
||||||
|
var receiveBuffer = new byte[1024];
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var result = await client.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), cts.Token);
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
throw new EndOfStreamException("WebSocket closed unexpectedly.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Count > 0)
|
||||||
|
{
|
||||||
|
buffer.Write(receiveBuffer, 0, result.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.EndOfMessage)
|
||||||
|
{
|
||||||
|
return buffer.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async Task BridgeForwardsWebSocketFramesToTcpAndBack()
|
||||||
|
{
|
||||||
|
using var hc = CreateTestContext();
|
||||||
|
using var targetListener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
|
targetListener.Start();
|
||||||
|
|
||||||
|
var targetPort = ((IPEndPoint)targetListener.LocalEndpoint).Port;
|
||||||
|
|
||||||
|
var bridge = new WebSocketDapBridge();
|
||||||
|
bridge.Initialize(hc);
|
||||||
|
bridge.Start(0, targetPort);
|
||||||
|
var bridgePort = bridge.ListenPort;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var echoTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var targetClient = await targetListener.AcceptTcpClientAsync();
|
||||||
|
using var stream = targetClient.GetStream();
|
||||||
|
|
||||||
|
var headerBuilder = new StringBuilder();
|
||||||
|
var buffer = new byte[1];
|
||||||
|
var contentLength = -1;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var bytesRead = await stream.ReadAsync(buffer, 0, 1);
|
||||||
|
if (bytesRead == 0) break;
|
||||||
|
|
||||||
|
headerBuilder.Append((char)buffer[0]);
|
||||||
|
var headers = headerBuilder.ToString();
|
||||||
|
if (headers.EndsWith("\r\n\r\n", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
foreach (var line in headers.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
if (line.StartsWith("Content-Length: ", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
contentLength = int.Parse(line.Substring("Content-Length: ".Length).Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = new byte[contentLength];
|
||||||
|
var totalRead = 0;
|
||||||
|
while (totalRead < contentLength)
|
||||||
|
{
|
||||||
|
var bytesRead = await stream.ReadAsync(body, totalRead, contentLength - totalRead);
|
||||||
|
if (bytesRead == 0) break;
|
||||||
|
totalRead += bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
var header = $"Content-Length: {body.Length}\r\n\r\n";
|
||||||
|
var headerBytes = Encoding.ASCII.GetBytes(header);
|
||||||
|
await stream.WriteAsync(headerBytes, 0, headerBytes.Length);
|
||||||
|
await stream.WriteAsync(body, 0, body.Length);
|
||||||
|
await stream.FlushAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
using var client = new ClientWebSocket();
|
||||||
|
client.Options.Proxy = null;
|
||||||
|
await client.ConnectAsync(new Uri($"ws://127.0.0.1:{bridgePort}/"), CancellationToken.None);
|
||||||
|
|
||||||
|
var dapMessage = "{\"type\":\"request\",\"seq\":1,\"command\":\"initialize\"}";
|
||||||
|
var payload = Encoding.UTF8.GetBytes(dapMessage);
|
||||||
|
await client.SendAsync(new ArraySegment<byte>(payload), WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None);
|
||||||
|
|
||||||
|
var echoed = await ReadWebSocketMessageAsync(client, TimeSpan.FromSeconds(5));
|
||||||
|
Assert.Equal(payload, echoed);
|
||||||
|
|
||||||
|
await echoTask;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await bridge.ShutdownAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async Task BridgeRejectsNonWebSocketRequests()
|
||||||
|
{
|
||||||
|
using var hc = CreateTestContext();
|
||||||
|
|
||||||
|
var bridge = new WebSocketDapBridge();
|
||||||
|
bridge.Initialize(hc);
|
||||||
|
bridge.Start(0, 0);
|
||||||
|
var bridgePort = bridge.ListenPort;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var client = new TcpClient();
|
||||||
|
await client.ConnectAsync(IPAddress.Loopback, bridgePort);
|
||||||
|
using var stream = client.GetStream();
|
||||||
|
|
||||||
|
var request = Encoding.ASCII.GetBytes(
|
||||||
|
"GET / HTTP/1.1\r\n" +
|
||||||
|
"Host: localhost\r\n" +
|
||||||
|
"\r\n");
|
||||||
|
await stream.WriteAsync(request, 0, request.Length);
|
||||||
|
await stream.FlushAsync();
|
||||||
|
|
||||||
|
// Read until the server closes the connection (Connection: close).
|
||||||
|
// A single ReadAsync may return a partial response on some platforms.
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
var responseBuffer = new byte[1024];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length)) > 0)
|
||||||
|
{
|
||||||
|
ms.Write(responseBuffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = Encoding.ASCII.GetString(ms.ToArray());
|
||||||
|
|
||||||
|
Assert.Contains("400 BadRequest", response);
|
||||||
|
Assert.Contains("Expected a websocket upgrade request.", response);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await bridge.ShutdownAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
[InlineData(new byte[] { (byte)'G', (byte)'E', (byte)'T', (byte)' ' }, 1)]
|
||||||
|
[InlineData(new byte[] { 0x81, 0x85, 0x00, 0x00 }, 2)]
|
||||||
|
[InlineData(new byte[] { 0xC1, 0x85, 0x00, 0x00 }, 3)]
|
||||||
|
[InlineData(new byte[] { (byte)'P', (byte)'R', (byte)'I', (byte)' ' }, 4)]
|
||||||
|
[InlineData(new byte[] { 0x16, 0x03, 0x03, 0x01 }, 5)]
|
||||||
|
[InlineData(new byte[] { (byte)'B', (byte)'A', (byte)'D', (byte)'!' }, 0)]
|
||||||
|
public void ClassifyIncomingStreamPrefixDetectsExpectedProtocols(byte[] initialBytes, int expectedKind)
|
||||||
|
{
|
||||||
|
var actualKind = WebSocketDapBridge.ClassifyIncomingStreamPrefix(initialBytes);
|
||||||
|
Assert.Equal((WebSocketDapBridge.IncomingStreamPrefixKind)expectedKind, actualKind);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async Task BridgeRejectsOversizedWebSocketMessage()
|
||||||
|
{
|
||||||
|
using var hc = CreateTestContext();
|
||||||
|
using var targetListener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
|
targetListener.Start();
|
||||||
|
|
||||||
|
var targetPort = ((IPEndPoint)targetListener.LocalEndpoint).Port;
|
||||||
|
|
||||||
|
var bridge = new WebSocketDapBridge();
|
||||||
|
bridge.Initialize(hc);
|
||||||
|
bridge.MaxInboundMessageSize = 64; // artificially small limit for testing
|
||||||
|
bridge.Start(0, targetPort);
|
||||||
|
var bridgePort = bridge.ListenPort;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var client = new ClientWebSocket();
|
||||||
|
client.Options.Proxy = null;
|
||||||
|
await client.ConnectAsync(new Uri($"ws://127.0.0.1:{bridgePort}/"), CancellationToken.None);
|
||||||
|
|
||||||
|
// Send a message that exceeds the 64-byte limit
|
||||||
|
var oversizedPayload = new byte[128];
|
||||||
|
Array.Fill(oversizedPayload, (byte)'X');
|
||||||
|
await client.SendAsync(
|
||||||
|
new ArraySegment<byte>(oversizedPayload),
|
||||||
|
WebSocketMessageType.Text,
|
||||||
|
endOfMessage: true,
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
// The bridge should close the connection with MessageTooBig
|
||||||
|
var receiveBuffer = new byte[256];
|
||||||
|
using var receiveCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
|
var result = await client.ReceiveAsync(
|
||||||
|
new ArraySegment<byte>(receiveBuffer),
|
||||||
|
receiveCts.Token);
|
||||||
|
|
||||||
|
Assert.Equal(WebSocketMessageType.Close, result.MessageType);
|
||||||
|
Assert.Equal(WebSocketCloseStatus.MessageTooBig, client.CloseStatus);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await bridge.ShutdownAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async Task BridgeShutdownCompletesWhenPeerDoesNotCloseGracefully()
|
||||||
|
{
|
||||||
|
using var hc = CreateTestContext();
|
||||||
|
using var targetListener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
|
targetListener.Start();
|
||||||
|
|
||||||
|
var targetPort = ((IPEndPoint)targetListener.LocalEndpoint).Port;
|
||||||
|
|
||||||
|
var bridge = new WebSocketDapBridge();
|
||||||
|
bridge.Initialize(hc);
|
||||||
|
bridge.Start(0, targetPort);
|
||||||
|
var bridgePort = bridge.ListenPort;
|
||||||
|
|
||||||
|
// Connect a raw TCP client but never perform WebSocket close handshake
|
||||||
|
using var rawClient = new TcpClient();
|
||||||
|
await rawClient.ConnectAsync(IPAddress.Loopback, bridgePort);
|
||||||
|
|
||||||
|
// Shutdown should complete within a bounded time, not hang
|
||||||
|
var shutdownTask = bridge.ShutdownAsync();
|
||||||
|
var completed = await Task.WhenAny(shutdownTask, Task.Delay(TimeSpan.FromSeconds(15)));
|
||||||
|
Assert.True(completed == shutdownTask, "Bridge shutdown should complete within the timeout, not hang on a non-cooperative peer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -17,7 +17,7 @@ LAYOUT_DIR="$SCRIPT_DIR/../_layout"
|
|||||||
DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x"
|
DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x"
|
||||||
PACKAGE_DIR="$SCRIPT_DIR/../_package"
|
PACKAGE_DIR="$SCRIPT_DIR/../_package"
|
||||||
DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk"
|
DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk"
|
||||||
DOTNETSDK_VERSION="8.0.419"
|
DOTNETSDK_VERSION="8.0.420"
|
||||||
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
|
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
|
||||||
RUNNER_VERSION=$(cat runnerversion)
|
RUNNER_VERSION=$(cat runnerversion)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "8.0.419"
|
"version": "8.0.420"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.333.0
|
2.334.0
|
||||||
|
|||||||
Reference in New Issue
Block a user