mirror of
https://github.com/actions/runner.git
synced 2026-03-14 03:04:59 +08:00
Compare commits
5 Commits
users/tihu
...
rentziass/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
915e13c842 | ||
|
|
17b05ddaa4 | ||
|
|
9737dfadd5 | ||
|
|
cca15de3b3 | ||
|
|
8b1b23b5ce |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -78,7 +78,7 @@ jobs:
|
||||
# Upload runner package tar.gz/zip as artifact
|
||||
- name: Publish Artifact
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: runner-package-${{ matrix.runtime }}
|
||||
path: |
|
||||
@@ -111,10 +111,10 @@ jobs:
|
||||
core.setOutput('version', version);
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./images
|
||||
load: true
|
||||
|
||||
8
.github/workflows/docker-publish.yml
vendored
8
.github/workflows/docker-publish.yml
vendored
@@ -38,10 +38,10 @@ jobs:
|
||||
core.setOutput('version', runnerVersion);
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./images
|
||||
platforms: |
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }}
|
||||
|
||||
- name: Generate attestation
|
||||
uses: actions/attest-build-provenance@v4
|
||||
uses: actions/attest-build-provenance@v3
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.build-and-push.outputs.digest }}
|
||||
|
||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -118,7 +118,7 @@ jobs:
|
||||
# Upload runner package tar.gz/zip as artifact.
|
||||
- name: Publish Artifact
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: runner-packages-${{ matrix.runtime }}
|
||||
path: |
|
||||
@@ -309,10 +309,10 @@ jobs:
|
||||
core.setOutput('version', runnerVersion);
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -320,7 +320,7 @@ jobs:
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./images
|
||||
platforms: |
|
||||
@@ -339,7 +339,7 @@ jobs:
|
||||
org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }}
|
||||
|
||||
- name: Generate attestation
|
||||
uses: actions/attest-build-provenance@v4
|
||||
uses: actions/attest-build-provenance@v3
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.build-and-push.outputs.digest }}
|
||||
|
||||
@@ -5,8 +5,8 @@ ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG RUNNER_VERSION
|
||||
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
||||
ARG DOCKER_VERSION=29.3.0
|
||||
ARG BUILDX_VERSION=0.32.1
|
||||
ARG DOCKER_VERSION=29.2.0
|
||||
ARG BUILDX_VERSION=0.31.1
|
||||
|
||||
RUN apt update -y && apt install curl unzip -y
|
||||
|
||||
|
||||
443
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
443
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
@@ -12,9 +12,9 @@
|
||||
"@actions/glob": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@stylistic/eslint-plugin": "^5.9.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vercel/ncc": "^0.38.3",
|
||||
"eslint": "^8.47.0",
|
||||
@@ -93,9 +93,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/regexpp": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
|
||||
"integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
||||
@@ -228,9 +228,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz",
|
||||
"integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
|
||||
"version": "5.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.9.0.tgz",
|
||||
"integrity": "sha512-FqqSkvDMYJReydrMhlugc71M76yLLQWNfmGq+SIlLa7N3kHp8Qq8i2PyWrVNAfjOyOIY+xv9XaaYwvVW7vroMA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
@@ -321,19 +321,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
|
||||
"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz",
|
||||
"integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/type-utils": "8.54.0",
|
||||
"@typescript-eslint/utils": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
||||
"ignore": "^7.0.5",
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.47.0",
|
||||
"@typescript-eslint/type-utils": "8.47.0",
|
||||
"@typescript-eslint/utils": "8.47.0",
|
||||
"@typescript-eslint/visitor-keys": "8.47.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -343,7 +345,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^8.47.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -359,10 +361,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
@@ -371,16 +374,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz",
|
||||
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
||||
"debug": "^4.4.3"
|
||||
"@typescript-eslint/scope-manager": "8.47.0",
|
||||
"@typescript-eslint/types": "8.47.0",
|
||||
"@typescript-eslint/typescript-estree": "8.47.0",
|
||||
"@typescript-eslint/visitor-keys": "8.47.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -395,14 +399,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz",
|
||||
"integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz",
|
||||
"integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.54.0",
|
||||
"@typescript-eslint/types": "^8.54.0",
|
||||
"debug": "^4.4.3"
|
||||
"@typescript-eslint/tsconfig-utils": "^8.47.0",
|
||||
"@typescript-eslint/types": "^8.47.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -416,13 +421,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz",
|
||||
"integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz",
|
||||
"integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0"
|
||||
"@typescript-eslint/types": "8.47.0",
|
||||
"@typescript-eslint/visitor-keys": "8.47.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -433,10 +439,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz",
|
||||
"integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz",
|
||||
"integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -449,16 +456,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz",
|
||||
"integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz",
|
||||
"integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0",
|
||||
"@typescript-eslint/utils": "8.54.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
"@typescript-eslint/types": "8.47.0",
|
||||
"@typescript-eslint/typescript-estree": "8.47.0",
|
||||
"@typescript-eslint/utils": "8.47.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -473,10 +481,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
@@ -485,10 +494,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
|
||||
"integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz",
|
||||
"integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -498,20 +508,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz",
|
||||
"integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz",
|
||||
"integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.54.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^9.0.5",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
"@typescript-eslint/project-service": "8.47.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.47.0",
|
||||
"@typescript-eslint/types": "8.47.0",
|
||||
"@typescript-eslint/visitor-keys": "8.47.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -561,10 +573,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/ts-api-utils": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
@@ -573,15 +586,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz",
|
||||
"integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz",
|
||||
"integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0"
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.47.0",
|
||||
"@typescript-eslint/types": "8.47.0",
|
||||
"@typescript-eslint/typescript-estree": "8.47.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -596,12 +610,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz",
|
||||
"integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz",
|
||||
"integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/types": "8.47.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -617,6 +632,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -1134,10 +1150,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -4002,9 +4019,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"version": "7.6.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
|
||||
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -4302,51 +4319,6 @@
|
||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/titleize": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
|
||||
@@ -4784,9 +4756,9 @@
|
||||
}
|
||||
},
|
||||
"@eslint-community/regexpp": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
|
||||
"integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
|
||||
"dev": true
|
||||
},
|
||||
"@eslint/eslintrc": {
|
||||
@@ -4882,9 +4854,9 @@
|
||||
}
|
||||
},
|
||||
"@stylistic/eslint-plugin": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz",
|
||||
"integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
|
||||
"version": "5.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.9.0.tgz",
|
||||
"integrity": "sha512-FqqSkvDMYJReydrMhlugc71M76yLLQWNfmGq+SIlLa7N3kHp8Qq8i2PyWrVNAfjOyOIY+xv9XaaYwvVW7vroMA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
@@ -4942,19 +4914,20 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
|
||||
"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz",
|
||||
"integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/type-utils": "8.54.0",
|
||||
"@typescript-eslint/utils": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
||||
"ignore": "^7.0.5",
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.47.0",
|
||||
"@typescript-eslint/type-utils": "8.47.0",
|
||||
"@typescript-eslint/utils": "8.47.0",
|
||||
"@typescript-eslint/visitor-keys": "8.47.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ignore": {
|
||||
@@ -4964,98 +4937,99 @@
|
||||
"dev": true
|
||||
},
|
||||
"ts-api-utils": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz",
|
||||
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
||||
"debug": "^4.4.3"
|
||||
"@typescript-eslint/scope-manager": "8.47.0",
|
||||
"@typescript-eslint/types": "8.47.0",
|
||||
"@typescript-eslint/typescript-estree": "8.47.0",
|
||||
"@typescript-eslint/visitor-keys": "8.47.0",
|
||||
"debug": "^4.3.4"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/project-service": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz",
|
||||
"integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz",
|
||||
"integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.54.0",
|
||||
"@typescript-eslint/types": "^8.54.0",
|
||||
"debug": "^4.4.3"
|
||||
"@typescript-eslint/tsconfig-utils": "^8.47.0",
|
||||
"@typescript-eslint/types": "^8.47.0",
|
||||
"debug": "^4.3.4"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz",
|
||||
"integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz",
|
||||
"integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0"
|
||||
"@typescript-eslint/types": "8.47.0",
|
||||
"@typescript-eslint/visitor-keys": "8.47.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz",
|
||||
"integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz",
|
||||
"integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@typescript-eslint/type-utils": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz",
|
||||
"integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz",
|
||||
"integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0",
|
||||
"@typescript-eslint/utils": "8.54.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
"@typescript-eslint/types": "8.47.0",
|
||||
"@typescript-eslint/typescript-estree": "8.47.0",
|
||||
"@typescript-eslint/utils": "8.47.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ts-api-utils": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
|
||||
"integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz",
|
||||
"integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz",
|
||||
"integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz",
|
||||
"integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/project-service": "8.54.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^9.0.5",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
"@typescript-eslint/project-service": "8.47.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.47.0",
|
||||
"@typescript-eslint/types": "8.47.0",
|
||||
"@typescript-eslint/visitor-keys": "8.47.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"balanced-match": {
|
||||
@@ -5083,33 +5057,33 @@
|
||||
}
|
||||
},
|
||||
"ts-api-utils": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/utils": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz",
|
||||
"integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz",
|
||||
"integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0"
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.47.0",
|
||||
"@typescript-eslint/types": "8.47.0",
|
||||
"@typescript-eslint/typescript-estree": "8.47.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz",
|
||||
"integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==",
|
||||
"version": "8.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz",
|
||||
"integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/types": "8.47.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -5464,9 +5438,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -7415,9 +7389,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"version": "7.6.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
|
||||
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
|
||||
"dev": true
|
||||
},
|
||||
"shebang-command": {
|
||||
@@ -7613,31 +7587,6 @@
|
||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"titleize": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
|
||||
|
||||
@@ -35,9 +35,9 @@
|
||||
"@actions/glob": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@stylistic/eslint-plugin": "^5.9.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vercel/ncc": "^0.38.3",
|
||||
"eslint": "^8.47.0",
|
||||
|
||||
@@ -6,7 +6,7 @@ NODE_URL=https://nodejs.org/dist
|
||||
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.
|
||||
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
|
||||
NODE20_VERSION="20.20.1"
|
||||
NODE20_VERSION="20.20.0"
|
||||
NODE24_VERSION="24.14.0"
|
||||
|
||||
get_abs_path() {
|
||||
|
||||
@@ -115,14 +115,6 @@ namespace GitHub.Runner.Worker
|
||||
executionContext.Result = TaskResult.Failed;
|
||||
throw;
|
||||
}
|
||||
catch (FailedToDownloadActionException ex)
|
||||
{
|
||||
// Log the error and fail the PrepareActionsAsync Initialization.
|
||||
Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}");
|
||||
executionContext.InfrastructureError(ex.InnerException?.Message ?? ex.Message, category: "error_download_action");
|
||||
executionContext.Result = TaskResult.Failed;
|
||||
throw;
|
||||
}
|
||||
catch (InvalidActionArchiveException ex)
|
||||
{
|
||||
// Log the error and fail the PrepareActionsAsync Initialization.
|
||||
@@ -1165,101 +1157,92 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
// Allow up to 20 * 60s for any action to be downloaded from github graph.
|
||||
int timeoutSeconds = 20 * 60;
|
||||
try
|
||||
while (retryCount < 3)
|
||||
{
|
||||
while (retryCount < 3)
|
||||
string requestId = string.Empty;
|
||||
using (var actionDownloadTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)))
|
||||
using (var actionDownloadCancellation = CancellationTokenSource.CreateLinkedTokenSource(actionDownloadTimeout.Token, executionContext.CancellationToken))
|
||||
{
|
||||
string requestId = string.Empty;
|
||||
using (var actionDownloadTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)))
|
||||
using (var actionDownloadCancellation = CancellationTokenSource.CreateLinkedTokenSource(actionDownloadTimeout.Token, executionContext.CancellationToken))
|
||||
try
|
||||
{
|
||||
try
|
||||
//open zip stream in async mode
|
||||
using (FileStream fs = new(archiveFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: _defaultFileStreamBufferSize, useAsync: true))
|
||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||
using (var httpClient = new HttpClient(httpClientHandler))
|
||||
{
|
||||
//open zip stream in async mode
|
||||
using (FileStream fs = new(archiveFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: _defaultFileStreamBufferSize, useAsync: true))
|
||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||
using (var httpClient = new HttpClient(httpClientHandler))
|
||||
httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(downloadAuthToken);
|
||||
|
||||
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
||||
using (var response = await httpClient.GetAsync(downloadUrl))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(downloadAuthToken);
|
||||
|
||||
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
||||
using (var response = await httpClient.GetAsync(downloadUrl))
|
||||
requestId = UrlUtil.GetGitHubRequestId(response.Headers);
|
||||
if (!string.IsNullOrEmpty(requestId))
|
||||
{
|
||||
requestId = UrlUtil.GetGitHubRequestId(response.Headers);
|
||||
if (!string.IsNullOrEmpty(requestId))
|
||||
{
|
||||
Trace.Info($"Request URL: {downloadUrl} X-GitHub-Request-Id: {requestId} Http Status: {response.StatusCode}");
|
||||
}
|
||||
Trace.Info($"Request URL: {downloadUrl} X-GitHub-Request-Id: {requestId} Http Status: {response.StatusCode}");
|
||||
}
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
using (var result = await response.Content.ReadAsStreamAsync())
|
||||
{
|
||||
using (var result = await response.Content.ReadAsStreamAsync())
|
||||
{
|
||||
await result.CopyToAsync(fs, _defaultCopyBufferSize, actionDownloadCancellation.Token);
|
||||
await fs.FlushAsync(actionDownloadCancellation.Token);
|
||||
await result.CopyToAsync(fs, _defaultCopyBufferSize, actionDownloadCancellation.Token);
|
||||
await fs.FlushAsync(actionDownloadCancellation.Token);
|
||||
|
||||
// download succeed, break out the retry loop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
// It doesn't make sense to retry in this case, so just stop
|
||||
throw new ActionNotFoundException(new Uri(downloadUrl), requestId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Something else bad happened, let's go to our retry logic
|
||||
response.EnsureSuccessStatusCode();
|
||||
// download succeed, break out the retry loop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
// It doesn't make sense to retry in this case, so just stop
|
||||
throw new ActionNotFoundException(new Uri(downloadUrl), requestId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Something else bad happened, let's go to our retry logic
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (executionContext.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Trace.Info("Action download has been cancelled.");
|
||||
throw;
|
||||
}
|
||||
catch (OperationCanceledException ex) when (!executionContext.CancellationToken.IsCancellationRequested && retryCount >= 2)
|
||||
{
|
||||
Trace.Info($"Action download final retry timeout after {timeoutSeconds} seconds.");
|
||||
throw new TimeoutException($"Action '{downloadUrl}' download has timed out. Error: {ex.Message} {requestId}");
|
||||
}
|
||||
catch (ActionNotFoundException)
|
||||
{
|
||||
Trace.Info($"The action at '{downloadUrl}' does not exist");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (retryCount < 2)
|
||||
{
|
||||
retryCount++;
|
||||
Trace.Error($"Fail to download archive '{downloadUrl}' -- Attempt: {retryCount}");
|
||||
Trace.Error(ex);
|
||||
if (actionDownloadTimeout.Token.IsCancellationRequested)
|
||||
{
|
||||
// action download didn't finish within timeout
|
||||
executionContext.Warning($"Action '{downloadUrl}' didn't finish download within {timeoutSeconds} seconds. {requestId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
executionContext.Warning($"Failed to download action '{downloadUrl}'. Error: {ex.Message} {requestId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF")))
|
||||
catch (OperationCanceledException) when (executionContext.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
|
||||
executionContext.Warning($"Back off {backOff.TotalSeconds} seconds before retry.");
|
||||
await Task.Delay(backOff);
|
||||
Trace.Info("Action download has been cancelled.");
|
||||
throw;
|
||||
}
|
||||
catch (OperationCanceledException ex) when (!executionContext.CancellationToken.IsCancellationRequested && retryCount >= 2)
|
||||
{
|
||||
Trace.Info($"Action download final retry timeout after {timeoutSeconds} seconds.");
|
||||
throw new TimeoutException($"Action '{downloadUrl}' download has timed out. Error: {ex.Message} {requestId}");
|
||||
}
|
||||
catch (ActionNotFoundException)
|
||||
{
|
||||
Trace.Info($"The action at '{downloadUrl}' does not exist");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (retryCount < 2)
|
||||
{
|
||||
retryCount++;
|
||||
Trace.Error($"Fail to download archive '{downloadUrl}' -- Attempt: {retryCount}");
|
||||
Trace.Error(ex);
|
||||
if (actionDownloadTimeout.Token.IsCancellationRequested)
|
||||
{
|
||||
// action download didn't finish within timeout
|
||||
executionContext.Warning($"Action '{downloadUrl}' didn't finish download within {timeoutSeconds} seconds. {requestId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
executionContext.Warning($"Failed to download action '{downloadUrl}'. Error: {ex.Message} {requestId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!(ex is OperationCanceledException) && !executionContext.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Trace.Error($"Failed to download archive '{downloadUrl}' after {retryCount + 1} attempts.");
|
||||
Trace.Error(ex);
|
||||
throw new FailedToDownloadActionException($"Failed to download archive '{downloadUrl}' after {retryCount + 1} attempts.", ex);
|
||||
|
||||
if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF")))
|
||||
{
|
||||
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
|
||||
executionContext.Warning($"Back off {backOff.TotalSeconds} seconds before retry.");
|
||||
await Task.Delay(backOff);
|
||||
}
|
||||
}
|
||||
|
||||
ArgUtil.NotNullOrEmpty(archiveFile, nameof(archiveFile));
|
||||
|
||||
644
src/Runner.Worker/Dap/DapDebugSession.cs
Normal file
644
src/Runner.Worker/Dap/DapDebugSession.cs
Normal file
@@ -0,0 +1,644 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores information about a completed step for stack trace display.
|
||||
/// </summary>
|
||||
internal sealed class CompletedStepInfo
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public TaskResult? Result { get; set; }
|
||||
public int FrameId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal production DAP debug session.
|
||||
/// Handles step-level breakpoints with next/continue flow control,
|
||||
/// client reconnection, and cancellation signal propagation.
|
||||
///
|
||||
/// Scope inspection, REPL, step manipulation, and time-travel debugging
|
||||
/// are intentionally deferred to future iterations.
|
||||
/// </summary>
|
||||
public sealed class DapDebugSession : RunnerService, IDapDebugSession
|
||||
{
|
||||
// Thread ID for the single job execution thread
|
||||
private const int JobThreadId = 1;
|
||||
|
||||
// Frame ID for the current step (always 1)
|
||||
private const int CurrentFrameId = 1;
|
||||
|
||||
// Frame IDs for completed steps start at 1000
|
||||
private const int CompletedFrameIdBase = 1000;
|
||||
|
||||
private IDapServer _server;
|
||||
private DapSessionState _state = DapSessionState.WaitingForConnection;
|
||||
|
||||
// Synchronization for step execution
|
||||
private TaskCompletionSource<DapCommand> _commandTcs;
|
||||
private readonly object _stateLock = new object();
|
||||
|
||||
// Handshake completion — signaled when configurationDone is received
|
||||
private readonly TaskCompletionSource<bool> _handshakeTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
// Whether to pause before the next step (set by 'next' command)
|
||||
private bool _pauseOnNextStep = true;
|
||||
|
||||
// Current execution context
|
||||
private IStep _currentStep;
|
||||
private IExecutionContext _jobContext;
|
||||
private int _currentStepIndex;
|
||||
|
||||
// Track completed steps for stack trace
|
||||
private readonly List<CompletedStepInfo> _completedSteps = new List<CompletedStepInfo>();
|
||||
private int _nextCompletedFrameId = CompletedFrameIdBase;
|
||||
|
||||
// Client connection tracking for reconnection support
|
||||
private volatile bool _isClientConnected;
|
||||
|
||||
public bool IsActive =>
|
||||
_state == DapSessionState.Ready ||
|
||||
_state == DapSessionState.Paused ||
|
||||
_state == DapSessionState.Running;
|
||||
|
||||
public DapSessionState State => _state;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
Trace.Info("DapDebugSession initialized");
|
||||
}
|
||||
|
||||
public void SetDapServer(IDapServer server)
|
||||
{
|
||||
_server = server;
|
||||
Trace.Info("DAP server reference set");
|
||||
}
|
||||
|
||||
public async Task WaitForHandshakeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Trace.Info("Waiting for DAP handshake (configurationDone)...");
|
||||
|
||||
using (cancellationToken.Register(() => _handshakeTcs.TrySetCanceled()))
|
||||
{
|
||||
await _handshakeTcs.Task;
|
||||
}
|
||||
|
||||
Trace.Info("DAP handshake complete, session is ready");
|
||||
}
|
||||
|
||||
#region Message Dispatch
|
||||
|
||||
public async Task HandleMessageAsync(string messageJson, CancellationToken cancellationToken)
|
||||
{
|
||||
Request request = null;
|
||||
try
|
||||
{
|
||||
request = JsonConvert.DeserializeObject<Request>(messageJson);
|
||||
if (request == null)
|
||||
{
|
||||
Trace.Warning("Failed to deserialize DAP request");
|
||||
return;
|
||||
}
|
||||
|
||||
Trace.Info($"Handling DAP request: {request.Command}");
|
||||
|
||||
var response = request.Command switch
|
||||
{
|
||||
"initialize" => HandleInitialize(request),
|
||||
"attach" => HandleAttach(request),
|
||||
"configurationDone" => HandleConfigurationDone(request),
|
||||
"disconnect" => HandleDisconnect(request),
|
||||
"threads" => HandleThreads(request),
|
||||
"stackTrace" => HandleStackTrace(request),
|
||||
"scopes" => HandleScopes(request),
|
||||
"variables" => HandleVariables(request),
|
||||
"continue" => HandleContinue(request),
|
||||
"next" => HandleNext(request),
|
||||
"setBreakpoints" => HandleSetBreakpoints(request),
|
||||
"setExceptionBreakpoints" => HandleSetExceptionBreakpoints(request),
|
||||
_ => CreateResponse(request, false, $"Unsupported command: {request.Command}", body: null)
|
||||
};
|
||||
|
||||
response.RequestSeq = request.Seq;
|
||||
response.Command = request.Command;
|
||||
|
||||
_server?.SendResponse(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error($"Error handling request '{request?.Command}': {ex}");
|
||||
if (request != null)
|
||||
{
|
||||
var errorResponse = CreateResponse(request, false, ex.Message, body: null);
|
||||
errorResponse.RequestSeq = request.Seq;
|
||||
errorResponse.Command = request.Command;
|
||||
_server?.SendResponse(errorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DAP Request Handlers
|
||||
|
||||
private Response HandleInitialize(Request request)
|
||||
{
|
||||
if (request.Arguments != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var clientCaps = request.Arguments.ToObject<InitializeRequestArguments>();
|
||||
Trace.Info($"Client: {clientCaps?.ClientName ?? clientCaps?.ClientId ?? "unknown"}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"Failed to parse initialize arguments: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
_state = DapSessionState.Initializing;
|
||||
|
||||
// Build capabilities — MVP only supports configurationDone
|
||||
var capabilities = new Capabilities
|
||||
{
|
||||
SupportsConfigurationDoneRequest = true,
|
||||
// All other capabilities are false for MVP
|
||||
SupportsFunctionBreakpoints = false,
|
||||
SupportsConditionalBreakpoints = false,
|
||||
SupportsEvaluateForHovers = false,
|
||||
SupportsStepBack = false,
|
||||
SupportsSetVariable = false,
|
||||
SupportsRestartFrame = false,
|
||||
SupportsGotoTargetsRequest = false,
|
||||
SupportsStepInTargetsRequest = false,
|
||||
SupportsCompletionsRequest = false,
|
||||
SupportsModulesRequest = false,
|
||||
SupportsTerminateRequest = false,
|
||||
SupportTerminateDebuggee = false,
|
||||
SupportsDelayedStackTraceLoading = false,
|
||||
SupportsLoadedSourcesRequest = false,
|
||||
SupportsProgressReporting = false,
|
||||
SupportsRunInTerminalRequest = false,
|
||||
SupportsCancelRequest = false,
|
||||
SupportsExceptionOptions = false,
|
||||
SupportsValueFormattingOptions = false,
|
||||
SupportsExceptionInfoRequest = false,
|
||||
};
|
||||
|
||||
// Send initialized event after a brief delay to ensure the
|
||||
// response is delivered first (DAP spec requirement)
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(50);
|
||||
_server?.SendEvent(new Event
|
||||
{
|
||||
EventType = "initialized"
|
||||
});
|
||||
Trace.Info("Sent initialized event");
|
||||
});
|
||||
|
||||
Trace.Info("Initialize request handled, capabilities sent");
|
||||
return CreateResponse(request, true, body: capabilities);
|
||||
}
|
||||
|
||||
private Response HandleAttach(Request request)
|
||||
{
|
||||
Trace.Info("Attach request handled");
|
||||
return CreateResponse(request, true, body: null);
|
||||
}
|
||||
|
||||
private Response HandleConfigurationDone(Request request)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
_state = DapSessionState.Ready;
|
||||
}
|
||||
|
||||
_handshakeTcs.TrySetResult(true);
|
||||
|
||||
Trace.Info("Configuration done, debug session is ready");
|
||||
return CreateResponse(request, true, body: null);
|
||||
}
|
||||
|
||||
private Response HandleDisconnect(Request request)
|
||||
{
|
||||
Trace.Info("Disconnect request received");
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
_state = DapSessionState.Terminated;
|
||||
|
||||
// Release any blocked step execution
|
||||
_commandTcs?.TrySetResult(DapCommand.Disconnect);
|
||||
}
|
||||
|
||||
return CreateResponse(request, true, body: null);
|
||||
}
|
||||
|
||||
private Response HandleThreads(Request request)
|
||||
{
|
||||
var body = new ThreadsResponseBody
|
||||
{
|
||||
Threads = new List<Thread>
|
||||
{
|
||||
new Thread
|
||||
{
|
||||
Id = JobThreadId,
|
||||
Name = _jobContext != null
|
||||
? $"Job: {_jobContext.GetGitHubContext("job") ?? "workflow job"}"
|
||||
: "Job Thread"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return CreateResponse(request, true, body: body);
|
||||
}
|
||||
|
||||
private Response HandleStackTrace(Request request)
|
||||
{
|
||||
var frames = new List<StackFrame>();
|
||||
|
||||
// Add current step as the top frame
|
||||
if (_currentStep != null)
|
||||
{
|
||||
var resultIndicator = _currentStep.ExecutionContext?.Result != null
|
||||
? $" [{_currentStep.ExecutionContext.Result}]"
|
||||
: " [running]";
|
||||
|
||||
frames.Add(new StackFrame
|
||||
{
|
||||
Id = CurrentFrameId,
|
||||
Name = $"{_currentStep.DisplayName ?? "Current Step"}{resultIndicator}",
|
||||
Line = _currentStepIndex + 1,
|
||||
Column = 1,
|
||||
PresentationHint = "normal"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
frames.Add(new StackFrame
|
||||
{
|
||||
Id = CurrentFrameId,
|
||||
Name = "(no step executing)",
|
||||
Line = 0,
|
||||
Column = 1,
|
||||
PresentationHint = "subtle"
|
||||
});
|
||||
}
|
||||
|
||||
// Add completed steps as additional frames (most recent first)
|
||||
for (int i = _completedSteps.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var completedStep = _completedSteps[i];
|
||||
var resultStr = completedStep.Result.HasValue ? $" [{completedStep.Result}]" : "";
|
||||
frames.Add(new StackFrame
|
||||
{
|
||||
Id = completedStep.FrameId,
|
||||
Name = $"{completedStep.DisplayName}{resultStr}",
|
||||
Line = 1,
|
||||
Column = 1,
|
||||
PresentationHint = "subtle"
|
||||
});
|
||||
}
|
||||
|
||||
var body = new StackTraceResponseBody
|
||||
{
|
||||
StackFrames = frames,
|
||||
TotalFrames = frames.Count
|
||||
};
|
||||
|
||||
return CreateResponse(request, true, body: body);
|
||||
}
|
||||
|
||||
private Response HandleScopes(Request request)
|
||||
{
|
||||
// MVP: return empty scopes — scope inspection deferred
|
||||
return CreateResponse(request, true, body: new ScopesResponseBody
|
||||
{
|
||||
Scopes = new List<Scope>()
|
||||
});
|
||||
}
|
||||
|
||||
private Response HandleVariables(Request request)
|
||||
{
|
||||
// MVP: return empty variables — variable inspection deferred
|
||||
return CreateResponse(request, true, body: new VariablesResponseBody
|
||||
{
|
||||
Variables = new List<Variable>()
|
||||
});
|
||||
}
|
||||
|
||||
private Response HandleContinue(Request request)
|
||||
{
|
||||
Trace.Info("Continue command received");
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_state == DapSessionState.Paused)
|
||||
{
|
||||
_state = DapSessionState.Running;
|
||||
_pauseOnNextStep = false;
|
||||
_commandTcs?.TrySetResult(DapCommand.Continue);
|
||||
}
|
||||
}
|
||||
|
||||
return CreateResponse(request, true, body: new ContinueResponseBody
|
||||
{
|
||||
AllThreadsContinued = true
|
||||
});
|
||||
}
|
||||
|
||||
private Response HandleNext(Request request)
|
||||
{
|
||||
Trace.Info("Next (step over) command received");
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_state == DapSessionState.Paused)
|
||||
{
|
||||
_state = DapSessionState.Running;
|
||||
_pauseOnNextStep = true;
|
||||
_commandTcs?.TrySetResult(DapCommand.Next);
|
||||
}
|
||||
}
|
||||
|
||||
return CreateResponse(request, true, body: null);
|
||||
}
|
||||
|
||||
private Response HandleSetBreakpoints(Request request)
|
||||
{
|
||||
// MVP: acknowledge but don't process breakpoints
|
||||
// All steps pause automatically via _pauseOnNextStep
|
||||
return CreateResponse(request, true, body: new { breakpoints = Array.Empty<object>() });
|
||||
}
|
||||
|
||||
private Response HandleSetExceptionBreakpoints(Request request)
|
||||
{
|
||||
// MVP: acknowledge but don't process exception breakpoints
|
||||
return CreateResponse(request, true, body: null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Step Lifecycle
|
||||
|
||||
public async Task OnStepStartingAsync(IStep step, IExecutionContext jobContext, bool isFirstStep, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!IsActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_currentStep = step;
|
||||
_jobContext = jobContext;
|
||||
_currentStepIndex = _completedSteps.Count;
|
||||
|
||||
// Determine if we should pause
|
||||
bool shouldPause = isFirstStep || _pauseOnNextStep;
|
||||
|
||||
if (!shouldPause)
|
||||
{
|
||||
Trace.Info($"Step starting (not pausing): {step.DisplayName}");
|
||||
return;
|
||||
}
|
||||
|
||||
var reason = isFirstStep ? "entry" : "step";
|
||||
var description = isFirstStep
|
||||
? $"Stopped at job entry: {step.DisplayName}"
|
||||
: $"Stopped before step: {step.DisplayName}";
|
||||
|
||||
Trace.Info($"Step starting: {step.DisplayName} (reason: {reason})");
|
||||
|
||||
// Send stopped event to debugger (only if client is connected)
|
||||
SendStoppedEvent(reason, description);
|
||||
|
||||
// Wait for debugger command
|
||||
await WaitForCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public void OnStepCompleted(IStep step)
|
||||
{
|
||||
if (!IsActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = step.ExecutionContext?.Result;
|
||||
Trace.Info($"Step completed: {step.DisplayName}, result: {result}");
|
||||
|
||||
// Add to completed steps list for stack trace
|
||||
_completedSteps.Add(new CompletedStepInfo
|
||||
{
|
||||
DisplayName = step.DisplayName,
|
||||
Result = result,
|
||||
FrameId = _nextCompletedFrameId++
|
||||
});
|
||||
}
|
||||
|
||||
public void OnJobCompleted()
|
||||
{
|
||||
if (!IsActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Trace.Info("Job completed, sending terminated event");
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
_state = DapSessionState.Terminated;
|
||||
}
|
||||
|
||||
_server?.SendEvent(new Event
|
||||
{
|
||||
EventType = "terminated",
|
||||
Body = new TerminatedEventBody()
|
||||
});
|
||||
|
||||
var exitCode = _jobContext?.Result == TaskResult.Succeeded ? 0 : 1;
|
||||
_server?.SendEvent(new Event
|
||||
{
|
||||
EventType = "exited",
|
||||
Body = new ExitedEventBody
|
||||
{
|
||||
ExitCode = exitCode
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void CancelSession()
|
||||
{
|
||||
Trace.Info("CancelSession called - terminating debug session");
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_state == DapSessionState.Terminated)
|
||||
{
|
||||
Trace.Info("Session already terminated, ignoring CancelSession");
|
||||
return;
|
||||
}
|
||||
_state = DapSessionState.Terminated;
|
||||
}
|
||||
|
||||
// Send terminated event to debugger so it updates its UI
|
||||
_server?.SendEvent(new Event
|
||||
{
|
||||
EventType = "terminated",
|
||||
Body = new TerminatedEventBody()
|
||||
});
|
||||
|
||||
// Send exited event with cancellation exit code (130 = SIGINT convention)
|
||||
_server?.SendEvent(new Event
|
||||
{
|
||||
EventType = "exited",
|
||||
Body = new ExitedEventBody { ExitCode = 130 }
|
||||
});
|
||||
|
||||
// Release any pending command waits
|
||||
_commandTcs?.TrySetResult(DapCommand.Disconnect);
|
||||
|
||||
// Release handshake wait if still pending
|
||||
_handshakeTcs.TrySetCanceled();
|
||||
|
||||
Trace.Info("Debug session cancelled");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client Connection Tracking
|
||||
|
||||
public void HandleClientConnected()
|
||||
{
|
||||
_isClientConnected = true;
|
||||
Trace.Info("Client connected to debug session");
|
||||
|
||||
// If we're paused, re-send the stopped event so the new client
|
||||
// knows the current state (important for reconnection)
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_state == DapSessionState.Paused && _currentStep != null)
|
||||
{
|
||||
Trace.Info("Re-sending stopped event to reconnected client");
|
||||
var description = $"Stopped before step: {_currentStep.DisplayName}";
|
||||
SendStoppedEvent("step", description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleClientDisconnected()
|
||||
{
|
||||
_isClientConnected = false;
|
||||
Trace.Info("Client disconnected from debug session");
|
||||
|
||||
// Intentionally do NOT release the command TCS here.
|
||||
// The session stays paused, waiting for a client to reconnect.
|
||||
// The server's connection loop will accept a new client and
|
||||
// call HandleClientConnected, which re-sends the stopped event.
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Blocks the step execution thread until a debugger command is received
|
||||
/// or the job is cancelled.
|
||||
/// </summary>
|
||||
private async Task WaitForCommandAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
_state = DapSessionState.Paused;
|
||||
_commandTcs = new TaskCompletionSource<DapCommand>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
|
||||
Trace.Info("Waiting for debugger command...");
|
||||
|
||||
using (cancellationToken.Register(() =>
|
||||
{
|
||||
Trace.Info("Job cancellation detected, releasing debugger wait");
|
||||
_commandTcs?.TrySetResult(DapCommand.Disconnect);
|
||||
}))
|
||||
{
|
||||
var command = await _commandTcs.Task;
|
||||
|
||||
Trace.Info($"Received command: {command}");
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_state == DapSessionState.Paused)
|
||||
{
|
||||
_state = DapSessionState.Running;
|
||||
}
|
||||
}
|
||||
|
||||
// Send continued event for normal flow commands
|
||||
if (!cancellationToken.IsCancellationRequested &&
|
||||
(command == DapCommand.Continue || command == DapCommand.Next))
|
||||
{
|
||||
_server?.SendEvent(new Event
|
||||
{
|
||||
EventType = "continued",
|
||||
Body = new ContinuedEventBody
|
||||
{
|
||||
ThreadId = JobThreadId,
|
||||
AllThreadsContinued = true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a stopped event to the connected client.
|
||||
/// Silently no-ops if no client is connected.
|
||||
/// </summary>
|
||||
private void SendStoppedEvent(string reason, string description)
|
||||
{
|
||||
if (!_isClientConnected)
|
||||
{
|
||||
Trace.Info($"No client connected, deferring stopped event: {description}");
|
||||
return;
|
||||
}
|
||||
|
||||
_server?.SendEvent(new Event
|
||||
{
|
||||
EventType = "stopped",
|
||||
Body = new StoppedEventBody
|
||||
{
|
||||
Reason = reason,
|
||||
Description = description,
|
||||
ThreadId = JobThreadId,
|
||||
AllThreadsStopped = true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a DAP response with common fields pre-populated.
|
||||
/// </summary>
|
||||
private Response CreateResponse(Request request, bool success, string message = null, object body = null)
|
||||
{
|
||||
return new Response
|
||||
{
|
||||
Type = "response",
|
||||
RequestSeq = request.Seq,
|
||||
Command = request.Command,
|
||||
Success = success,
|
||||
Message = success ? null : message,
|
||||
Body = body
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
1134
src/Runner.Worker/Dap/DapMessages.cs
Normal file
1134
src/Runner.Worker/Dap/DapMessages.cs
Normal file
File diff suppressed because it is too large
Load Diff
466
src/Runner.Worker/Dap/DapServer.cs
Normal file
466
src/Runner.Worker/Dap/DapServer.cs
Normal file
@@ -0,0 +1,466 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Common;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
/// <summary>
|
||||
/// Production TCP server for the Debug Adapter Protocol.
|
||||
/// Handles Content-Length message framing, JSON serialization,
|
||||
/// client reconnection, and graceful shutdown.
|
||||
/// </summary>
|
||||
public sealed class DapServer : RunnerService, IDapServer
|
||||
{
|
||||
private const string ContentLengthHeader = "Content-Length: ";
|
||||
|
||||
private TcpListener _listener;
|
||||
private TcpClient _client;
|
||||
private NetworkStream _stream;
|
||||
private IDapDebugSession _session;
|
||||
private CancellationTokenSource _cts;
|
||||
private TaskCompletionSource<bool> _connectionTcs;
|
||||
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
|
||||
private int _nextSeq = 1;
|
||||
private volatile bool _acceptConnections = true;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
Trace.Info("DapServer initialized");
|
||||
}
|
||||
|
||||
public void SetSession(IDapDebugSession session)
|
||||
{
|
||||
_session = session;
|
||||
Trace.Info("Debug session set");
|
||||
}
|
||||
|
||||
public async Task StartAsync(int port, CancellationToken cancellationToken)
|
||||
{
|
||||
Trace.Info($"Starting DAP server on port {port}");
|
||||
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_connectionTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
_listener = new TcpListener(IPAddress.Loopback, port);
|
||||
_listener.Start();
|
||||
Trace.Info($"DAP server listening on 127.0.0.1:{port}");
|
||||
|
||||
// Start the connection loop in the background
|
||||
_ = ConnectionLoopAsync(_cts.Token);
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepts client connections in a loop, supporting reconnection.
|
||||
/// When a client disconnects, the server waits for a new connection
|
||||
/// without blocking step execution.
|
||||
/// </summary>
|
||||
private async Task ConnectionLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (_acceptConnections && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
Trace.Info("Waiting for debug client connection...");
|
||||
|
||||
using (cancellationToken.Register(() =>
|
||||
{
|
||||
try { _listener?.Stop(); }
|
||||
catch { /* listener already stopped */ }
|
||||
}))
|
||||
{
|
||||
_client = await _listener.AcceptTcpClientAsync();
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
_stream = _client.GetStream();
|
||||
var remoteEndPoint = _client.Client.RemoteEndPoint;
|
||||
Trace.Info($"Debug client connected from {remoteEndPoint}");
|
||||
|
||||
// Signal first connection (no-op on subsequent connections)
|
||||
_connectionTcs.TrySetResult(true);
|
||||
|
||||
// Notify session of new client
|
||||
_session?.HandleClientConnected();
|
||||
|
||||
// Process messages until client disconnects
|
||||
await ProcessMessagesAsync(cancellationToken);
|
||||
|
||||
// Client disconnected — notify session and clean up
|
||||
Trace.Info("Client disconnected, waiting for reconnection...");
|
||||
_session?.HandleClientDisconnected();
|
||||
CleanupConnection();
|
||||
}
|
||||
catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (SocketException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"Connection error: {ex.Message}");
|
||||
CleanupConnection();
|
||||
|
||||
if (!_acceptConnections || cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Brief delay before accepting next connection
|
||||
try
|
||||
{
|
||||
await Task.Delay(100, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_connectionTcs.TrySetCanceled();
|
||||
Trace.Info("Connection loop ended");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up the current client connection without stopping the listener.
|
||||
/// </summary>
|
||||
private void CleanupConnection()
|
||||
{
|
||||
try { _stream?.Close(); } catch { /* best effort */ }
|
||||
try { _client?.Close(); } catch { /* best effort */ }
|
||||
_stream = null;
|
||||
_client = null;
|
||||
}
|
||||
|
||||
public async Task WaitForConnectionAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Trace.Info("Waiting for debug client to connect...");
|
||||
|
||||
using (cancellationToken.Register(() => _connectionTcs.TrySetCanceled()))
|
||||
{
|
||||
await _connectionTcs.Task;
|
||||
}
|
||||
|
||||
Trace.Info("Debug client connected");
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
Trace.Info("Stopping DAP server");
|
||||
|
||||
_acceptConnections = false;
|
||||
_cts?.Cancel();
|
||||
|
||||
CleanupConnection();
|
||||
|
||||
try { _listener?.Stop(); }
|
||||
catch { /* best effort */ }
|
||||
|
||||
await Task.CompletedTask;
|
||||
|
||||
Trace.Info("DAP server stopped");
|
||||
}
|
||||
|
||||
private async Task ProcessMessagesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Trace.Info("Starting DAP message processing loop");
|
||||
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested && _client?.Connected == true)
|
||||
{
|
||||
var json = await ReadMessageAsync(cancellationToken);
|
||||
if (json == null)
|
||||
{
|
||||
Trace.Info("Client disconnected (end of stream)");
|
||||
break;
|
||||
}
|
||||
|
||||
await ProcessSingleMessageAsync(json, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Trace.Info("Message processing cancelled");
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Trace.Info($"Connection closed: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error($"Error in message loop: {ex}");
|
||||
}
|
||||
|
||||
Trace.Info("DAP message processing loop ended");
|
||||
}
|
||||
|
||||
private async Task ProcessSingleMessageAsync(string json, CancellationToken cancellationToken)
|
||||
{
|
||||
Request request = null;
|
||||
try
|
||||
{
|
||||
request = JsonConvert.DeserializeObject<Request>(json);
|
||||
if (request == null || request.Type != "request")
|
||||
{
|
||||
Trace.Warning($"Received non-request message: {json}");
|
||||
return;
|
||||
}
|
||||
|
||||
Trace.Info($"Received request: seq={request.Seq}, command={request.Command}");
|
||||
|
||||
if (_session == null)
|
||||
{
|
||||
Trace.Error("No debug session configured");
|
||||
SendErrorResponse(request, "No debug session configured");
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass raw JSON to session — session handles deserialization, dispatch,
|
||||
// and calls back to SendResponse when done.
|
||||
await _session.HandleMessageAsync(json, cancellationToken);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Trace.Error($"Failed to parse request: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error($"Error processing request: {ex}");
|
||||
if (request != null)
|
||||
{
|
||||
SendErrorResponse(request, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SendErrorResponse(Request request, string message)
|
||||
{
|
||||
var response = new Response
|
||||
{
|
||||
Type = "response",
|
||||
RequestSeq = request.Seq,
|
||||
Command = request.Command,
|
||||
Success = false,
|
||||
Message = message,
|
||||
Body = new ErrorResponseBody
|
||||
{
|
||||
Error = new Message
|
||||
{
|
||||
Id = 1,
|
||||
Format = message,
|
||||
ShowUser = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SendResponse(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a DAP message using Content-Length framing.
|
||||
/// Format: Content-Length: N\r\n\r\n{json}
|
||||
/// </summary>
|
||||
private async Task<string> ReadMessageAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
int contentLength = -1;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var line = await ReadLineAsync(cancellationToken);
|
||||
if (line == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (line.Length == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (line.StartsWith(ContentLengthHeader, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var lengthStr = line.Substring(ContentLengthHeader.Length).Trim();
|
||||
if (!int.TryParse(lengthStr, out contentLength))
|
||||
{
|
||||
throw new InvalidDataException($"Invalid Content-Length: {lengthStr}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contentLength < 0)
|
||||
{
|
||||
throw new InvalidDataException("Missing Content-Length header");
|
||||
}
|
||||
|
||||
var buffer = new byte[contentLength];
|
||||
var totalRead = 0;
|
||||
while (totalRead < contentLength)
|
||||
{
|
||||
var bytesRead = await _stream.ReadAsync(buffer, totalRead, contentLength - totalRead, cancellationToken);
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
throw new EndOfStreamException("Connection closed while reading message body");
|
||||
}
|
||||
totalRead += bytesRead;
|
||||
}
|
||||
|
||||
var json = Encoding.UTF8.GetString(buffer);
|
||||
Trace.Verbose($"Received: {json}");
|
||||
return json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a line terminated by \r\n from the network stream.
|
||||
/// </summary>
|
||||
private async Task<string> ReadLineAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var lineBuilder = new StringBuilder();
|
||||
var buffer = new byte[1];
|
||||
var previousWasCr = false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var bytesRead = await _stream.ReadAsync(buffer, 0, 1, cancellationToken);
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
return lineBuilder.Length > 0 ? lineBuilder.ToString() : null;
|
||||
}
|
||||
|
||||
var c = (char)buffer[0];
|
||||
|
||||
if (c == '\n' && previousWasCr)
|
||||
{
|
||||
if (lineBuilder.Length > 0 && lineBuilder[lineBuilder.Length - 1] == '\r')
|
||||
{
|
||||
lineBuilder.Length--;
|
||||
}
|
||||
return lineBuilder.ToString();
|
||||
}
|
||||
|
||||
previousWasCr = (c == '\r');
|
||||
lineBuilder.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes and writes a DAP message with Content-Length framing.
|
||||
/// Must be called within the _sendLock.
|
||||
/// </summary>
|
||||
private void SendMessageInternal(ProtocolMessage message)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(message, new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
});
|
||||
|
||||
var bodyBytes = Encoding.UTF8.GetBytes(json);
|
||||
var header = $"Content-Length: {bodyBytes.Length}\r\n\r\n";
|
||||
var headerBytes = Encoding.ASCII.GetBytes(header);
|
||||
|
||||
_stream.Write(headerBytes, 0, headerBytes.Length);
|
||||
_stream.Write(bodyBytes, 0, bodyBytes.Length);
|
||||
_stream.Flush();
|
||||
|
||||
Trace.Verbose($"Sent: {json}");
|
||||
}
|
||||
|
||||
public void SendMessage(ProtocolMessage message)
|
||||
{
|
||||
if (_stream == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_sendLock.Wait();
|
||||
try
|
||||
{
|
||||
message.Seq = _nextSeq++;
|
||||
SendMessageInternal(message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendLock.Release();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"Failed to send message: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void SendEvent(Event evt)
|
||||
{
|
||||
if (_stream == null)
|
||||
{
|
||||
Trace.Warning($"Cannot send event '{evt.EventType}': no client connected");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_sendLock.Wait();
|
||||
try
|
||||
{
|
||||
evt.Seq = _nextSeq++;
|
||||
SendMessageInternal(evt);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendLock.Release();
|
||||
}
|
||||
Trace.Info($"Sent event: {evt.EventType}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"Failed to send event '{evt.EventType}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void SendResponse(Response response)
|
||||
{
|
||||
if (_stream == null)
|
||||
{
|
||||
Trace.Warning($"Cannot send response for '{response.Command}': no client connected");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_sendLock.Wait();
|
||||
try
|
||||
{
|
||||
response.Seq = _nextSeq++;
|
||||
SendMessageInternal(response);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendLock.Release();
|
||||
}
|
||||
Trace.Info($"Sent response: seq={response.Seq}, command={response.Command}, success={response.Success}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"Failed to send response for '{response.Command}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/Runner.Worker/Dap/IDapDebugSession.cs
Normal file
32
src/Runner.Worker/Dap/IDapDebugSession.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Common;
|
||||
|
||||
namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
public enum DapSessionState
|
||||
{
|
||||
WaitingForConnection,
|
||||
Initializing,
|
||||
Ready,
|
||||
Paused,
|
||||
Running,
|
||||
Terminated
|
||||
}
|
||||
|
||||
[ServiceLocator(Default = typeof(DapDebugSession))]
|
||||
public interface IDapDebugSession : IRunnerService
|
||||
{
|
||||
bool IsActive { get; }
|
||||
DapSessionState State { get; }
|
||||
void SetDapServer(IDapServer server);
|
||||
Task WaitForHandshakeAsync(CancellationToken cancellationToken);
|
||||
Task OnStepStartingAsync(IStep step, IExecutionContext jobContext, bool isFirstStep, CancellationToken cancellationToken);
|
||||
void OnStepCompleted(IStep step);
|
||||
void OnJobCompleted();
|
||||
void CancelSession();
|
||||
void HandleClientConnected();
|
||||
void HandleClientDisconnected();
|
||||
Task HandleMessageAsync(string messageJson, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
18
src/Runner.Worker/Dap/IDapServer.cs
Normal file
18
src/Runner.Worker/Dap/IDapServer.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Common;
|
||||
|
||||
namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
[ServiceLocator(Default = typeof(DapServer))]
|
||||
public interface IDapServer : IRunnerService
|
||||
{
|
||||
void SetSession(IDapDebugSession session);
|
||||
Task StartAsync(int port, CancellationToken cancellationToken);
|
||||
Task WaitForConnectionAsync(CancellationToken cancellationToken);
|
||||
Task StopAsync();
|
||||
void SendMessage(ProtocolMessage message);
|
||||
void SendEvent(Event evt);
|
||||
void SendResponse(Response response);
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,8 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
List<string> StepEnvironmentOverrides { get; }
|
||||
|
||||
IExecutionContext Root { get; }
|
||||
ExecutionContext Root { get; }
|
||||
ExecutionContext Parent { get; }
|
||||
|
||||
// Initialize
|
||||
void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token);
|
||||
@@ -250,9 +251,7 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
IExecutionContext IExecutionContext.Root => Root;
|
||||
|
||||
private ExecutionContext Root
|
||||
public ExecutionContext Root
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -267,7 +266,13 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ExecutionContext Parent
|
||||
{
|
||||
get
|
||||
{
|
||||
return _parentExecutionContext;
|
||||
}
|
||||
}
|
||||
|
||||
public JobContext JobContext
|
||||
{
|
||||
@@ -963,6 +968,9 @@ namespace GitHub.Runner.Worker
|
||||
// Verbosity (from GitHub.Step_Debug).
|
||||
Global.WriteDebug = Global.Variables.Step_Debug ?? false;
|
||||
|
||||
// Debugger enabled flag (from acquire response).
|
||||
Global.EnableDebugger = message.EnableDebugger;
|
||||
|
||||
// Hook up JobServerQueueThrottling event, we will log warning on server tarpit.
|
||||
_jobServerQueue.JobServerQueueThrottling += JobServerQueueThrottling_EventReceived;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace GitHub.Runner.Worker
|
||||
public StepsContext StepsContext { get; set; }
|
||||
public Variables Variables { get; set; }
|
||||
public bool WriteDebug { get; set; }
|
||||
public bool EnableDebugger { get; set; }
|
||||
public string InfrastructureFailureCategory { get; set; }
|
||||
public JObject ContainerHookState { get; set; }
|
||||
public bool HasTemplateEvaluatorMismatch { get; set; }
|
||||
|
||||
@@ -13,6 +13,7 @@ using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Services.WebApi;
|
||||
using Sdk.RSWebApi.Contracts;
|
||||
@@ -112,6 +113,9 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
IExecutionContext jobContext = null;
|
||||
CancellationTokenRegistration? runnerShutdownRegistration = null;
|
||||
IDapServer dapServer = null;
|
||||
IDapDebugSession debugSession = null;
|
||||
CancellationTokenRegistration? dapCancellationRegistration = null;
|
||||
try
|
||||
{
|
||||
// Create the job execution context.
|
||||
@@ -121,6 +125,36 @@ namespace GitHub.Runner.Worker
|
||||
jobContext.Start();
|
||||
jobContext.Debug($"Starting: {message.JobDisplayName}");
|
||||
|
||||
if (jobContext.Global.EnableDebugger)
|
||||
{
|
||||
Trace.Info("Debugger enabled for this job run");
|
||||
|
||||
try
|
||||
{
|
||||
var port = 4711;
|
||||
var portEnv = Environment.GetEnvironmentVariable("ACTIONS_DAP_PORT");
|
||||
if (!string.IsNullOrEmpty(portEnv) && int.TryParse(portEnv, out var customPort))
|
||||
{
|
||||
port = customPort;
|
||||
}
|
||||
|
||||
dapServer = HostContext.GetService<IDapServer>();
|
||||
debugSession = HostContext.GetService<IDapDebugSession>();
|
||||
|
||||
dapServer.SetSession(debugSession);
|
||||
debugSession.SetDapServer(dapServer);
|
||||
|
||||
await dapServer.StartAsync(port, jobRequestCancellationToken);
|
||||
Trace.Info($"DAP server started on port {port}, listening for debugger client");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"Failed to start DAP server: {ex.Message}. Job will continue without debugging.");
|
||||
dapServer = null;
|
||||
debugSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
runnerShutdownRegistration = HostContext.RunnerShutdownToken.Register(() =>
|
||||
{
|
||||
// log an issue, then runner get shutdown by Ctrl-C or Ctrl-Break.
|
||||
@@ -219,6 +253,39 @@ namespace GitHub.Runner.Worker
|
||||
await Task.WhenAny(_jobServerQueue.JobRecordUpdated.Task, Task.Delay(1000));
|
||||
}
|
||||
|
||||
// Wait for DAP debugger client connection and handshake after "Set up job"
|
||||
// so the job page shows the setup step before we block on the debugger
|
||||
if (dapServer != null && debugSession != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Trace.Info("Waiting for debugger client connection...");
|
||||
await dapServer.WaitForConnectionAsync(jobRequestCancellationToken);
|
||||
Trace.Info("Debugger client connected.");
|
||||
|
||||
await debugSession.WaitForHandshakeAsync(jobRequestCancellationToken);
|
||||
Trace.Info("DAP handshake complete.");
|
||||
|
||||
dapCancellationRegistration = jobRequestCancellationToken.Register(() =>
|
||||
{
|
||||
Trace.Info("Job cancellation requested, cancelling debug session.");
|
||||
debugSession.CancelSession();
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException) when (jobRequestCancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Trace.Info("Job was cancelled before debugger client connected. Continuing without debugger.");
|
||||
dapServer = null;
|
||||
debugSession = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"Failed to complete DAP handshake: {ex.Message}. Job will continue without debugging.");
|
||||
dapServer = null;
|
||||
debugSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Run all job steps
|
||||
Trace.Info("Run all job steps.");
|
||||
var stepsRunner = HostContext.GetService<IStepsRunner>();
|
||||
@@ -259,6 +326,25 @@ namespace GitHub.Runner.Worker
|
||||
runnerShutdownRegistration = null;
|
||||
}
|
||||
|
||||
if (dapCancellationRegistration.HasValue)
|
||||
{
|
||||
dapCancellationRegistration.Value.Dispose();
|
||||
dapCancellationRegistration = null;
|
||||
}
|
||||
|
||||
if (dapServer != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Trace.Info("Stopping DAP server");
|
||||
await dapServer.StopAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"Error stopping DAP server: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
await ShutdownQueue(throwOnFailure: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using GitHub.Actions.WorkflowParser;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
@@ -227,12 +226,8 @@ namespace GitHub.Runner.Worker
|
||||
Func<TNew> newEvaluator,
|
||||
Func<TLegacy, TNew, bool> resultComparer)
|
||||
{
|
||||
// Use the root (job-level) cancellation token to detect cancellation race conditions.
|
||||
// The step-level token only fires on step timeout, not on job cancellation.
|
||||
// Job cancellation mutates JobContext.Status which expression functions read,
|
||||
// so we need the root token to properly detect cancellation between evaluator runs.
|
||||
var rootCancellationToken = _context.Root?.CancellationToken ?? CancellationToken.None;
|
||||
var cancellationRequestedBefore = rootCancellationToken.IsCancellationRequested;
|
||||
// Capture cancellation state before evaluation
|
||||
var cancellationRequestedBefore = _context.CancellationToken.IsCancellationRequested;
|
||||
|
||||
// Legacy evaluator
|
||||
var legacyException = default(Exception);
|
||||
@@ -266,7 +261,7 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
|
||||
// Capture cancellation state after evaluation
|
||||
var cancellationRequestedAfter = rootCancellationToken.IsCancellationRequested;
|
||||
var cancellationRequestedAfter = _context.CancellationToken.IsCancellationRequested;
|
||||
|
||||
// Compare results or exceptions
|
||||
bool hasMismatch = false;
|
||||
|
||||
@@ -10,6 +10,7 @@ using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
using GitHub.Runner.Worker.Expressions;
|
||||
|
||||
namespace GitHub.Runner.Worker
|
||||
@@ -50,6 +51,16 @@ namespace GitHub.Runner.Worker
|
||||
jobContext.JobContext.Status = (jobContext.Result ?? TaskResult.Succeeded).ToActionResult();
|
||||
var scopeInputs = new Dictionary<string, PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
||||
bool checkPostJobActions = false;
|
||||
IDapDebugSession debugSession = null;
|
||||
try
|
||||
{
|
||||
debugSession = HostContext.GetService<IDapDebugSession>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Debug session not available — continue without debugging
|
||||
}
|
||||
bool isFirstStep = true;
|
||||
while (jobContext.JobSteps.Count > 0 || !checkPostJobActions)
|
||||
{
|
||||
if (jobContext.JobSteps.Count == 0 && !checkPostJobActions)
|
||||
@@ -226,9 +237,35 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pause for DAP debugger before step execution
|
||||
if (debugSession?.IsActive == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await debugSession.OnStepStartingAsync(step, jobContext, isFirstStep, jobContext.CancellationToken);
|
||||
isFirstStep = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"DAP OnStepStarting error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Run the step
|
||||
await RunStepAsync(step, jobContext.CancellationToken);
|
||||
CompleteStep(step);
|
||||
|
||||
if (debugSession?.IsActive == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
debugSession.OnStepCompleted(step);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"DAP OnStepCompleted error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -255,6 +292,18 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
Trace.Info($"Current state: job state = '{jobContext.Result}'");
|
||||
}
|
||||
|
||||
if (debugSession?.IsActive == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
debugSession.OnJobCompleted();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"DAP OnJobCompleted error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunStepAsync(IStep step, CancellationToken jobCancellationToken)
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace GitHub.DistributedTask.Expressions2
|
||||
IEnumerable<IFunctionInfo> functions,
|
||||
Boolean allowCaseFunction = true)
|
||||
{
|
||||
var context = new ParseContext(expression, trace, namedValues, functions, allowCaseFunction: allowCaseFunction);
|
||||
var context = new ParseContext(expression, trace, namedValues, functions, allowCaseFunction);
|
||||
context.Trace.Info($"Parsing expression: <{expression}>");
|
||||
return CreateTree(context);
|
||||
}
|
||||
|
||||
@@ -253,6 +253,13 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public bool EnableDebugger
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of variables associated with the current context.
|
||||
/// </summary>
|
||||
|
||||
@@ -2556,25 +2556,6 @@ namespace GitHub.DistributedTask.WebApi
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class FailedToDownloadActionException : DistributedTaskException
|
||||
{
|
||||
public FailedToDownloadActionException(String message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public FailedToDownloadActionException(String message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
private FailedToDownloadActionException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class InvalidActionArchiveException : DistributedTaskException
|
||||
{
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.DistributedTask.ObjectTemplating;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Sdk
|
||||
{
|
||||
/// <summary>
|
||||
/// Regression tests for ExpressionParser.CreateTree to verify that
|
||||
/// allowCaseFunction does not accidentally set allowUnknownKeywords.
|
||||
/// </summary>
|
||||
public sealed class ExpressionParserL0
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Sdk")]
|
||||
public void CreateTree_RejectsUnrecognizedNamedValue()
|
||||
{
|
||||
// Regression: allowCaseFunction was passed positionally into
|
||||
// the allowUnknownKeywords parameter, causing all named values
|
||||
// to be silently accepted.
|
||||
var parser = new ExpressionParser();
|
||||
var namedValues = new List<INamedValueInfo>
|
||||
{
|
||||
new NamedValueInfo<ContextValueNode>("inputs"),
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<ParseException>(() =>
|
||||
parser.CreateTree("github.event.repository.private", null, namedValues, null));
|
||||
|
||||
Assert.Contains("Unrecognized named-value", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Sdk")]
|
||||
public void CreateTree_AcceptsRecognizedNamedValue()
|
||||
{
|
||||
var parser = new ExpressionParser();
|
||||
var namedValues = new List<INamedValueInfo>
|
||||
{
|
||||
new NamedValueInfo<ContextValueNode>("inputs"),
|
||||
};
|
||||
|
||||
var node = parser.CreateTree("inputs.foo", null, namedValues, null);
|
||||
|
||||
Assert.NotNull(node);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Sdk")]
|
||||
public void CreateTree_CaseFunctionWorks_WhenAllowed()
|
||||
{
|
||||
var parser = new ExpressionParser();
|
||||
var namedValues = new List<INamedValueInfo>
|
||||
{
|
||||
new NamedValueInfo<ContextValueNode>("github"),
|
||||
};
|
||||
|
||||
var node = parser.CreateTree("case(github.event_name, 'push', 'Push Event')", null, namedValues, null, allowCaseFunction: true);
|
||||
|
||||
Assert.NotNull(node);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Sdk")]
|
||||
public void CreateTree_CaseFunctionRejected_WhenDisallowed()
|
||||
{
|
||||
var parser = new ExpressionParser();
|
||||
var namedValues = new List<INamedValueInfo>
|
||||
{
|
||||
new NamedValueInfo<ContextValueNode>("github"),
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<ParseException>(() =>
|
||||
parser.CreateTree("case(github.event_name, 'push', 'Push Event')", null, namedValues, null, allowCaseFunction: false));
|
||||
|
||||
Assert.Contains("Unrecognized function", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Sdk")]
|
||||
public void CreateTree_CaseFunctionDoesNotAffectUnknownKeywords()
|
||||
{
|
||||
// The key regression test: with allowCaseFunction=true (default),
|
||||
// unrecognized named values must still be rejected.
|
||||
var parser = new ExpressionParser();
|
||||
var namedValues = new List<INamedValueInfo>
|
||||
{
|
||||
new NamedValueInfo<ContextValueNode>("inputs"),
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<ParseException>(() =>
|
||||
parser.CreateTree("github.ref", null, namedValues, null, allowCaseFunction: true));
|
||||
|
||||
Assert.Contains("Unrecognized named-value", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/Test/L0/Sdk/RSWebApi/AgentJobRequestMessageL0.cs
Normal file
76
src/Test/L0/Sdk/RSWebApi/AgentJobRequestMessageL0.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization.Json;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
using GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Actions.RunService.WebApi.Tests;
|
||||
|
||||
public sealed class AgentJobRequestMessageL0
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Common")]
|
||||
public void VerifyEnableDebuggerDeserialization_WithTrue()
|
||||
{
|
||||
// Arrange
|
||||
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
|
||||
string jsonWithEnabledDebugger = DoubleQuotify("{'EnableDebugger': true}");
|
||||
|
||||
// Act
|
||||
using var stream = new MemoryStream();
|
||||
stream.Write(Encoding.UTF8.GetBytes(jsonWithEnabledDebugger));
|
||||
stream.Position = 0;
|
||||
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(recoveredMessage);
|
||||
Assert.True(recoveredMessage.EnableDebugger, "EnableDebugger should be true when JSON contains 'EnableDebugger': true");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Common")]
|
||||
public void VerifyEnableDebuggerDeserialization_DefaultToFalse()
|
||||
{
|
||||
// Arrange
|
||||
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
|
||||
string jsonWithoutDebugger = DoubleQuotify("{'messageType': 'PipelineAgentJobRequest'}");
|
||||
|
||||
// Act
|
||||
using var stream = new MemoryStream();
|
||||
stream.Write(Encoding.UTF8.GetBytes(jsonWithoutDebugger));
|
||||
stream.Position = 0;
|
||||
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(recoveredMessage);
|
||||
Assert.False(recoveredMessage.EnableDebugger, "EnableDebugger should default to false when JSON field is absent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Common")]
|
||||
public void VerifyEnableDebuggerDeserialization_WithFalse()
|
||||
{
|
||||
// Arrange
|
||||
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
|
||||
string jsonWithDisabledDebugger = DoubleQuotify("{'EnableDebugger': false}");
|
||||
|
||||
// Act
|
||||
using var stream = new MemoryStream();
|
||||
stream.Write(Encoding.UTF8.GetBytes(jsonWithDisabledDebugger));
|
||||
stream.Position = 0;
|
||||
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(recoveredMessage);
|
||||
Assert.False(recoveredMessage.EnableDebugger, "EnableDebugger should be false when JSON contains 'EnableDebugger': false");
|
||||
}
|
||||
|
||||
private static string DoubleQuotify(string text)
|
||||
{
|
||||
return text.Replace('\'', '"');
|
||||
}
|
||||
}
|
||||
@@ -198,8 +198,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
Func<Task> action = async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions);
|
||||
|
||||
//Assert
|
||||
var ex = await Assert.ThrowsAsync<FailedToDownloadActionException>(action);
|
||||
Assert.IsType<ActionNotFoundException>(ex.InnerException);
|
||||
await Assert.ThrowsAsync<ActionNotFoundException>(action);
|
||||
|
||||
var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "main.completed");
|
||||
Assert.False(File.Exists(watermarkFile));
|
||||
|
||||
@@ -928,58 +928,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Load_ContainerAction_RejectsInvalidExpressionContext()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
|
||||
var actionManifest = new ActionManifestManager();
|
||||
actionManifest.Initialize(_hc);
|
||||
|
||||
// Act & Assert — github is not a valid context for container-runs-env (only inputs is allowed)
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_env_invalid_context.yml")));
|
||||
|
||||
Assert.Contains("Failed to load", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Load_ContainerAction_AcceptsValidExpressionContext()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
|
||||
var actionManifest = new ActionManifestManager();
|
||||
actionManifest.Initialize(_hc);
|
||||
|
||||
// Act — inputs is a valid context for container-runs-env
|
||||
var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_arg_env_expression.yml"));
|
||||
|
||||
// Assert
|
||||
var containerAction = result.Execution as ContainerActionExecutionDataNew;
|
||||
Assert.NotNull(containerAction);
|
||||
Assert.Equal("${{ inputs.entryPoint }}", containerAction.Environment[1].Value.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
private void Setup([CallerMemberName] string name = "")
|
||||
{
|
||||
_ecTokenSource?.Dispose();
|
||||
|
||||
@@ -926,58 +926,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Load_ContainerAction_RejectsInvalidExpressionContext()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
|
||||
var actionManifest = new ActionManifestManagerLegacy();
|
||||
actionManifest.Initialize(_hc);
|
||||
|
||||
// Act & Assert — github is not a valid context for container-runs-env (only inputs is allowed)
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_env_invalid_context.yml")));
|
||||
|
||||
Assert.Contains("Failed to load", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Load_ContainerAction_AcceptsValidExpressionContext()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
|
||||
var actionManifest = new ActionManifestManagerLegacy();
|
||||
actionManifest.Initialize(_hc);
|
||||
|
||||
// Act — inputs is a valid context for container-runs-env
|
||||
var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_arg_env_expression.yml"));
|
||||
|
||||
// Assert
|
||||
var containerAction = result.Execution as ContainerActionExecutionData;
|
||||
Assert.NotNull(containerAction);
|
||||
Assert.Equal("${{ inputs.entryPoint }}", containerAction.Environment[1].Value.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
private void Setup([CallerMemberName] string name = "")
|
||||
{
|
||||
_ecTokenSource?.Dispose();
|
||||
|
||||
@@ -379,40 +379,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Load_BothParsersRejectInvalidExpressionContext()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange — regression test: both parsers must reject github context
|
||||
// in container-runs-env (only inputs is allowed per schema)
|
||||
Setup();
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||
|
||||
var legacyManager = new ActionManifestManagerLegacy();
|
||||
legacyManager.Initialize(_hc);
|
||||
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
|
||||
|
||||
var newManager = new ActionManifestManager();
|
||||
newManager.Initialize(_hc);
|
||||
_hc.SetSingleton<IActionManifestManager>(newManager);
|
||||
|
||||
var wrapper = new ActionManifestManagerWrapper();
|
||||
wrapper.Initialize(_hc);
|
||||
|
||||
var manifestPath = Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_env_invalid_context.yml");
|
||||
|
||||
// Act & Assert — both parsers should reject, wrapper should throw
|
||||
Assert.Throws<ArgumentException>(() => wrapper.Load(_ec.Object, manifestPath));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFullExceptionMessage(Exception ex)
|
||||
{
|
||||
var messages = new List<string>();
|
||||
|
||||
611
src/Test/L0/Worker/DapDebugSessionL0.cs
Normal file
611
src/Test/L0/Worker/DapDebugSessionL0.cs
Normal file
@@ -0,0 +1,611 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Worker;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
using Moq;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
public sealed class DapDebugSessionL0
|
||||
{
|
||||
private DapDebugSession _session;
|
||||
private Mock<IDapServer> _mockServer;
|
||||
private List<Event> _sentEvents;
|
||||
private List<Response> _sentResponses;
|
||||
|
||||
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
|
||||
{
|
||||
var hc = new TestHostContext(this, testName);
|
||||
|
||||
_session = new DapDebugSession();
|
||||
_session.Initialize(hc);
|
||||
|
||||
_sentEvents = new List<Event>();
|
||||
_sentResponses = new List<Response>();
|
||||
|
||||
_mockServer = new Mock<IDapServer>();
|
||||
_mockServer.Setup(x => x.SendEvent(It.IsAny<Event>()))
|
||||
.Callback<Event>(e => _sentEvents.Add(e));
|
||||
_mockServer.Setup(x => x.SendResponse(It.IsAny<Response>()))
|
||||
.Callback<Response>(r => _sentResponses.Add(r));
|
||||
|
||||
_session.SetDapServer(_mockServer.Object);
|
||||
|
||||
return hc;
|
||||
}
|
||||
|
||||
private Mock<IStep> CreateMockStep(string displayName, TaskResult? result = null)
|
||||
{
|
||||
var mockEc = new Mock<IExecutionContext>();
|
||||
mockEc.SetupAllProperties();
|
||||
mockEc.Object.Result = result;
|
||||
|
||||
var mockStep = new Mock<IStep>();
|
||||
mockStep.Setup(x => x.DisplayName).Returns(displayName);
|
||||
mockStep.Setup(x => x.ExecutionContext).Returns(mockEc.Object);
|
||||
|
||||
return mockStep;
|
||||
}
|
||||
|
||||
private Mock<IExecutionContext> CreateMockJobContext()
|
||||
{
|
||||
var mockJobContext = new Mock<IExecutionContext>();
|
||||
mockJobContext.Setup(x => x.GetGitHubContext("job")).Returns("test-job");
|
||||
return mockJobContext;
|
||||
}
|
||||
|
||||
private async Task InitializeSessionAsync()
|
||||
{
|
||||
var initJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 1,
|
||||
Type = "request",
|
||||
Command = "initialize"
|
||||
});
|
||||
await _session.HandleMessageAsync(initJson, CancellationToken.None);
|
||||
|
||||
var attachJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 2,
|
||||
Type = "request",
|
||||
Command = "attach"
|
||||
});
|
||||
await _session.HandleMessageAsync(attachJson, CancellationToken.None);
|
||||
|
||||
var configJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 3,
|
||||
Type = "request",
|
||||
Command = "configurationDone"
|
||||
});
|
||||
await _session.HandleMessageAsync(configJson, CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void InitialStateIsWaitingForConnection()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
Assert.Equal(DapSessionState.WaitingForConnection, _session.State);
|
||||
Assert.False(_session.IsActive);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task InitializeHandlerSetsInitializingState()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 1,
|
||||
Type = "request",
|
||||
Command = "initialize"
|
||||
});
|
||||
|
||||
await _session.HandleMessageAsync(json, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DapSessionState.Initializing, _session.State);
|
||||
Assert.Single(_sentResponses);
|
||||
Assert.True(_sentResponses[0].Success);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task ConfigurationDoneSetsReadyState()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
|
||||
Assert.Equal(DapSessionState.Ready, _session.State);
|
||||
Assert.True(_session.IsActive);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task OnStepStartingPausesAndSendsStoppedEvent()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_session.HandleClientConnected();
|
||||
|
||||
// Wait for the async initialized event to arrive, then clear
|
||||
await Task.Delay(200);
|
||||
_sentEvents.Clear();
|
||||
|
||||
var step = CreateMockStep("Checkout code");
|
||||
var jobContext = CreateMockJobContext();
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
var stepTask = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, cts.Token);
|
||||
|
||||
await Task.Delay(100);
|
||||
Assert.False(stepTask.IsCompleted);
|
||||
Assert.Equal(DapSessionState.Paused, _session.State);
|
||||
|
||||
var stoppedEvents = _sentEvents.FindAll(e => e.EventType == "stopped");
|
||||
Assert.Single(stoppedEvents);
|
||||
|
||||
var continueJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 10,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
|
||||
|
||||
await Task.WhenAny(stepTask, Task.Delay(5000));
|
||||
Assert.True(stepTask.IsCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task NextCommandPausesOnFollowingStep()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_session.HandleClientConnected();
|
||||
_sentEvents.Clear();
|
||||
|
||||
var step1 = CreateMockStep("Step 1");
|
||||
var jobContext = CreateMockJobContext();
|
||||
|
||||
var step1Task = _session.OnStepStartingAsync(step1.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
|
||||
|
||||
var nextJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 10,
|
||||
Type = "request",
|
||||
Command = "next"
|
||||
});
|
||||
await _session.HandleMessageAsync(nextJson, CancellationToken.None);
|
||||
await Task.WhenAny(step1Task, Task.Delay(5000));
|
||||
Assert.True(step1Task.IsCompleted);
|
||||
|
||||
_session.OnStepCompleted(step1.Object);
|
||||
_sentEvents.Clear();
|
||||
|
||||
var step2 = CreateMockStep("Step 2");
|
||||
var step2Task = _session.OnStepStartingAsync(step2.Object, jobContext.Object, isFirstStep: false, CancellationToken.None);
|
||||
|
||||
await Task.Delay(100);
|
||||
Assert.False(step2Task.IsCompleted);
|
||||
Assert.Equal(DapSessionState.Paused, _session.State);
|
||||
|
||||
var stoppedEvents = _sentEvents.FindAll(e => e.EventType == "stopped");
|
||||
Assert.Single(stoppedEvents);
|
||||
|
||||
var continueJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 11,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
|
||||
await Task.WhenAny(step2Task, Task.Delay(5000));
|
||||
Assert.True(step2Task.IsCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task ContinueCommandSkipsNextPause()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_session.HandleClientConnected();
|
||||
_sentEvents.Clear();
|
||||
|
||||
var step1 = CreateMockStep("Step 1");
|
||||
var jobContext = CreateMockJobContext();
|
||||
|
||||
var step1Task = _session.OnStepStartingAsync(step1.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
|
||||
|
||||
var continueJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 10,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
|
||||
await Task.WhenAny(step1Task, Task.Delay(5000));
|
||||
Assert.True(step1Task.IsCompleted);
|
||||
|
||||
_session.OnStepCompleted(step1.Object);
|
||||
_sentEvents.Clear();
|
||||
|
||||
var step2 = CreateMockStep("Step 2");
|
||||
var step2Task = _session.OnStepStartingAsync(step2.Object, jobContext.Object, isFirstStep: false, CancellationToken.None);
|
||||
|
||||
await Task.WhenAny(step2Task, Task.Delay(5000));
|
||||
Assert.True(step2Task.IsCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task CancellationUnblocksPausedStep()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_session.HandleClientConnected();
|
||||
_sentEvents.Clear();
|
||||
|
||||
var step = CreateMockStep("Step 1");
|
||||
var jobContext = CreateMockJobContext();
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
var stepTask = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, cts.Token);
|
||||
|
||||
await Task.Delay(100);
|
||||
Assert.False(stepTask.IsCompleted);
|
||||
Assert.Equal(DapSessionState.Paused, _session.State);
|
||||
|
||||
cts.Cancel();
|
||||
|
||||
await Task.WhenAny(stepTask, Task.Delay(5000));
|
||||
Assert.True(stepTask.IsCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task CancelSessionSendsTerminatedAndExitedEvents()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_sentEvents.Clear();
|
||||
|
||||
_session.CancelSession();
|
||||
|
||||
Assert.Equal(DapSessionState.Terminated, _session.State);
|
||||
Assert.False(_session.IsActive);
|
||||
|
||||
var terminatedEvents = _sentEvents.FindAll(e => e.EventType == "terminated");
|
||||
var exitedEvents = _sentEvents.FindAll(e => e.EventType == "exited");
|
||||
Assert.Single(terminatedEvents);
|
||||
Assert.Single(exitedEvents);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task CancelSessionReleasesBlockedStep()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_session.HandleClientConnected();
|
||||
_sentEvents.Clear();
|
||||
|
||||
var step = CreateMockStep("Blocked Step");
|
||||
var jobContext = CreateMockJobContext();
|
||||
|
||||
var stepTask = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
|
||||
|
||||
await Task.Delay(100);
|
||||
Assert.False(stepTask.IsCompleted);
|
||||
|
||||
_session.CancelSession();
|
||||
|
||||
await Task.WhenAny(stepTask, Task.Delay(5000));
|
||||
Assert.True(stepTask.IsCompleted);
|
||||
Assert.Equal(DapSessionState.Terminated, _session.State);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task ReconnectionResendStoppedEvent()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_session.HandleClientConnected();
|
||||
|
||||
// Wait for the async initialized event to arrive, then clear
|
||||
await Task.Delay(200);
|
||||
_sentEvents.Clear();
|
||||
|
||||
var step = CreateMockStep("Step 1");
|
||||
var jobContext = CreateMockJobContext();
|
||||
|
||||
var stepTask = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
|
||||
|
||||
await Task.Delay(100);
|
||||
Assert.Equal(DapSessionState.Paused, _session.State);
|
||||
var stoppedEvents = _sentEvents.FindAll(e => e.EventType == "stopped");
|
||||
Assert.Single(stoppedEvents);
|
||||
|
||||
_session.HandleClientDisconnected();
|
||||
Assert.Equal(DapSessionState.Paused, _session.State);
|
||||
|
||||
_sentEvents.Clear();
|
||||
_session.HandleClientConnected();
|
||||
|
||||
Assert.Single(_sentEvents);
|
||||
Assert.Equal("stopped", _sentEvents[0].EventType);
|
||||
|
||||
var continueJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 20,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
|
||||
await Task.WhenAny(stepTask, Task.Delay(5000));
|
||||
Assert.True(stepTask.IsCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task DisconnectCommandTerminatesSession()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
|
||||
var disconnectJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 10,
|
||||
Type = "request",
|
||||
Command = "disconnect"
|
||||
});
|
||||
await _session.HandleMessageAsync(disconnectJson, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DapSessionState.Terminated, _session.State);
|
||||
Assert.False(_session.IsActive);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task OnStepCompletedTracksCompletedSteps()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_session.HandleClientConnected();
|
||||
|
||||
var step1 = CreateMockStep("Step 1");
|
||||
step1.Object.ExecutionContext.Result = TaskResult.Succeeded;
|
||||
var jobContext = CreateMockJobContext();
|
||||
|
||||
var step1Task = _session.OnStepStartingAsync(step1.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
|
||||
|
||||
var continueJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 10,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
|
||||
await Task.WhenAny(step1Task, Task.Delay(5000));
|
||||
|
||||
_session.OnStepCompleted(step1.Object);
|
||||
|
||||
var stackTraceJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 11,
|
||||
Type = "request",
|
||||
Command = "stackTrace"
|
||||
});
|
||||
_sentResponses.Clear();
|
||||
await _session.HandleMessageAsync(stackTraceJson, CancellationToken.None);
|
||||
|
||||
Assert.Single(_sentResponses);
|
||||
Assert.True(_sentResponses[0].Success);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task OnJobCompletedSendsTerminatedAndExitedEvents()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_sentEvents.Clear();
|
||||
|
||||
_session.OnJobCompleted();
|
||||
|
||||
Assert.Equal(DapSessionState.Terminated, _session.State);
|
||||
|
||||
var terminatedEvents = _sentEvents.FindAll(e => e.EventType == "terminated");
|
||||
var exitedEvents = _sentEvents.FindAll(e => e.EventType == "exited");
|
||||
Assert.Single(terminatedEvents);
|
||||
Assert.Single(exitedEvents);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task OnStepStartingNoOpWhenNotActive()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var step = CreateMockStep("Step 1");
|
||||
var jobContext = CreateMockJobContext();
|
||||
|
||||
var task = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
|
||||
|
||||
await Task.WhenAny(task, Task.Delay(5000));
|
||||
Assert.True(task.IsCompleted);
|
||||
|
||||
_mockServer.Verify(x => x.SendEvent(It.IsAny<Event>()), Times.Never);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task ThreadsCommandReturnsJobThread()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
|
||||
var threadsJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 10,
|
||||
Type = "request",
|
||||
Command = "threads"
|
||||
});
|
||||
_sentResponses.Clear();
|
||||
await _session.HandleMessageAsync(threadsJson, CancellationToken.None);
|
||||
|
||||
Assert.Single(_sentResponses);
|
||||
Assert.True(_sentResponses[0].Success);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task UnsupportedCommandReturnsErrorResponse()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
|
||||
var json = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 99,
|
||||
Type = "request",
|
||||
Command = "stepIn"
|
||||
});
|
||||
_sentResponses.Clear();
|
||||
await _session.HandleMessageAsync(json, CancellationToken.None);
|
||||
|
||||
Assert.Single(_sentResponses);
|
||||
Assert.False(_sentResponses[0].Success);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task FullFlowInitAttachConfigStepContinueComplete()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_session.HandleClientConnected();
|
||||
_sentEvents.Clear();
|
||||
_sentResponses.Clear();
|
||||
|
||||
Assert.Equal(DapSessionState.Ready, _session.State);
|
||||
|
||||
var step = CreateMockStep("Run tests");
|
||||
var jobContext = CreateMockJobContext();
|
||||
|
||||
var stepTask = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
|
||||
|
||||
await Task.Delay(100);
|
||||
Assert.Equal(DapSessionState.Paused, _session.State);
|
||||
|
||||
var stoppedEvents = _sentEvents.FindAll(e => e.EventType == "stopped");
|
||||
Assert.Single(stoppedEvents);
|
||||
|
||||
var continueJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 10,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
|
||||
await Task.WhenAny(stepTask, Task.Delay(5000));
|
||||
Assert.True(stepTask.IsCompleted);
|
||||
|
||||
var continuedEvents = _sentEvents.FindAll(e => e.EventType == "continued");
|
||||
Assert.Single(continuedEvents);
|
||||
|
||||
step.Object.ExecutionContext.Result = TaskResult.Succeeded;
|
||||
_session.OnStepCompleted(step.Object);
|
||||
|
||||
_sentEvents.Clear();
|
||||
_session.OnJobCompleted();
|
||||
|
||||
Assert.Equal(DapSessionState.Terminated, _session.State);
|
||||
var terminatedEvents = _sentEvents.FindAll(e => e.EventType == "terminated");
|
||||
var exitedEvents = _sentEvents.FindAll(e => e.EventType == "exited");
|
||||
Assert.Single(terminatedEvents);
|
||||
Assert.Single(exitedEvents);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task DoubleCancelSessionIsIdempotent()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_sentEvents.Clear();
|
||||
|
||||
_session.CancelSession();
|
||||
_session.CancelSession();
|
||||
|
||||
Assert.Equal(DapSessionState.Terminated, _session.State);
|
||||
|
||||
var terminatedEvents = _sentEvents.FindAll(e => e.EventType == "terminated");
|
||||
Assert.Single(terminatedEvents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
233
src/Test/L0/Worker/DapMessagesL0.cs
Normal file
233
src/Test/L0/Worker/DapMessagesL0.cs
Normal file
@@ -0,0 +1,233 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
public sealed class DapMessagesL0
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void RequestSerializesCorrectly()
|
||||
{
|
||||
var request = new Request
|
||||
{
|
||||
Seq = 1,
|
||||
Type = "request",
|
||||
Command = "initialize",
|
||||
Arguments = JObject.FromObject(new { clientID = "test-client" })
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(request);
|
||||
var deserialized = JsonConvert.DeserializeObject<Request>(json);
|
||||
|
||||
Assert.Equal(1, deserialized.Seq);
|
||||
Assert.Equal("request", deserialized.Type);
|
||||
Assert.Equal("initialize", deserialized.Command);
|
||||
Assert.Equal("test-client", deserialized.Arguments["clientID"].ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ResponseSerializesCorrectly()
|
||||
{
|
||||
var response = new Response
|
||||
{
|
||||
Seq = 2,
|
||||
Type = "response",
|
||||
RequestSeq = 1,
|
||||
Success = true,
|
||||
Command = "initialize",
|
||||
Body = new Capabilities { SupportsConfigurationDoneRequest = true }
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(response);
|
||||
var deserialized = JsonConvert.DeserializeObject<Response>(json);
|
||||
|
||||
Assert.Equal(2, deserialized.Seq);
|
||||
Assert.Equal("response", deserialized.Type);
|
||||
Assert.Equal(1, deserialized.RequestSeq);
|
||||
Assert.True(deserialized.Success);
|
||||
Assert.Equal("initialize", deserialized.Command);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void EventSerializesWithCorrectType()
|
||||
{
|
||||
var evt = new Event
|
||||
{
|
||||
EventType = "stopped",
|
||||
Body = new StoppedEventBody
|
||||
{
|
||||
Reason = "entry",
|
||||
Description = "Stopped at entry",
|
||||
ThreadId = 1,
|
||||
AllThreadsStopped = true
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Equal("event", evt.Type);
|
||||
|
||||
var json = JsonConvert.SerializeObject(evt);
|
||||
Assert.Contains("\"type\":\"event\"", json);
|
||||
Assert.Contains("\"event\":\"stopped\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void StoppedEventBodyOmitsNullFields()
|
||||
{
|
||||
var body = new StoppedEventBody
|
||||
{
|
||||
Reason = "step"
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(body);
|
||||
Assert.Contains("\"reason\":\"step\"", json);
|
||||
Assert.DoesNotContain("\"threadId\"", json);
|
||||
Assert.DoesNotContain("\"allThreadsStopped\"", json);
|
||||
Assert.DoesNotContain("\"description\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void CapabilitiesMvpDefaults()
|
||||
{
|
||||
var caps = new Capabilities
|
||||
{
|
||||
SupportsConfigurationDoneRequest = true,
|
||||
SupportsFunctionBreakpoints = false,
|
||||
SupportsStepBack = false
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(caps);
|
||||
var deserialized = JsonConvert.DeserializeObject<Capabilities>(json);
|
||||
|
||||
Assert.True(deserialized.SupportsConfigurationDoneRequest);
|
||||
Assert.False(deserialized.SupportsFunctionBreakpoints);
|
||||
Assert.False(deserialized.SupportsStepBack);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ContinueResponseBodySerialization()
|
||||
{
|
||||
var body = new ContinueResponseBody { AllThreadsContinued = true };
|
||||
var json = JsonConvert.SerializeObject(body);
|
||||
var deserialized = JsonConvert.DeserializeObject<ContinueResponseBody>(json);
|
||||
|
||||
Assert.True(deserialized.AllThreadsContinued);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ThreadsResponseBodySerialization()
|
||||
{
|
||||
var body = new ThreadsResponseBody
|
||||
{
|
||||
Threads = new List<Thread>
|
||||
{
|
||||
new Thread { Id = 1, Name = "Job Thread" }
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(body);
|
||||
var deserialized = JsonConvert.DeserializeObject<ThreadsResponseBody>(json);
|
||||
|
||||
Assert.Single(deserialized.Threads);
|
||||
Assert.Equal(1, deserialized.Threads[0].Id);
|
||||
Assert.Equal("Job Thread", deserialized.Threads[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void StackFrameSerialization()
|
||||
{
|
||||
var frame = new StackFrame
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Step: Checkout",
|
||||
Line = 1,
|
||||
Column = 1,
|
||||
PresentationHint = "normal"
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(frame);
|
||||
var deserialized = JsonConvert.DeserializeObject<StackFrame>(json);
|
||||
|
||||
Assert.Equal(1, deserialized.Id);
|
||||
Assert.Equal("Step: Checkout", deserialized.Name);
|
||||
Assert.Equal("normal", deserialized.PresentationHint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ExitedEventBodySerialization()
|
||||
{
|
||||
var body = new ExitedEventBody { ExitCode = 130 };
|
||||
var json = JsonConvert.SerializeObject(body);
|
||||
var deserialized = JsonConvert.DeserializeObject<ExitedEventBody>(json);
|
||||
|
||||
Assert.Equal(130, deserialized.ExitCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void DapCommandEnumValues()
|
||||
{
|
||||
Assert.Equal(0, (int)DapCommand.Continue);
|
||||
Assert.Equal(1, (int)DapCommand.Next);
|
||||
Assert.Equal(4, (int)DapCommand.Disconnect);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void RequestDeserializesFromRawJson()
|
||||
{
|
||||
var json = @"{""seq"":5,""type"":""request"",""command"":""continue"",""arguments"":{""threadId"":1}}";
|
||||
var request = JsonConvert.DeserializeObject<Request>(json);
|
||||
|
||||
Assert.Equal(5, request.Seq);
|
||||
Assert.Equal("request", request.Type);
|
||||
Assert.Equal("continue", request.Command);
|
||||
Assert.Equal(1, request.Arguments["threadId"].Value<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ErrorResponseBodySerialization()
|
||||
{
|
||||
var body = new ErrorResponseBody
|
||||
{
|
||||
Error = new Message
|
||||
{
|
||||
Id = 1,
|
||||
Format = "Something went wrong",
|
||||
ShowUser = true
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(body);
|
||||
var deserialized = JsonConvert.DeserializeObject<ErrorResponseBody>(json);
|
||||
|
||||
Assert.Equal(1, deserialized.Error.Id);
|
||||
Assert.Equal("Something went wrong", deserialized.Error.Format);
|
||||
Assert.True(deserialized.Error.ShowUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
170
src/Test/L0/Worker/DapServerL0.cs
Normal file
170
src/Test/L0/Worker/DapServerL0.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
public sealed class DapServerL0
|
||||
{
|
||||
private DapServer _server;
|
||||
|
||||
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
|
||||
{
|
||||
var hc = new TestHostContext(this, testName);
|
||||
_server = new DapServer();
|
||||
_server.Initialize(hc);
|
||||
return hc;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void InitializeSucceeds()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
Assert.NotNull(_server);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetSessionAcceptsMock()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var mockSession = new Mock<IDapDebugSession>();
|
||||
_server.SetSession(mockSession.Object);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SendEventNoClientDoesNotThrow()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var evt = new Event
|
||||
{
|
||||
EventType = "stopped",
|
||||
Body = new StoppedEventBody
|
||||
{
|
||||
Reason = "entry",
|
||||
ThreadId = 1,
|
||||
AllThreadsStopped = true
|
||||
}
|
||||
};
|
||||
|
||||
_server.SendEvent(evt);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SendResponseNoClientDoesNotThrow()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var response = new Response
|
||||
{
|
||||
Type = "response",
|
||||
RequestSeq = 1,
|
||||
Command = "initialize",
|
||||
Success = true
|
||||
};
|
||||
|
||||
_server.SendResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SendMessageNoClientDoesNotThrow()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var msg = new ProtocolMessage
|
||||
{
|
||||
Type = "response",
|
||||
Seq = 1
|
||||
};
|
||||
|
||||
_server.SendMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task StopWithoutStartDoesNotThrow()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await _server.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task StartAndStopOnAvailablePort()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
await _server.StartAsync(0, cts.Token);
|
||||
await _server.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task WaitForConnectionCancelledByCancellationToken()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
await _server.StartAsync(0, cts.Token);
|
||||
|
||||
var waitTask = _server.WaitForConnectionAsync(cts.Token);
|
||||
|
||||
cts.Cancel();
|
||||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
|
||||
{
|
||||
await waitTask;
|
||||
});
|
||||
|
||||
await _server.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task StartAndStopMultipleTimesDoesNotThrow()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var cts1 = new CancellationTokenSource();
|
||||
await _server.StartAsync(0, cts1.Token);
|
||||
await _server.StopAsync();
|
||||
|
||||
_server = new DapServer();
|
||||
_server.Initialize(CreateTestContext());
|
||||
var cts2 = new CancellationTokenSource();
|
||||
await _server.StartAsync(0, cts2.Token);
|
||||
await _server.StopAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
public sealed class PipelineTemplateEvaluatorWrapperL0
|
||||
{
|
||||
private CancellationTokenSource _ecTokenSource;
|
||||
private CancellationTokenSource _rootTokenSource;
|
||||
private Mock<IExecutionContext> _ec;
|
||||
private TestHostContext _hc;
|
||||
|
||||
@@ -66,7 +65,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
|
||||
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
||||
|
||||
// Call EvaluateAndCompare directly: the new evaluator cancels the root token
|
||||
// Call EvaluateAndCompare directly: the new evaluator cancels the token
|
||||
// and returns a different value, forcing hasMismatch = true.
|
||||
// Because cancellation flipped during the evaluation window, the
|
||||
// mismatch should be skipped.
|
||||
@@ -75,7 +74,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
() => "legacy-value",
|
||||
() =>
|
||||
{
|
||||
_rootTokenSource.Cancel();
|
||||
_ecTokenSource.Cancel();
|
||||
return "different-value";
|
||||
},
|
||||
(legacy, @new) => string.Equals(legacy, @new, StringComparison.Ordinal));
|
||||
@@ -89,43 +88,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void EvaluateAndCompare_SkipsMismatchRecording_WhenRootCancellationOccursBetweenEvaluators()
|
||||
{
|
||||
// Simulates job-level cancellation firing between legacy and new evaluator runs.
|
||||
// Root is mocked with a separate CancellationTokenSource to exercise the
|
||||
// _context.Root?.CancellationToken path (the job-level token).
|
||||
try
|
||||
{
|
||||
Setup();
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
||||
|
||||
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
||||
|
||||
// Legacy evaluator cancels the root token (simulating job cancel) and returns a value.
|
||||
// The new evaluator returns a different value. The mismatch should be skipped.
|
||||
var result = wrapper.EvaluateAndCompare<string, string>(
|
||||
"TestRootCancellationSkip",
|
||||
() =>
|
||||
{
|
||||
var legacyValue = "legacy-value";
|
||||
_rootTokenSource.Cancel();
|
||||
return legacyValue;
|
||||
},
|
||||
() => "different-value",
|
||||
(legacy, @new) => string.Equals(legacy, @new, StringComparison.Ordinal));
|
||||
|
||||
Assert.Equal("legacy-value", result);
|
||||
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
@@ -900,8 +862,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
_ecTokenSource?.Dispose();
|
||||
_ecTokenSource = new CancellationTokenSource();
|
||||
_rootTokenSource?.Dispose();
|
||||
_rootTokenSource = new CancellationTokenSource();
|
||||
|
||||
_hc = new TestHostContext(this, name);
|
||||
|
||||
@@ -917,9 +877,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
WriteDebug = true,
|
||||
});
|
||||
_ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token);
|
||||
var rootEc = new Mock<IExecutionContext>();
|
||||
rootEc.Setup(x => x.CancellationToken).Returns(_rootTokenSource.Token);
|
||||
_ec.Setup(x => x.Root).Returns(rootEc.Object);
|
||||
_ec.Setup(x => x.ExpressionValues).Returns(expressionValues);
|
||||
_ec.Setup(x => x.ExpressionFunctions).Returns(expressionFunctions);
|
||||
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { _hc.GetTrace().Info($"{tag}{message}"); });
|
||||
@@ -928,8 +885,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
|
||||
private void Teardown()
|
||||
{
|
||||
_ecTokenSource?.Dispose();
|
||||
_rootTokenSource?.Dispose();
|
||||
_hc?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
name: 'Action With Invalid Context'
|
||||
description: 'Docker action that uses github context in env (only inputs is allowed)'
|
||||
inputs:
|
||||
my-input:
|
||||
description: 'A test input'
|
||||
required: false
|
||||
default: 'hello'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
||||
env:
|
||||
VALID: '${{ inputs.my-input }}'
|
||||
INVALID: '${{ github.event.repository.private }}'
|
||||
Reference in New Issue
Block a user