mirror of
https://github.com/actions/runner.git
synced 2025-12-10 20:36:49 +00:00
Compare commits
29 Commits
v2.302.1
...
users/jww3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afcca9bfa4 | ||
|
|
157e03616e | ||
|
|
30f686b9c2 | ||
|
|
ec5d72810f | ||
|
|
0484afeec7 | ||
|
|
1ceb1a67f2 | ||
|
|
9f778b814d | ||
|
|
92258f9ea1 | ||
|
|
74eeb82684 | ||
|
|
0e7ca9aedb | ||
|
|
bb7b1e8259 | ||
|
|
440c81b770 | ||
|
|
9958fc0374 | ||
|
|
81b07eb1c4 | ||
|
|
514ecec5a3 | ||
|
|
128b212b13 | ||
|
|
2dfa28e6e0 | ||
|
|
fd96246580 | ||
|
|
8ef48200b4 | ||
|
|
d61b27b839 | ||
|
|
542e8a3c98 | ||
|
|
e8975514fd | ||
|
|
0befa62f64 | ||
|
|
aaf02ab34c | ||
|
|
02c9d1c704 | ||
|
|
982784d704 | ||
|
|
8c096baf49 | ||
|
|
8d6972e38b | ||
|
|
1ab35b0938 |
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v2
|
||||||
# Override language selection by uncommenting this and choosing your languages
|
# Override language selection by uncommenting this and choosing your languages
|
||||||
# with:
|
# with:
|
||||||
# languages: go, javascript, csharp, python, cpp, java
|
# languages: go, javascript, csharp, python, cpp, java
|
||||||
@@ -38,4 +38,4 @@ jobs:
|
|||||||
working-directory: src
|
working-directory: src
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|||||||
1
.github/workflows/lint.yml
vendored
1
.github/workflows/lint.yml
vendored
@@ -18,7 +18,6 @@ jobs:
|
|||||||
uses: github/super-linter@v4
|
uses: github/super-linter@v4
|
||||||
env:
|
env:
|
||||||
DEFAULT_BRANCH: ${{ github.base_ref }}
|
DEFAULT_BRANCH: ${{ github.base_ref }}
|
||||||
DISABLE_ERRORS: true
|
|
||||||
EDITORCONFIG_FILE_NAME: .editorconfig
|
EDITORCONFIG_FILE_NAME: .editorconfig
|
||||||
LINTER_RULES_PATH: /src/
|
LINTER_RULES_PATH: /src/
|
||||||
VALIDATE_ALL_CODEBASE: false
|
VALIDATE_ALL_CODEBASE: false
|
||||||
|
|||||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -131,7 +131,7 @@ jobs:
|
|||||||
file=$(ls)
|
file=$(ls)
|
||||||
sha=$(sha256sum $file | awk '{ print $1 }')
|
sha=$(sha256sum $file | awk '{ print $1 }')
|
||||||
echo "Computed sha256: $sha for $file"
|
echo "Computed sha256: $sha for $file"
|
||||||
echo "::set-output name=${{matrix.runtime}}-sha256::$sha"
|
echo "${{matrix.runtime}}-sha256=$sha" >> $GITHUB_OUTPUT
|
||||||
shell: bash
|
shell: bash
|
||||||
id: sha
|
id: sha
|
||||||
name: Compute SHA256
|
name: Compute SHA256
|
||||||
@@ -140,8 +140,8 @@ jobs:
|
|||||||
file=$(ls)
|
file=$(ls)
|
||||||
sha=$(sha256sum $file | awk '{ print $1 }')
|
sha=$(sha256sum $file | awk '{ print $1 }')
|
||||||
echo "Computed sha256: $sha for $file"
|
echo "Computed sha256: $sha for $file"
|
||||||
echo "::set-output name=${{matrix.runtime}}-sha256::$sha"
|
echo "${{matrix.runtime}}-sha256=$sha" >> $GITHUB_OUTPUT
|
||||||
echo "::set-output name=sha256::$sha"
|
echo "sha256=$sha" >> $GITHUB_OUTPUT
|
||||||
shell: bash
|
shell: bash
|
||||||
id: sha_noexternals
|
id: sha_noexternals
|
||||||
name: Compute SHA256
|
name: Compute SHA256
|
||||||
@@ -150,8 +150,8 @@ jobs:
|
|||||||
file=$(ls)
|
file=$(ls)
|
||||||
sha=$(sha256sum $file | awk '{ print $1 }')
|
sha=$(sha256sum $file | awk '{ print $1 }')
|
||||||
echo "Computed sha256: $sha for $file"
|
echo "Computed sha256: $sha for $file"
|
||||||
echo "::set-output name=${{matrix.runtime}}-sha256::$sha"
|
echo "${{matrix.runtime}}-sha256=$sha" >> $GITHUB_OUTPUT
|
||||||
echo "::set-output name=sha256::$sha"
|
echo "sha256=$sha" >> $GITHUB_OUTPUT
|
||||||
shell: bash
|
shell: bash
|
||||||
id: sha_noruntime
|
id: sha_noruntime
|
||||||
name: Compute SHA256
|
name: Compute SHA256
|
||||||
@@ -160,8 +160,8 @@ jobs:
|
|||||||
file=$(ls)
|
file=$(ls)
|
||||||
sha=$(sha256sum $file | awk '{ print $1 }')
|
sha=$(sha256sum $file | awk '{ print $1 }')
|
||||||
echo "Computed sha256: $sha for $file"
|
echo "Computed sha256: $sha for $file"
|
||||||
echo "::set-output name=${{matrix.runtime}}-sha256::$sha"
|
echo "${{matrix.runtime}}-sha256=$sha" >> $GITHUB_OUTPUT
|
||||||
echo "::set-output name=sha256::$sha"
|
echo "sha256=$sha" >> $GITHUB_OUTPUT
|
||||||
shell: bash
|
shell: bash
|
||||||
id: sha_noruntime_noexternals
|
id: sha_noruntime_noexternals
|
||||||
name: Compute SHA256
|
name: Compute SHA256
|
||||||
|
|||||||
65
docs/adrs/2494-runner-image-tags.md
Normal file
65
docs/adrs/2494-runner-image-tags.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# ADR 2494: Runner Image Tags
|
||||||
|
|
||||||
|
**Date**: 2023-03-17
|
||||||
|
|
||||||
|
**Status**: Accepted<!-- |Accepted|Rejected|Superceded|Deprecated -->
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Following the [adoption of actions-runner-controller by GitHub](https://github.com/actions/actions-runner-controller/discussions/2072) and the introduction of the new runner scale set autoscaling mode, we needed to provide a basic runner image that could be used off the shelf without much friction.
|
||||||
|
|
||||||
|
The [current runner image](https://github.com/actions/runner/pkgs/container/actions-runner) is published to GHCR. Each release of this image is tagged with the runner version and the most recent release is also tagged with `latest`.
|
||||||
|
|
||||||
|
While the use of `latest` is common practice, we recommend that users pin a specific version of the runner image for a predictable runtime and improved security posture. However, we still notice that a large number of end users are relying on the `latest` tag & raising issues when they encounter problems.
|
||||||
|
|
||||||
|
Add to that, the community actions-runner-controller maintainers have issued a [deprecation notice](https://github.com/actions/actions-runner-controller/issues/2056) of the `latest` tag for the existing runner images (https://github.com/orgs/actions-runner-controller/packages).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Proceed with Option 2, keeping the `latest` tag and adding the `NOTES.txt` file to our helm charts with the notice.
|
||||||
|
|
||||||
|
### Option 1: Remove the `latest` tag
|
||||||
|
|
||||||
|
By removing the `latest` tag, we have to proceed with either of these options:
|
||||||
|
|
||||||
|
1. Remove the runner image reference in the `values.yaml` provided with the `gha-runner-scale-set` helm chart and mark these fields as required so that users have to explicitly specify a runner image and a specific tag. This will obviously introduce more friction for users who want to start using actions-runner-controller for the first time.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: runner
|
||||||
|
image: ""
|
||||||
|
tag: ""
|
||||||
|
command: ["/home/runner/run.sh"]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Pin a specific runner image tag in the `values.yaml` provided with the `gha-runner-scale-set` helm chart. This will reduce friction for users who want to start using actions-runner-controller for the first time but will require us to update the `values.yaml` with every new runner release.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: runner
|
||||||
|
image: "ghcr.io/actions/actions-runner"
|
||||||
|
tag: "v2.300.0"
|
||||||
|
command: ["/home/runner/run.sh"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Keep the `latest` tag
|
||||||
|
|
||||||
|
Keeping the `latest` tag is also a reasonable option especially if we don't expect to make any breaking changes to the runner image. We could enhance this by adding a [NOTES.txt](https://helm.sh/docs/chart_template_guide/notes_files/) to the helm chart which will be displayed to the user after a successful helm install/upgrade. This will help users understand the implications of using the `latest` tag and how to pin a specific version of the runner image.
|
||||||
|
|
||||||
|
The runner image release workflow will need to be updated so that the image is pushed to GHCR and tagged only when the runner rollout has reached all scale units.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Proceeding with **option 1** means:
|
||||||
|
|
||||||
|
1. We will enhance the runtime predictability and security posture of our end users
|
||||||
|
1. We will have to update the `values.yaml` with every new runner release (that can be automated)
|
||||||
|
1. We will introduce friction for users who want to start using actions-runner-controller for the first time
|
||||||
|
|
||||||
|
Proceeding with **option 2** means:
|
||||||
|
|
||||||
|
1. We will have to continue to maintain the `latest` tag
|
||||||
|
1. We will assume that end users will be able to handle the implications of using the `latest` tag
|
||||||
|
1. Runner image release workflow needs to be updated
|
||||||
@@ -158,3 +158,11 @@ cat (Runner/Worker)_TIMESTAMP.log # view your log file
|
|||||||
|
|
||||||
We use the .NET Foundation and CoreCLR style guidelines [located here](
|
We use the .NET Foundation and CoreCLR style guidelines [located here](
|
||||||
https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/coding-style.md)
|
https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/coding-style.md)
|
||||||
|
|
||||||
|
### Format C# Code
|
||||||
|
|
||||||
|
To format both staged and unstaged .cs files
|
||||||
|
```
|
||||||
|
cd ./src
|
||||||
|
./dev.(cmd|sh) format
|
||||||
|
```
|
||||||
@@ -2,7 +2,7 @@ FROM mcr.microsoft.com/dotnet/runtime-deps:6.0 as build
|
|||||||
|
|
||||||
ARG RUNNER_VERSION
|
ARG RUNNER_VERSION
|
||||||
ARG RUNNER_ARCH="x64"
|
ARG RUNNER_ARCH="x64"
|
||||||
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.2.0
|
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.3.1
|
||||||
ARG DOCKER_VERSION=20.10.23
|
ARG DOCKER_VERSION=20.10.23
|
||||||
|
|
||||||
RUN apt update -y && apt install curl unzip -y
|
RUN apt update -y && apt install curl unzip -y
|
||||||
@@ -24,11 +24,26 @@ RUN export DOCKER_ARCH=x86_64 \
|
|||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0
|
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0
|
||||||
|
|
||||||
ENV RUNNER_ALLOW_RUNASROOT=1
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
ENV RUNNER_MANUALLY_TRAP_SIG=1
|
ENV RUNNER_MANUALLY_TRAP_SIG=1
|
||||||
ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1
|
ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1
|
||||||
|
|
||||||
WORKDIR /actions-runner
|
RUN apt-get update -y \
|
||||||
COPY --from=build /actions-runner .
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
sudo \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN install -o root -g root -m 755 docker/* /usr/bin/ && rm -rf docker
|
RUN adduser --disabled-password --gecos "" --uid 1001 runner \
|
||||||
|
&& groupadd docker --gid 123 \
|
||||||
|
&& usermod -aG sudo runner \
|
||||||
|
&& usermod -aG docker runner \
|
||||||
|
&& echo "%sudo ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers \
|
||||||
|
&& echo "Defaults env_keep += \"DEBIAN_FRONTEND\"" >> /etc/sudoers
|
||||||
|
|
||||||
|
WORKDIR /home/runner
|
||||||
|
|
||||||
|
COPY --chown=runner:docker --from=build /actions-runner .
|
||||||
|
|
||||||
|
RUN install -o root -g root -m 755 docker/* /usr/bin/ && rm -rf docker
|
||||||
|
|
||||||
|
USER runner
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
## Features
|
## Features
|
||||||
- Add support for ghe.com domain (#2420)
|
- Support matrix context in output keys (#2477)
|
||||||
- Add docker cli to the runner image. (#2425)
|
- Add update certificates to `./run.sh` if `RUNNER_UPDATE_CA_CERTS` env is set (#2471)
|
||||||
|
- Bypass all proxies for all hosts if `no_proxy='*'` is set (#2395)
|
||||||
|
- Change runner image to make user/folder align with `ubuntu-latest` hosted runner. (#2469)
|
||||||
|
|
||||||
## Bugs
|
## Bugs
|
||||||
- Fix URL construction bug for RunService (#2396)
|
- Exit on runner version deprecation error (#2299)
|
||||||
- Defer evaluation of a step's DisplayName until its condition is evaluated. (#2313)
|
- Runner service exit after consecutive re-try exits (#2426)
|
||||||
- Replace '(' and ')' with '[' and '] from OS.Description for fixing User-Agent header validation (#2288)
|
|
||||||
|
|
||||||
## Misc
|
## Misc
|
||||||
- Bump dotnet sdk to latest version. (#2392)
|
- Replace deprecated command with environment file (#2429)
|
||||||
- Start calling run service for job completion (#2412, #2423)
|
- Make requests to `Run` service to renew job request (#2461)
|
||||||
|
- Add job/step log upload to Result service (#2447, #2439)
|
||||||
|
|
||||||
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
|
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
|
||||||
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.
|
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.302.1
|
<Update to ./src/runnerversion when creating release>
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ if (exitServiceAfterNFailures <= 0) {
|
|||||||
exitServiceAfterNFailures = NaN;
|
exitServiceAfterNFailures = NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
var consecutiveFailureCount = 0;
|
var unknownFailureRetryCount = 0;
|
||||||
|
var retriableFailureRetryCount = 0;
|
||||||
|
|
||||||
var gracefulShutdown = function () {
|
var gracefulShutdown = function () {
|
||||||
console.log("Shutting down runner listener");
|
console.log("Shutting down runner listener");
|
||||||
@@ -62,7 +63,8 @@ var runService = function () {
|
|||||||
|
|
||||||
listener.stdout.on("data", (data) => {
|
listener.stdout.on("data", (data) => {
|
||||||
if (data.toString("utf8").includes("Listening for Jobs")) {
|
if (data.toString("utf8").includes("Listening for Jobs")) {
|
||||||
consecutiveFailureCount = 0;
|
unknownFailureRetryCount = 0;
|
||||||
|
retriableFailureRetryCount = 0;
|
||||||
}
|
}
|
||||||
process.stdout.write(data.toString("utf8"));
|
process.stdout.write(data.toString("utf8"));
|
||||||
});
|
});
|
||||||
@@ -92,24 +94,38 @@ var runService = function () {
|
|||||||
console.log(
|
console.log(
|
||||||
"Runner listener exit with retryable error, re-launch runner in 5 seconds."
|
"Runner listener exit with retryable error, re-launch runner in 5 seconds."
|
||||||
);
|
);
|
||||||
consecutiveFailureCount = 0;
|
unknownFailureRetryCount = 0;
|
||||||
|
retriableFailureRetryCount++;
|
||||||
|
if (retriableFailureRetryCount >= 10) {
|
||||||
|
console.error(
|
||||||
|
"Stopping the runner after 10 consecutive re-tryable failures"
|
||||||
|
);
|
||||||
|
stopping = true;
|
||||||
|
}
|
||||||
} else if (code === 3 || code === 4) {
|
} else if (code === 3 || code === 4) {
|
||||||
console.log(
|
console.log(
|
||||||
"Runner listener exit because of updating, re-launch runner in 5 seconds."
|
"Runner listener exit because of updating, re-launch runner in 5 seconds."
|
||||||
);
|
);
|
||||||
consecutiveFailureCount = 0;
|
unknownFailureRetryCount = 0;
|
||||||
|
retriableFailureRetryCount++;
|
||||||
|
if (retriableFailureRetryCount >= 10) {
|
||||||
|
console.error(
|
||||||
|
"Stopping the runner after 10 consecutive re-tryable failures"
|
||||||
|
);
|
||||||
|
stopping = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var messagePrefix = "Runner listener exit with undefined return code";
|
var messagePrefix = "Runner listener exit with undefined return code";
|
||||||
consecutiveFailureCount++;
|
unknownFailureRetryCount++;
|
||||||
|
retriableFailureRetryCount = 0;
|
||||||
if (
|
if (
|
||||||
!isNaN(exitServiceAfterNFailures) &&
|
!isNaN(exitServiceAfterNFailures) &&
|
||||||
consecutiveFailureCount >= exitServiceAfterNFailures
|
unknownFailureRetryCount >= exitServiceAfterNFailures
|
||||||
) {
|
) {
|
||||||
console.error(
|
console.error(
|
||||||
`${messagePrefix}, exiting service after ${consecutiveFailureCount} consecutive failures`
|
`${messagePrefix}, exiting service after ${unknownFailureRetryCount} consecutive failures`
|
||||||
);
|
);
|
||||||
gracefulShutdown();
|
stopping = true
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`${messagePrefix}, re-launch runner in 5 seconds.`);
|
console.log(`${messagePrefix}, re-launch runner in 5 seconds.`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,33 @@ runWithManualTrap() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateCerts() {
|
||||||
|
local sudo_prefix=""
|
||||||
|
local user_id=`id -u`
|
||||||
|
|
||||||
|
if [ $user_id -ne 0 ]; then
|
||||||
|
if [[ ! -x "$(command -v sudo)" ]]; then
|
||||||
|
echo "Warning: failed to update certificate store: sudo is required but not found"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
sudo_prefix="sudo"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -x "$(command -v update-ca-certificates)" ]]; then
|
||||||
|
eval $sudo_prefix "update-ca-certificates"
|
||||||
|
elif [[ -x "$(command -v update-ca-trust)" ]]; then
|
||||||
|
eval $sudo_prefix "update-ca-trust"
|
||||||
|
else
|
||||||
|
echo "Warning: failed to update certificate store: update-ca-certificates or update-ca-trust not found. This can happen if you're using a different runner base image."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ! -z "$RUNNER_UPDATE_CA_CERTS" ]]; then
|
||||||
|
updateCerts
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z "$RUNNER_MANUALLY_TRAP_SIG" ]]; then
|
if [[ -z "$RUNNER_MANUALLY_TRAP_SIG" ]]; then
|
||||||
run $*
|
run $*
|
||||||
else
|
else
|
||||||
|
|||||||
56
src/Runner.Common/BrokerServer.cs
Normal file
56
src/Runner.Common/BrokerServer.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.Actions.RunService.WebApi;
|
||||||
|
using GitHub.DistributedTask.Pipelines;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Services.Common;
|
||||||
|
using Sdk.RSWebApi.Contracts;
|
||||||
|
using Sdk.WebApi.WebApi.RawClient;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Common
|
||||||
|
{
|
||||||
|
[ServiceLocator(Default = typeof(BrokerServer))]
|
||||||
|
public interface IBrokerServer : IRunnerService
|
||||||
|
{
|
||||||
|
Task ConnectAsync(Uri serverUrl, VssCredentials credentials);
|
||||||
|
|
||||||
|
Task<TaskAgentMessage> GetRunnerMessageAsync(CancellationToken token, TaskAgentStatus status, string version);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class BrokerServer : RunnerService, IBrokerServer
|
||||||
|
{
|
||||||
|
private bool _hasConnection;
|
||||||
|
private Uri _brokerUri;
|
||||||
|
private RawConnection _connection;
|
||||||
|
private BrokerHttpClient _brokerHttpClient;
|
||||||
|
|
||||||
|
public async Task ConnectAsync(Uri serverUri, VssCredentials credentials)
|
||||||
|
{
|
||||||
|
_brokerUri = serverUri;
|
||||||
|
|
||||||
|
_connection = VssUtil.CreateRawConnection(serverUri, credentials);
|
||||||
|
_brokerHttpClient = await _connection.GetClientAsync<BrokerHttpClient>();
|
||||||
|
_hasConnection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckConnection()
|
||||||
|
{
|
||||||
|
if (!_hasConnection)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"SetConnection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<TaskAgentMessage> GetRunnerMessageAsync(CancellationToken cancellationToken, TaskAgentStatus status, string version)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
var jobMessage = RetryRequest<TaskAgentMessage>(
|
||||||
|
async () => await _brokerHttpClient.GetRunnerMessageAsync(version, status, cancellationToken), cancellationToken);
|
||||||
|
|
||||||
|
return jobMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,12 @@ namespace GitHub.Runner.Common
|
|||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public string MonitorSocketAddress { get; set; }
|
public string MonitorSocketAddress { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public bool UseV2Flow { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public string ServerUrlV2 { get; set; }
|
||||||
|
|
||||||
[IgnoreDataMember]
|
[IgnoreDataMember]
|
||||||
public bool IsHostedServer
|
public bool IsHostedServer
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@@ -53,7 +53,7 @@ namespace GitHub.Runner.Common
|
|||||||
private static int[] _vssHttpCredentialEventIds = new int[] { 11, 13, 14, 15, 16, 17, 18, 20, 21, 22, 27, 29 };
|
private static int[] _vssHttpCredentialEventIds = new int[] { 11, 13, 14, 15, 16, 17, 18, 20, 21, 22, 27, 29 };
|
||||||
private readonly ConcurrentDictionary<Type, object> _serviceInstances = new();
|
private readonly ConcurrentDictionary<Type, object> _serviceInstances = new();
|
||||||
private readonly ConcurrentDictionary<Type, Type> _serviceTypes = new();
|
private readonly ConcurrentDictionary<Type, Type> _serviceTypes = new();
|
||||||
private readonly ISecretMasker _secretMasker = new SecretMasker();
|
private readonly ISecretMasker _secretMasker;
|
||||||
private readonly List<ProductInfoHeaderValue> _userAgents = new() { new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version) };
|
private readonly List<ProductInfoHeaderValue> _userAgents = new() { new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version) };
|
||||||
private CancellationTokenSource _runnerShutdownTokenSource = new();
|
private CancellationTokenSource _runnerShutdownTokenSource = new();
|
||||||
private object _perfLock = new();
|
private object _perfLock = new();
|
||||||
@@ -82,17 +82,20 @@ namespace GitHub.Runner.Common
|
|||||||
_loadContext = AssemblyLoadContext.GetLoadContext(typeof(HostContext).GetTypeInfo().Assembly);
|
_loadContext = AssemblyLoadContext.GetLoadContext(typeof(HostContext).GetTypeInfo().Assembly);
|
||||||
_loadContext.Unloading += LoadContext_Unloading;
|
_loadContext.Unloading += LoadContext_Unloading;
|
||||||
|
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscape);
|
var masks = new List<ValueEncoder>()
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift1);
|
{
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift2);
|
ValueEncoders.EnumerateBase64Variations,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.CommandLineArgumentEscape);
|
ValueEncoders.CommandLineArgumentEscape,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.ExpressionStringEscape);
|
ValueEncoders.ExpressionStringEscape,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);
|
ValueEncoders.JsonStringEscape,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.UriDataEscape);
|
ValueEncoders.UriDataEscape,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.XmlDataEscape);
|
ValueEncoders.XmlDataEscape,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.TrimDoubleQuotes);
|
ValueEncoders.TrimDoubleQuotes,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.PowerShellPreAmpersandEscape);
|
ValueEncoders.PowerShellPreAmpersandEscape,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.PowerShellPostAmpersandEscape);
|
ValueEncoders.PowerShellPostAmpersandEscape
|
||||||
|
};
|
||||||
|
_secretMasker = new SecretMasker(masks);
|
||||||
|
|
||||||
|
|
||||||
// Create StdoutTraceListener if ENV is set
|
// Create StdoutTraceListener if ENV is set
|
||||||
StdoutTraceListener stdoutTraceListener = null;
|
StdoutTraceListener stdoutTraceListener = null;
|
||||||
@@ -220,7 +223,7 @@ namespace GitHub.Runner.Common
|
|||||||
var runnerFile = GetConfigFile(WellKnownConfigFile.Runner);
|
var runnerFile = GetConfigFile(WellKnownConfigFile.Runner);
|
||||||
if (File.Exists(runnerFile))
|
if (File.Exists(runnerFile))
|
||||||
{
|
{
|
||||||
var runnerSettings = IOUtil.LoadObject<RunnerSettings>(runnerFile);
|
var runnerSettings = IOUtil.LoadObject<RunnerSettings>(runnerFile, true);
|
||||||
_userAgents.Add(new ProductInfoHeaderValue("RunnerId", runnerSettings.AgentId.ToString(CultureInfo.InvariantCulture)));
|
_userAgents.Add(new ProductInfoHeaderValue("RunnerId", runnerSettings.AgentId.ToString(CultureInfo.InvariantCulture)));
|
||||||
_userAgents.Add(new ProductInfoHeaderValue("GroupId", runnerSettings.PoolId.ToString(CultureInfo.InvariantCulture)));
|
_userAgents.Add(new ProductInfoHeaderValue("GroupId", runnerSettings.PoolId.ToString(CultureInfo.InvariantCulture)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,11 @@ namespace GitHub.Runner.Common
|
|||||||
Task ConnectAsync(VssConnection jobConnection);
|
Task ConnectAsync(VssConnection jobConnection);
|
||||||
|
|
||||||
void InitializeWebsocketClient(ServiceEndpoint serviceEndpoint);
|
void InitializeWebsocketClient(ServiceEndpoint serviceEndpoint);
|
||||||
void InitializeResultsClient(Uri uri, string token);
|
|
||||||
|
|
||||||
// logging and console
|
// logging and console
|
||||||
Task<TaskLog> AppendLogContentAsync(Guid scopeIdentifier, string hubName, Guid planId, int logId, Stream uploadStream, CancellationToken cancellationToken);
|
Task<TaskLog> AppendLogContentAsync(Guid scopeIdentifier, string hubName, Guid planId, int logId, Stream uploadStream, CancellationToken cancellationToken);
|
||||||
Task AppendTimelineRecordFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, long? startLine, CancellationToken cancellationToken);
|
Task AppendTimelineRecordFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, long? startLine, CancellationToken cancellationToken);
|
||||||
Task<TaskAttachment> CreateAttachmentAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, String type, String name, Stream uploadStream, CancellationToken cancellationToken);
|
Task<TaskAttachment> CreateAttachmentAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, String type, String name, Stream uploadStream, CancellationToken cancellationToken);
|
||||||
Task CreateStepSymmaryAsync(string planId, string jobId, string stepId, string file, CancellationToken cancellationToken);
|
|
||||||
Task<TaskLog> CreateLogAsync(Guid scopeIdentifier, string hubName, Guid planId, TaskLog log, CancellationToken cancellationToken);
|
Task<TaskLog> CreateLogAsync(Guid scopeIdentifier, string hubName, Guid planId, TaskLog log, CancellationToken cancellationToken);
|
||||||
Task<Timeline> CreateTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken);
|
Task<Timeline> CreateTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken);
|
||||||
Task<List<TimelineRecord>> UpdateTimelineRecordsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, IEnumerable<TimelineRecord> records, CancellationToken cancellationToken);
|
Task<List<TimelineRecord>> UpdateTimelineRecordsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, IEnumerable<TimelineRecord> records, CancellationToken cancellationToken);
|
||||||
@@ -44,7 +42,6 @@ namespace GitHub.Runner.Common
|
|||||||
private bool _hasConnection;
|
private bool _hasConnection;
|
||||||
private VssConnection _connection;
|
private VssConnection _connection;
|
||||||
private TaskHttpClient _taskClient;
|
private TaskHttpClient _taskClient;
|
||||||
private ResultsHttpClient _resultsClient;
|
|
||||||
private ClientWebSocket _websocketClient;
|
private ClientWebSocket _websocketClient;
|
||||||
|
|
||||||
private ServiceEndpoint _serviceEndpoint;
|
private ServiceEndpoint _serviceEndpoint;
|
||||||
@@ -148,12 +145,6 @@ namespace GitHub.Runner.Common
|
|||||||
InitializeWebsocketClient(TimeSpan.Zero);
|
InitializeWebsocketClient(TimeSpan.Zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InitializeResultsClient(Uri uri, string token)
|
|
||||||
{
|
|
||||||
var httpMessageHandler = HostContext.CreateHttpClientHandler();
|
|
||||||
this._resultsClient = new ResultsHttpClient(uri, httpMessageHandler, token, disposeHandler: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
CloseWebSocket(WebSocketCloseStatus.NormalClosure, CancellationToken.None);
|
CloseWebSocket(WebSocketCloseStatus.NormalClosure, CancellationToken.None);
|
||||||
@@ -316,15 +307,6 @@ namespace GitHub.Runner.Common
|
|||||||
return _taskClient.CreateAttachmentAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, type, name, uploadStream, cancellationToken: cancellationToken);
|
return _taskClient.CreateAttachmentAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, type, name, uploadStream, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task CreateStepSymmaryAsync(string planId, string jobId, string stepId, string file, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (_resultsClient != null)
|
|
||||||
{
|
|
||||||
return _resultsClient.UploadStepSummaryAsync(planId, jobId, stepId, file, cancellationToken: cancellationToken);
|
|
||||||
}
|
|
||||||
throw new InvalidOperationException("Results client is not initialized.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Task<TaskLog> CreateLogAsync(Guid scopeIdentifier, string hubName, Guid planId, TaskLog log, CancellationToken cancellationToken)
|
public Task<TaskLog> CreateLogAsync(Guid scopeIdentifier, string hubName, Guid planId, TaskLog log, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace GitHub.Runner.Common
|
|||||||
void Start(Pipelines.AgentJobRequestMessage jobRequest);
|
void Start(Pipelines.AgentJobRequestMessage jobRequest);
|
||||||
void QueueWebConsoleLine(Guid stepRecordId, string line, long? lineNumber = null);
|
void QueueWebConsoleLine(Guid stepRecordId, string line, long? lineNumber = null);
|
||||||
void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource);
|
void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource);
|
||||||
void QueueSummaryUpload(Guid stepRecordId, string name, string path, bool deleteSource);
|
void QueueResultsUpload(Guid timelineRecordId, string name, string path, string type, bool deleteSource, bool finalize, bool firstBlock, long totalLines);
|
||||||
void QueueTimelineRecordUpdate(Guid timelineId, TimelineRecord timelineRecord);
|
void QueueTimelineRecordUpdate(Guid timelineId, TimelineRecord timelineRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ namespace GitHub.Runner.Common
|
|||||||
private static readonly TimeSpan _delayForWebConsoleLineDequeue = TimeSpan.FromMilliseconds(500);
|
private static readonly TimeSpan _delayForWebConsoleLineDequeue = TimeSpan.FromMilliseconds(500);
|
||||||
private static readonly TimeSpan _delayForTimelineUpdateDequeue = TimeSpan.FromMilliseconds(500);
|
private static readonly TimeSpan _delayForTimelineUpdateDequeue = TimeSpan.FromMilliseconds(500);
|
||||||
private static readonly TimeSpan _delayForFileUploadDequeue = TimeSpan.FromMilliseconds(1000);
|
private static readonly TimeSpan _delayForFileUploadDequeue = TimeSpan.FromMilliseconds(1000);
|
||||||
private static readonly TimeSpan _delayForSummaryUploadDequeue = TimeSpan.FromMilliseconds(1000);
|
private static readonly TimeSpan _delayForResultsUploadDequeue = TimeSpan.FromMilliseconds(1000);
|
||||||
|
|
||||||
// Job message information
|
// Job message information
|
||||||
private Guid _scopeIdentifier;
|
private Guid _scopeIdentifier;
|
||||||
@@ -46,7 +46,7 @@ namespace GitHub.Runner.Common
|
|||||||
// queue for file upload (log file or attachment)
|
// queue for file upload (log file or attachment)
|
||||||
private readonly ConcurrentQueue<UploadFileInfo> _fileUploadQueue = new();
|
private readonly ConcurrentQueue<UploadFileInfo> _fileUploadQueue = new();
|
||||||
|
|
||||||
private readonly ConcurrentQueue<SummaryUploadFileInfo> _summaryFileUploadQueue = new();
|
private readonly ConcurrentQueue<ResultsUploadFileInfo> _resultsFileUploadQueue = new();
|
||||||
|
|
||||||
// queue for timeline or timeline record update (one queue per timeline)
|
// queue for timeline or timeline record update (one queue per timeline)
|
||||||
private readonly ConcurrentDictionary<Guid, ConcurrentQueue<TimelineRecord>> _timelineUpdateQueue = new();
|
private readonly ConcurrentDictionary<Guid, ConcurrentQueue<TimelineRecord>> _timelineUpdateQueue = new();
|
||||||
@@ -60,11 +60,12 @@ namespace GitHub.Runner.Common
|
|||||||
// Task for each queue's dequeue process
|
// Task for each queue's dequeue process
|
||||||
private Task _webConsoleLineDequeueTask;
|
private Task _webConsoleLineDequeueTask;
|
||||||
private Task _fileUploadDequeueTask;
|
private Task _fileUploadDequeueTask;
|
||||||
private Task _summaryUploadDequeueTask;
|
private Task _resultsUploadDequeueTask;
|
||||||
private Task _timelineUpdateDequeueTask;
|
private Task _timelineUpdateDequeueTask;
|
||||||
|
|
||||||
// common
|
// common
|
||||||
private IJobServer _jobServer;
|
private IJobServer _jobServer;
|
||||||
|
private IResultsServer _resultsServer;
|
||||||
private Task[] _allDequeueTasks;
|
private Task[] _allDequeueTasks;
|
||||||
private readonly TaskCompletionSource<int> _jobCompletionSource = new();
|
private readonly TaskCompletionSource<int> _jobCompletionSource = new();
|
||||||
private readonly TaskCompletionSource<int> _jobRecordUpdated = new();
|
private readonly TaskCompletionSource<int> _jobRecordUpdated = new();
|
||||||
@@ -84,10 +85,14 @@ namespace GitHub.Runner.Common
|
|||||||
private bool _webConsoleLineAggressiveDequeue = true;
|
private bool _webConsoleLineAggressiveDequeue = true;
|
||||||
private bool _firstConsoleOutputs = true;
|
private bool _firstConsoleOutputs = true;
|
||||||
|
|
||||||
|
private bool _resultsClientInitiated = false;
|
||||||
|
private delegate Task ResultsFileUploadHandler(ResultsUploadFileInfo file);
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
public override void Initialize(IHostContext hostContext)
|
||||||
{
|
{
|
||||||
base.Initialize(hostContext);
|
base.Initialize(hostContext);
|
||||||
_jobServer = hostContext.GetService<IJobServer>();
|
_jobServer = hostContext.GetService<IJobServer>();
|
||||||
|
_resultsServer = hostContext.GetService<IResultsServer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start(Pipelines.AgentJobRequestMessage jobRequest)
|
public void Start(Pipelines.AgentJobRequestMessage jobRequest)
|
||||||
@@ -108,10 +113,10 @@ namespace GitHub.Runner.Common
|
|||||||
!string.IsNullOrEmpty(resultsReceiverEndpoint))
|
!string.IsNullOrEmpty(resultsReceiverEndpoint))
|
||||||
{
|
{
|
||||||
Trace.Info("Initializing results client");
|
Trace.Info("Initializing results client");
|
||||||
_jobServer.InitializeResultsClient(new Uri(resultsReceiverEndpoint), accessToken);
|
_resultsServer.InitializeResultsClient(new Uri(resultsReceiverEndpoint), accessToken);
|
||||||
|
_resultsClientInitiated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (_queueInProcess)
|
if (_queueInProcess)
|
||||||
{
|
{
|
||||||
Trace.Info("No-opt, all queue process tasks are running.");
|
Trace.Info("No-opt, all queue process tasks are running.");
|
||||||
@@ -140,12 +145,12 @@ namespace GitHub.Runner.Common
|
|||||||
_fileUploadDequeueTask = ProcessFilesUploadQueueAsync();
|
_fileUploadDequeueTask = ProcessFilesUploadQueueAsync();
|
||||||
|
|
||||||
Trace.Info("Start results file upload queue.");
|
Trace.Info("Start results file upload queue.");
|
||||||
_summaryUploadDequeueTask = ProcessSummaryUploadQueueAsync();
|
_resultsUploadDequeueTask = ProcessResultsUploadQueueAsync();
|
||||||
|
|
||||||
Trace.Info("Start process timeline update queue.");
|
Trace.Info("Start process timeline update queue.");
|
||||||
_timelineUpdateDequeueTask = ProcessTimelinesUpdateQueueAsync();
|
_timelineUpdateDequeueTask = ProcessTimelinesUpdateQueueAsync();
|
||||||
|
|
||||||
_allDequeueTasks = new Task[] { _webConsoleLineDequeueTask, _fileUploadDequeueTask, _timelineUpdateDequeueTask, _summaryUploadDequeueTask };
|
_allDequeueTasks = new Task[] { _webConsoleLineDequeueTask, _fileUploadDequeueTask, _timelineUpdateDequeueTask, _resultsUploadDequeueTask };
|
||||||
_queueInProcess = true;
|
_queueInProcess = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,9 +181,9 @@ namespace GitHub.Runner.Common
|
|||||||
await ProcessFilesUploadQueueAsync(runOnce: true);
|
await ProcessFilesUploadQueueAsync(runOnce: true);
|
||||||
Trace.Info("File upload queue drained.");
|
Trace.Info("File upload queue drained.");
|
||||||
|
|
||||||
Trace.Verbose("Draining results summary upload queue.");
|
Trace.Verbose("Draining results upload queue.");
|
||||||
await ProcessSummaryUploadQueueAsync(runOnce: true);
|
await ProcessResultsUploadQueueAsync(runOnce: true);
|
||||||
Trace.Info("Results summary upload queue drained.");
|
Trace.Info("Results upload queue drained.");
|
||||||
|
|
||||||
// ProcessTimelinesUpdateQueueAsync() will throw exception during shutdown
|
// ProcessTimelinesUpdateQueueAsync() will throw exception during shutdown
|
||||||
// if there is any timeline records that failed to update contains output variabls.
|
// if there is any timeline records that failed to update contains output variabls.
|
||||||
@@ -230,21 +235,43 @@ namespace GitHub.Runner.Common
|
|||||||
_fileUploadQueue.Enqueue(newFile);
|
_fileUploadQueue.Enqueue(newFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void QueueSummaryUpload(Guid stepRecordId, string name, string path, bool deleteSource)
|
public void QueueResultsUpload(Guid timelineRecordId, string name, string path, string type, bool deleteSource, bool finalize, bool firstBlock, long totalLines)
|
||||||
{
|
{
|
||||||
|
if (!_resultsClientInitiated)
|
||||||
|
{
|
||||||
|
Trace.Verbose("Skipping results upload");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (deleteSource)
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Info("Catch exception during delete skipped results upload file.");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// all parameter not null, file path exist.
|
// all parameter not null, file path exist.
|
||||||
var newFile = new SummaryUploadFileInfo()
|
var newFile = new ResultsUploadFileInfo()
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
Path = path,
|
Path = path,
|
||||||
|
Type = type,
|
||||||
PlanId = _planId.ToString(),
|
PlanId = _planId.ToString(),
|
||||||
JobId = _jobTimelineRecordId.ToString(),
|
JobId = _jobTimelineRecordId.ToString(),
|
||||||
StepId = stepRecordId.ToString(),
|
RecordId = timelineRecordId,
|
||||||
DeleteSource = deleteSource
|
DeleteSource = deleteSource,
|
||||||
|
Finalize = finalize,
|
||||||
|
FirstBlock = firstBlock,
|
||||||
|
TotalLines = totalLines,
|
||||||
};
|
};
|
||||||
|
|
||||||
Trace.Verbose("Enqueue results file upload queue: file '{0}' attach to job {1} step {2}", newFile.Path, _jobTimelineRecordId, stepRecordId);
|
Trace.Verbose("Enqueue results file upload queue: file '{0}' attach to job {1} step {2}", newFile.Path, _jobTimelineRecordId, timelineRecordId);
|
||||||
_summaryFileUploadQueue.Enqueue(newFile);
|
_resultsFileUploadQueue.Enqueue(newFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void QueueTimelineRecordUpdate(Guid timelineId, TimelineRecord timelineRecord)
|
public void QueueTimelineRecordUpdate(Guid timelineId, TimelineRecord timelineRecord)
|
||||||
@@ -437,18 +464,18 @@ namespace GitHub.Runner.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessSummaryUploadQueueAsync(bool runOnce = false)
|
private async Task ProcessResultsUploadQueueAsync(bool runOnce = false)
|
||||||
{
|
{
|
||||||
Trace.Info("Starting results-based upload queue...");
|
Trace.Info("Starting results-based upload queue...");
|
||||||
|
|
||||||
while (!_jobCompletionSource.Task.IsCompleted || runOnce)
|
while (!_jobCompletionSource.Task.IsCompleted || runOnce)
|
||||||
{
|
{
|
||||||
List<SummaryUploadFileInfo> filesToUpload = new();
|
List<ResultsUploadFileInfo> filesToUpload = new();
|
||||||
SummaryUploadFileInfo dequeueFile;
|
ResultsUploadFileInfo dequeueFile;
|
||||||
while (_summaryFileUploadQueue.TryDequeue(out dequeueFile))
|
while (_resultsFileUploadQueue.TryDequeue(out dequeueFile))
|
||||||
{
|
{
|
||||||
filesToUpload.Add(dequeueFile);
|
filesToUpload.Add(dequeueFile);
|
||||||
// process at most 10 file upload.
|
// process at most 10 file uploads.
|
||||||
if (!runOnce && filesToUpload.Count > 10)
|
if (!runOnce && filesToUpload.Count > 10)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -459,7 +486,7 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
if (runOnce)
|
if (runOnce)
|
||||||
{
|
{
|
||||||
Trace.Info($"Uploading {filesToUpload.Count} summary files in one shot through results service.");
|
Trace.Info($"Uploading {filesToUpload.Count} file(s) in one shot through results service.");
|
||||||
}
|
}
|
||||||
|
|
||||||
int errorCount = 0;
|
int errorCount = 0;
|
||||||
@@ -467,30 +494,38 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await UploadSummaryFile(file);
|
if (String.Equals(file.Type, ChecksAttachmentType.StepSummary, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await UploadSummaryFile(file);
|
||||||
|
}
|
||||||
|
else if (String.Equals(file.Type, CoreAttachmentType.ResultsLog, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (file.RecordId != _jobTimelineRecordId)
|
||||||
|
{
|
||||||
|
Trace.Info($"Got a step log file to send to results service.");
|
||||||
|
await UploadResultsStepLogFile(file);
|
||||||
|
}
|
||||||
|
else if (file.RecordId == _jobTimelineRecordId)
|
||||||
|
{
|
||||||
|
Trace.Info($"Got a job log file to send to results service.");
|
||||||
|
await UploadResultsJobLogFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var issue = new Issue() { Type = IssueType.Warning, Message = $"Caught exception during summary file upload to results. {ex.Message}" };
|
Trace.Info("Catch exception during file upload to results, keep going since the process is best effort.");
|
||||||
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.ResultsUploadFailure;
|
|
||||||
|
|
||||||
var telemetryRecord = new TimelineRecord()
|
|
||||||
{
|
|
||||||
Id = Constants.Runner.TelemetryRecordId,
|
|
||||||
};
|
|
||||||
telemetryRecord.Issues.Add(issue);
|
|
||||||
QueueTimelineRecordUpdate(_jobTimelineId, telemetryRecord);
|
|
||||||
|
|
||||||
Trace.Info("Catch exception during summary file upload to results, keep going since the process is best effort.");
|
|
||||||
Trace.Error(ex);
|
Trace.Error(ex);
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
errorCount++;
|
errorCount++;
|
||||||
|
|
||||||
|
// If we hit any exceptions uploading to Results, let's skip any additional uploads to Results
|
||||||
|
_resultsClientInitiated = false;
|
||||||
|
|
||||||
|
SendResultsTelemetry(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Trace.Info("Tried to upload {0} summary files to results, success rate: {1}/{0}.", filesToUpload.Count, filesToUpload.Count - errorCount);
|
Trace.Info("Tried to upload {0} file(s) to results, success rate: {1}/{0}.", filesToUpload.Count, filesToUpload.Count - errorCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runOnce)
|
if (runOnce)
|
||||||
@@ -499,11 +534,24 @@ namespace GitHub.Runner.Common
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await Task.Delay(_delayForSummaryUploadDequeue);
|
await Task.Delay(_delayForResultsUploadDequeue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SendResultsTelemetry(Exception ex)
|
||||||
|
{
|
||||||
|
var issue = new Issue() { Type = IssueType.Warning, Message = $"Caught exception with results. {ex.Message}" };
|
||||||
|
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.ResultsUploadFailure;
|
||||||
|
|
||||||
|
var telemetryRecord = new TimelineRecord()
|
||||||
|
{
|
||||||
|
Id = Constants.Runner.TelemetryRecordId,
|
||||||
|
};
|
||||||
|
telemetryRecord.Issues.Add(issue);
|
||||||
|
QueueTimelineRecordUpdate(_jobTimelineId, telemetryRecord);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ProcessTimelinesUpdateQueueAsync(bool runOnce = false)
|
private async Task ProcessTimelinesUpdateQueueAsync(bool runOnce = false)
|
||||||
{
|
{
|
||||||
while (!_jobCompletionSource.Task.IsCompleted || runOnce)
|
while (!_jobCompletionSource.Task.IsCompleted || runOnce)
|
||||||
@@ -574,6 +622,22 @@ namespace GitHub.Runner.Common
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _jobServer.UpdateTimelineRecordsAsync(_scopeIdentifier, _hubName, _planId, update.TimelineId, update.PendingRecords, default(CancellationToken));
|
await _jobServer.UpdateTimelineRecordsAsync(_scopeIdentifier, _hubName, _planId, update.TimelineId, update.PendingRecords, default(CancellationToken));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_resultsClientInitiated)
|
||||||
|
{
|
||||||
|
await _resultsServer.UpdateResultsWorkflowStepsAsync(_scopeIdentifier, _hubName, _planId, update.TimelineId, update.PendingRecords, default(CancellationToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Trace.Info("Catch exception during update steps, skip update Results.");
|
||||||
|
Trace.Error(e);
|
||||||
|
_resultsClientInitiated = false;
|
||||||
|
|
||||||
|
SendResultsTelemetry(e);
|
||||||
|
}
|
||||||
|
|
||||||
if (_bufferedRetryRecords.Remove(update.TimelineId))
|
if (_bufferedRetryRecords.Remove(update.TimelineId))
|
||||||
{
|
{
|
||||||
Trace.Verbose("Cleanup buffered timeline record for timeline: {0}.", update.TimelineId);
|
Trace.Verbose("Cleanup buffered timeline record for timeline: {0}.", update.TimelineId);
|
||||||
@@ -776,16 +840,50 @@ namespace GitHub.Runner.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UploadSummaryFile(SummaryUploadFileInfo file)
|
private async Task UploadSummaryFile(ResultsUploadFileInfo file)
|
||||||
{
|
{
|
||||||
|
Trace.Info($"Starting to upload summary file to results service {file.Name}, {file.Path}");
|
||||||
|
ResultsFileUploadHandler summaryHandler = async (file) =>
|
||||||
|
{
|
||||||
|
await _resultsServer.CreateResultsStepSummaryAsync(file.PlanId, file.JobId, file.RecordId, file.Path, CancellationToken.None);
|
||||||
|
};
|
||||||
|
|
||||||
|
await UploadResultsFile(file, summaryHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UploadResultsStepLogFile(ResultsUploadFileInfo file)
|
||||||
|
{
|
||||||
|
Trace.Info($"Starting upload of step log file to results service {file.Name}, {file.Path}");
|
||||||
|
ResultsFileUploadHandler stepLogHandler = async (file) =>
|
||||||
|
{
|
||||||
|
await _resultsServer.CreateResultsStepLogAsync(file.PlanId, file.JobId, file.RecordId, file.Path, file.Finalize, file.FirstBlock, file.TotalLines, CancellationToken.None);
|
||||||
|
};
|
||||||
|
|
||||||
|
await UploadResultsFile(file, stepLogHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UploadResultsJobLogFile(ResultsUploadFileInfo file)
|
||||||
|
{
|
||||||
|
Trace.Info($"Starting upload of job log file to results service {file.Name}, {file.Path}");
|
||||||
|
ResultsFileUploadHandler jobLogHandler = async (file) =>
|
||||||
|
{
|
||||||
|
await _resultsServer.CreateResultsJobLogAsync(file.PlanId, file.JobId, file.Path, file.Finalize, file.FirstBlock, file.TotalLines, CancellationToken.None);
|
||||||
|
};
|
||||||
|
|
||||||
|
await UploadResultsFile(file, jobLogHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UploadResultsFile(ResultsUploadFileInfo file, ResultsFileUploadHandler uploadHandler)
|
||||||
|
{
|
||||||
|
if (!_resultsClientInitiated)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bool uploadSucceed = false;
|
bool uploadSucceed = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Upload the step summary
|
await uploadHandler(file);
|
||||||
Trace.Info($"Starting to upload summary file to results service {file.Name}, {file.Path}");
|
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
await _jobServer.CreateStepSymmaryAsync(file.PlanId, file.JobId, file.StepId, file.Path, cancellationTokenSource.Token);
|
|
||||||
|
|
||||||
uploadSucceed = true;
|
uploadSucceed = true;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -798,7 +896,7 @@ namespace GitHub.Runner.Common
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Trace.Info("Catch exception during delete success results uploaded summary file.");
|
Trace.Info("Exception encountered during deletion of a temporary file that was already successfully uploaded to results.");
|
||||||
Trace.Error(ex);
|
Trace.Error(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -822,18 +920,20 @@ namespace GitHub.Runner.Common
|
|||||||
public bool DeleteSource { get; set; }
|
public bool DeleteSource { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class SummaryUploadFileInfo
|
internal class ResultsUploadFileInfo
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
public string PlanId { get; set; }
|
public string PlanId { get; set; }
|
||||||
public string JobId { get; set; }
|
public string JobId { get; set; }
|
||||||
public string StepId { get; set; }
|
public Guid RecordId { get; set; }
|
||||||
public bool DeleteSource { get; set; }
|
public bool DeleteSource { get; set; }
|
||||||
|
public bool Finalize { get; set; }
|
||||||
|
public bool FirstBlock { get; set; }
|
||||||
|
public long TotalLines { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
internal class ConsoleLineInfo
|
internal class ConsoleLineInfo
|
||||||
{
|
{
|
||||||
public ConsoleLineInfo(Guid recordId, string line, long? lineNumber)
|
public ConsoleLineInfo(Guid recordId, string line, long? lineNumber)
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ namespace GitHub.Runner.Common
|
|||||||
// 8 MB
|
// 8 MB
|
||||||
public const int PageSize = 8 * 1024 * 1024;
|
public const int PageSize = 8 * 1024 * 1024;
|
||||||
|
|
||||||
|
// For Results
|
||||||
|
public static string BlocksFolder = "blocks";
|
||||||
|
|
||||||
|
// 2 MB
|
||||||
|
public const int BlockSize = 2 * 1024 * 1024;
|
||||||
|
|
||||||
private Guid _timelineId;
|
private Guid _timelineId;
|
||||||
private Guid _timelineRecordId;
|
private Guid _timelineRecordId;
|
||||||
private FileStream _pageData;
|
private FileStream _pageData;
|
||||||
@@ -32,6 +38,13 @@ namespace GitHub.Runner.Common
|
|||||||
private string _pagesFolder;
|
private string _pagesFolder;
|
||||||
private IJobServerQueue _jobServerQueue;
|
private IJobServerQueue _jobServerQueue;
|
||||||
|
|
||||||
|
private string _resultsDataFileName;
|
||||||
|
private FileStream _resultsBlockData;
|
||||||
|
private StreamWriter _resultsBlockWriter;
|
||||||
|
private string _resultsBlockFolder;
|
||||||
|
private int _blockByteCount;
|
||||||
|
private int _blockCount;
|
||||||
|
|
||||||
public long TotalLines => _totalLines;
|
public long TotalLines => _totalLines;
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
public override void Initialize(IHostContext hostContext)
|
||||||
@@ -39,8 +52,10 @@ namespace GitHub.Runner.Common
|
|||||||
base.Initialize(hostContext);
|
base.Initialize(hostContext);
|
||||||
_totalLines = 0;
|
_totalLines = 0;
|
||||||
_pagesFolder = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Diag), PagingFolder);
|
_pagesFolder = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Diag), PagingFolder);
|
||||||
_jobServerQueue = HostContext.GetService<IJobServerQueue>();
|
|
||||||
Directory.CreateDirectory(_pagesFolder);
|
Directory.CreateDirectory(_pagesFolder);
|
||||||
|
_resultsBlockFolder = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Diag), BlocksFolder);
|
||||||
|
Directory.CreateDirectory(_resultsBlockFolder);
|
||||||
|
_jobServerQueue = HostContext.GetService<IJobServerQueue>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Setup(Guid timelineId, Guid timelineRecordId)
|
public void Setup(Guid timelineId, Guid timelineRecordId)
|
||||||
@@ -60,11 +75,17 @@ namespace GitHub.Runner.Common
|
|||||||
// lazy creation on write
|
// lazy creation on write
|
||||||
if (_pageWriter == null)
|
if (_pageWriter == null)
|
||||||
{
|
{
|
||||||
Create();
|
NewPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_resultsBlockWriter == null)
|
||||||
|
{
|
||||||
|
NewBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
string line = $"{DateTime.UtcNow.ToString("O")} {message}";
|
string line = $"{DateTime.UtcNow.ToString("O")} {message}";
|
||||||
_pageWriter.WriteLine(line);
|
_pageWriter.WriteLine(line);
|
||||||
|
_resultsBlockWriter.WriteLine(line);
|
||||||
|
|
||||||
_totalLines++;
|
_totalLines++;
|
||||||
if (line.IndexOf('\n') != -1)
|
if (line.IndexOf('\n') != -1)
|
||||||
@@ -78,21 +99,25 @@ namespace GitHub.Runner.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_byteCount += System.Text.Encoding.UTF8.GetByteCount(line);
|
var bytes = System.Text.Encoding.UTF8.GetByteCount(line);
|
||||||
|
_byteCount += bytes;
|
||||||
|
_blockByteCount += bytes;
|
||||||
if (_byteCount >= PageSize)
|
if (_byteCount >= PageSize)
|
||||||
{
|
{
|
||||||
NewPage();
|
NewPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_blockByteCount >= BlockSize)
|
||||||
|
{
|
||||||
|
NewBlock();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void End()
|
public void End()
|
||||||
{
|
{
|
||||||
EndPage();
|
EndPage();
|
||||||
}
|
EndBlock(true);
|
||||||
|
|
||||||
private void Create()
|
|
||||||
{
|
|
||||||
NewPage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NewPage()
|
private void NewPage()
|
||||||
@@ -117,5 +142,27 @@ namespace GitHub.Runner.Common
|
|||||||
_jobServerQueue.QueueFileUpload(_timelineId, _timelineRecordId, "DistributedTask.Core.Log", "CustomToolLog", _dataFileName, true);
|
_jobServerQueue.QueueFileUpload(_timelineId, _timelineRecordId, "DistributedTask.Core.Log", "CustomToolLog", _dataFileName, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void NewBlock()
|
||||||
|
{
|
||||||
|
EndBlock(false);
|
||||||
|
_blockByteCount = 0;
|
||||||
|
_resultsDataFileName = Path.Combine(_resultsBlockFolder, $"{_timelineId}_{_timelineRecordId}.{++_blockCount}");
|
||||||
|
_resultsBlockData = new FileStream(_resultsDataFileName, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite);
|
||||||
|
_resultsBlockWriter = new StreamWriter(_resultsBlockData, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EndBlock(bool finalize)
|
||||||
|
{
|
||||||
|
if (_resultsBlockWriter != null)
|
||||||
|
{
|
||||||
|
_resultsBlockWriter.Flush();
|
||||||
|
_resultsBlockData.Flush();
|
||||||
|
_resultsBlockWriter.Dispose();
|
||||||
|
_resultsBlockWriter = null;
|
||||||
|
_resultsBlockData = null;
|
||||||
|
_jobServerQueue.QueueResultsUpload(_timelineRecordId, "ResultsLog", _resultsDataFileName, "Results.Core.Log", deleteSource: true, finalize, firstBlock: _resultsDataFileName.EndsWith(".1"), totalLines: _totalLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
98
src/Runner.Common/ResultsServer.cs
Normal file
98
src/Runner.Common/ResultsServer.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Services.Results.Client;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Common
|
||||||
|
{
|
||||||
|
[ServiceLocator(Default = typeof(ResultServer))]
|
||||||
|
public interface IResultsServer : IRunnerService
|
||||||
|
{
|
||||||
|
void InitializeResultsClient(Uri uri, string token);
|
||||||
|
|
||||||
|
// logging and console
|
||||||
|
Task CreateResultsStepSummaryAsync(string planId, string jobId, Guid stepId, string file,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task CreateResultsStepLogAsync(string planId, string jobId, Guid stepId, string file, bool finalize,
|
||||||
|
bool firstBlock, long lineCount, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task CreateResultsJobLogAsync(string planId, string jobId, string file, bool finalize, bool firstBlock,
|
||||||
|
long lineCount, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task UpdateResultsWorkflowStepsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId,
|
||||||
|
IEnumerable<TimelineRecord> records, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ResultServer : RunnerService, IResultsServer
|
||||||
|
{
|
||||||
|
private ResultsHttpClient _resultsClient;
|
||||||
|
|
||||||
|
public void InitializeResultsClient(Uri uri, string token)
|
||||||
|
{
|
||||||
|
var httpMessageHandler = HostContext.CreateHttpClientHandler();
|
||||||
|
this._resultsClient = new ResultsHttpClient(uri, httpMessageHandler, token, disposeHandler: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CreateResultsStepSummaryAsync(string planId, string jobId, Guid stepId, string file,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_resultsClient != null)
|
||||||
|
{
|
||||||
|
return _resultsClient.UploadStepSummaryAsync(planId, jobId, stepId, file,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Results client is not initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CreateResultsStepLogAsync(string planId, string jobId, Guid stepId, string file, bool finalize,
|
||||||
|
bool firstBlock, long lineCount, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_resultsClient != null)
|
||||||
|
{
|
||||||
|
return _resultsClient.UploadResultsStepLogAsync(planId, jobId, stepId, file, finalize, firstBlock,
|
||||||
|
lineCount, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Results client is not initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CreateResultsJobLogAsync(string planId, string jobId, string file, bool finalize, bool firstBlock,
|
||||||
|
long lineCount, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_resultsClient != null)
|
||||||
|
{
|
||||||
|
return _resultsClient.UploadResultsJobLogAsync(planId, jobId, file, finalize, firstBlock, lineCount,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Results client is not initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UpdateResultsWorkflowStepsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId,
|
||||||
|
IEnumerable<TimelineRecord> records, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_resultsClient != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var timelineRecords = records.ToList();
|
||||||
|
return _resultsClient.UpdateWorkflowStepsAsync(planId, new List<TimelineRecord>(timelineRecords),
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Log error, but continue as this call is best-effort
|
||||||
|
Trace.Info($"Failed to update steps status due to {ex.GetType().Name}");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Results client is not initialized.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -7,6 +7,7 @@ using GitHub.DistributedTask.Pipelines;
|
|||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
|
using Sdk.RSWebApi.Contracts;
|
||||||
using Sdk.WebApi.WebApi.RawClient;
|
using Sdk.WebApi.WebApi.RawClient;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common
|
namespace GitHub.Runner.Common
|
||||||
@@ -19,6 +20,8 @@ namespace GitHub.Runner.Common
|
|||||||
Task<AgentJobRequestMessage> GetJobMessageAsync(string id, CancellationToken token);
|
Task<AgentJobRequestMessage> GetJobMessageAsync(string id, CancellationToken token);
|
||||||
|
|
||||||
Task CompleteJobAsync(Guid planId, Guid jobId, TaskResult result, Dictionary<String, VariableValue> outputs, IList<StepResult> stepResults, CancellationToken token);
|
Task CompleteJobAsync(Guid planId, Guid jobId, TaskResult result, Dictionary<String, VariableValue> outputs, IList<StepResult> stepResults, CancellationToken token);
|
||||||
|
|
||||||
|
Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class RunServer : RunnerService, IRunServer
|
public sealed class RunServer : RunnerService, IRunServer
|
||||||
@@ -64,5 +67,18 @@ namespace GitHub.Runner.Common
|
|||||||
return RetryRequest(
|
return RetryRequest(
|
||||||
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, cancellationToken), cancellationToken);
|
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, cancellationToken), cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
var renewJobResponse = RetryRequest<RenewJobResponse>(
|
||||||
|
async () => await _runServiceHttpClient.RenewJobAsync(requestUri, planId, jobId, cancellationToken), cancellationToken);
|
||||||
|
if (renewJobResponse == null)
|
||||||
|
{
|
||||||
|
throw new TaskOrchestrationJobNotFoundException(jobId.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return renewJobResponse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
237
src/Runner.Common/RunnerDotcomServer.cs
Normal file
237
src/Runner.Common/RunnerDotcomServer.cs
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.Services.WebApi;
|
||||||
|
using GitHub.Services.Common;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Common
|
||||||
|
{
|
||||||
|
[ServiceLocator(Default = typeof(RunnerDotcomServer))]
|
||||||
|
public interface IRunnerDotcomServer : IRunnerService
|
||||||
|
{
|
||||||
|
Task<List<TaskAgent>> GetRunnersAsync(int runnerGroupId, string githubUrl, string githubToken, string agentName);
|
||||||
|
|
||||||
|
Task<DistributedTask.WebApi.Runner> AddRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken, string publicKey);
|
||||||
|
Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken);
|
||||||
|
|
||||||
|
string GetGitHubRequestId(HttpResponseHeaders headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RequestType
|
||||||
|
{
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Delete
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RunnerDotcomServer : RunnerService, IRunnerDotcomServer
|
||||||
|
{
|
||||||
|
private ITerminal _term;
|
||||||
|
|
||||||
|
public override void Initialize(IHostContext hostContext)
|
||||||
|
{
|
||||||
|
base.Initialize(hostContext);
|
||||||
|
_term = hostContext.GetService<ITerminal>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<List<TaskAgent>> GetRunnersAsync(int runnerGroupId, string githubUrl, string githubToken, string agentName = null)
|
||||||
|
{
|
||||||
|
var githubApiUrl = "";
|
||||||
|
var gitHubUrlBuilder = new UriBuilder(githubUrl);
|
||||||
|
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (path.Length == 1)
|
||||||
|
{
|
||||||
|
// org runner
|
||||||
|
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions/runner-groups/{runnerGroupId}/runners";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/orgs/{path[0]}/actions/runner-groups/{runnerGroupId}/runners";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (path.Length == 2)
|
||||||
|
{
|
||||||
|
// repo or enterprise runner.
|
||||||
|
if (!string.Equals(path[0], "enterprises", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{path[0]}/{path[1]}/actions/runner-groups/{runnerGroupId}/runners";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/{path[0]}/{path[1]}/actions/runner-groups/{runnerGroupId}/runners";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"'{githubUrl}' should point to an org or enterprise.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var runnersList = await RetryRequest<ListRunnersResponse>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
|
||||||
|
var agents = runnersList.ToTaskAgents();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(agentName))
|
||||||
|
{
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
return agents.Where(x => string.Equals(x.Name, agentName, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken)
|
||||||
|
{
|
||||||
|
var githubApiUrl = "";
|
||||||
|
var gitHubUrlBuilder = new UriBuilder(githubUrl);
|
||||||
|
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (path.Length == 1)
|
||||||
|
{
|
||||||
|
// org runner
|
||||||
|
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions/runner-groups";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/orgs/{path[0]}/actions/runner-groups";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (path.Length == 2)
|
||||||
|
{
|
||||||
|
// repo or enterprise runner.
|
||||||
|
if (!string.Equals(path[0], "enterprises", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{path[0]}/{path[1]}/actions/runner-groups";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/{path[0]}/{path[1]}/actions/runner-groups";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"'{githubUrl}' should point to an org or enterprise.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var agentPools = await RetryRequest<RunnerGroupList>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
|
||||||
|
|
||||||
|
return agentPools?.ToAgentPoolList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DistributedTask.WebApi.Runner> AddRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken, string publicKey)
|
||||||
|
{
|
||||||
|
var gitHubUrlBuilder = new UriBuilder(githubUrl);
|
||||||
|
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
string githubApiUrl;
|
||||||
|
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/actions/runners/register";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/actions/runners/register";
|
||||||
|
}
|
||||||
|
|
||||||
|
var bodyObject = new Dictionary<string, Object>()
|
||||||
|
{
|
||||||
|
{"url", githubUrl},
|
||||||
|
{"group_id", runnerGroupId},
|
||||||
|
{"name", agent.Name},
|
||||||
|
{"version", agent.Version},
|
||||||
|
{"updates_disabled", agent.DisableUpdate},
|
||||||
|
{"ephemeral", agent.Ephemeral},
|
||||||
|
{"labels", agent.Labels},
|
||||||
|
{"public_key", publicKey}
|
||||||
|
};
|
||||||
|
|
||||||
|
var body = new StringContent(StringUtil.ConvertToJson(bodyObject), null, "application/json");
|
||||||
|
|
||||||
|
return await RetryRequest<DistributedTask.WebApi.Runner>(githubApiUrl, githubToken, RequestType.Post, 3, "Failed to add agent", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<T> RetryRequest<T>(string githubApiUrl, string githubToken, RequestType requestType, int maxRetryAttemptsCount = 5, string errorMessage = null, StringContent body = null)
|
||||||
|
{
|
||||||
|
int retry = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
retry++;
|
||||||
|
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||||
|
using (var httpClient = new HttpClient(httpClientHandler))
|
||||||
|
{
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("RemoteAuth", githubToken);
|
||||||
|
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
||||||
|
|
||||||
|
var responseStatus = System.Net.HttpStatusCode.OK;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = null;
|
||||||
|
if (requestType == RequestType.Get)
|
||||||
|
{
|
||||||
|
response = await httpClient.GetAsync(githubApiUrl);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response = await httpClient.PostAsync(githubApiUrl, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response != null)
|
||||||
|
{
|
||||||
|
responseStatus = response.StatusCode;
|
||||||
|
var githubRequestId = GetGitHubRequestId(response.Headers);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Trace.Info($"Http response code: {response.StatusCode} from '{requestType.ToString()} {githubApiUrl}' ({githubRequestId})");
|
||||||
|
var jsonResponse = await response.Content.ReadAsStringAsync();
|
||||||
|
return StringUtil.ConvertFromJson<T>(jsonResponse);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_term.WriteError($"Http response code: {response.StatusCode} from '{requestType.ToString()} {githubApiUrl}' (Request Id: {githubRequestId})");
|
||||||
|
var errorResponse = await response.Content.ReadAsStringAsync();
|
||||||
|
_term.WriteError(errorResponse);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (retry < maxRetryAttemptsCount && responseStatus != System.Net.HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
Trace.Error($"{errorMessage} -- Atempt: {retry}");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5));
|
||||||
|
Trace.Info($"Retrying in {backOff.Seconds} seconds");
|
||||||
|
await Task.Delay(backOff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetGitHubRequestId(HttpResponseHeaders headers)
|
||||||
|
{
|
||||||
|
if (headers.TryGetValues("x-github-request-id", out var headerValues))
|
||||||
|
{
|
||||||
|
return headerValues.FirstOrDefault();
|
||||||
|
}
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/Runner.Common/Util/MessageUtil.cs
Normal file
14
src/Runner.Common/Util/MessageUtil.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace GitHub.Runner.Common.Util
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
|
||||||
|
public static class MessageUtil
|
||||||
|
{
|
||||||
|
public static bool IsRunServiceJob(string messageType)
|
||||||
|
{
|
||||||
|
return string.Equals(messageType, JobRequestMessageTypes.RunnerJobRequest, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
209
src/Runner.Listener/BrokerMessageListener.cs
Normal file
209
src/Runner.Listener/BrokerMessageListener.cs
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Listener.Configuration;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Services.Common;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
|
using GitHub.Services.OAuth;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Listener
|
||||||
|
{
|
||||||
|
public sealed class BrokerMessageListener : RunnerService, IMessageListener
|
||||||
|
{
|
||||||
|
private RunnerSettings _settings;
|
||||||
|
private ITerminal _term;
|
||||||
|
private TimeSpan _getNextMessageRetryInterval;
|
||||||
|
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
|
||||||
|
private CancellationTokenSource _getMessagesTokenSource;
|
||||||
|
private IBrokerServer _brokerServer;
|
||||||
|
|
||||||
|
public override void Initialize(IHostContext hostContext)
|
||||||
|
{
|
||||||
|
base.Initialize(hostContext);
|
||||||
|
|
||||||
|
_term = HostContext.GetService<ITerminal>();
|
||||||
|
_brokerServer = HostContext.GetService<IBrokerServer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Boolean> CreateSessionAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
await RefreshBrokerConnection();
|
||||||
|
return await Task.FromResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteSessionAsync()
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnJobStatus(object sender, JobStatusEventArgs e)
|
||||||
|
{
|
||||||
|
Trace.Info("Received job status event. JobState: {0}", e.Status);
|
||||||
|
runnerStatus = e.Status;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_getMessagesTokenSource?.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
Trace.Info("_getMessagesTokenSource is already disposed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TaskAgentMessage> GetNextMessageAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
bool encounteringError = false;
|
||||||
|
int continuousError = 0;
|
||||||
|
Stopwatch heartbeat = new();
|
||||||
|
heartbeat.Restart();
|
||||||
|
var maxRetryCount = 10;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
TaskAgentMessage message = null;
|
||||||
|
_getMessagesTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
message = await _brokerServer.GetRunnerMessageAsync(_getMessagesTokenSource.Token, runnerStatus, BuildConstants.RunnerPackage.Version);
|
||||||
|
|
||||||
|
if (message == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (_getMessagesTokenSource.Token.IsCancellationRequested && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Trace.Info("Get messages has been cancelled using local token source. Continue to get messages with new status.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Trace.Info("Get next message has been cancelled.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (TaskAgentAccessTokenExpiredException)
|
||||||
|
{
|
||||||
|
Trace.Info("Runner OAuth token has been revoked. Unable to pull message.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (AccessDeniedException e) when (e.InnerException is InvalidTaskAgentVersionException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error("Catch exception during get next message.");
|
||||||
|
Trace.Error(ex);
|
||||||
|
|
||||||
|
if (!IsGetNextMessageExceptionRetriable(ex))
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continuousError++;
|
||||||
|
//retry after a random backoff to avoid service throttling
|
||||||
|
//in case of there is a service error happened and all agents get kicked off of the long poll and all agent try to reconnect back at the same time.
|
||||||
|
if (continuousError <= 5)
|
||||||
|
{
|
||||||
|
// random backoff [15, 30]
|
||||||
|
_getNextMessageRetryInterval = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(30), _getNextMessageRetryInterval);
|
||||||
|
}
|
||||||
|
else if (continuousError >= maxRetryCount)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// more aggressive backoff [30, 60]
|
||||||
|
_getNextMessageRetryInterval = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(60), _getNextMessageRetryInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!encounteringError)
|
||||||
|
{
|
||||||
|
//print error only on the first consecutive error
|
||||||
|
_term.WriteError($"{DateTime.UtcNow:u}: Runner connect error: {ex.Message}. Retrying until reconnected.");
|
||||||
|
encounteringError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-create VssConnection before next retry
|
||||||
|
await RefreshBrokerConnection();
|
||||||
|
|
||||||
|
Trace.Info("Sleeping for {0} seconds before retrying.", _getNextMessageRetryInterval.TotalSeconds);
|
||||||
|
await HostContext.Delay(_getNextMessageRetryInterval, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_getMessagesTokenSource.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == null)
|
||||||
|
{
|
||||||
|
if (heartbeat.Elapsed > TimeSpan.FromMinutes(30))
|
||||||
|
{
|
||||||
|
Trace.Info($"No message retrieved within last 30 minutes.");
|
||||||
|
heartbeat.Restart();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Verbose($"No message retrieved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Info($"Message '{message.MessageId}' received.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteMessageAsync(TaskAgentMessage message)
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsGetNextMessageExceptionRetriable(Exception ex)
|
||||||
|
{
|
||||||
|
if (ex is TaskAgentNotFoundException ||
|
||||||
|
ex is TaskAgentPoolNotFoundException ||
|
||||||
|
ex is TaskAgentSessionExpiredException ||
|
||||||
|
ex is AccessDeniedException ||
|
||||||
|
ex is VssUnauthorizedException)
|
||||||
|
{
|
||||||
|
Trace.Info($"Non-retriable exception: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Info($"Retriable exception: {ex.Message}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshBrokerConnection()
|
||||||
|
{
|
||||||
|
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||||
|
_settings = configManager.LoadSettings();
|
||||||
|
|
||||||
|
if (_settings.ServerUrlV2 == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("ServerUrlV2 is not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
var credMgr = HostContext.GetService<ICredentialManager>();
|
||||||
|
VssCredentials creds = credMgr.LoadCredentials();
|
||||||
|
await _brokerServer.ConnectAsync(new Uri(_settings.ServerUrlV2), creds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,12 +31,14 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
{
|
{
|
||||||
private IConfigurationStore _store;
|
private IConfigurationStore _store;
|
||||||
private IRunnerServer _runnerServer;
|
private IRunnerServer _runnerServer;
|
||||||
|
private IRunnerDotcomServer _dotcomServer;
|
||||||
private ITerminal _term;
|
private ITerminal _term;
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
public override void Initialize(IHostContext hostContext)
|
||||||
{
|
{
|
||||||
base.Initialize(hostContext);
|
base.Initialize(hostContext);
|
||||||
_runnerServer = HostContext.GetService<IRunnerServer>();
|
_runnerServer = HostContext.GetService<IRunnerServer>();
|
||||||
|
_dotcomServer = HostContext.GetService<IRunnerDotcomServer>();
|
||||||
Trace.Verbose("Creating _store");
|
Trace.Verbose("Creating _store");
|
||||||
_store = hostContext.GetService<IConfigurationStore>();
|
_store = hostContext.GetService<IConfigurationStore>();
|
||||||
Trace.Verbose("store created");
|
Trace.Verbose("store created");
|
||||||
@@ -113,6 +115,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
ICredentialProvider credProvider = null;
|
ICredentialProvider credProvider = null;
|
||||||
VssCredentials creds = null;
|
VssCredentials creds = null;
|
||||||
_term.WriteSection("Authentication");
|
_term.WriteSection("Authentication");
|
||||||
|
string registerToken = string.Empty;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// When testing against a dev deployment of Actions Service, set this environment variable
|
// When testing against a dev deployment of Actions Service, set this environment variable
|
||||||
@@ -130,9 +133,11 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
runnerSettings.GitHubUrl = inputUrl;
|
runnerSettings.GitHubUrl = inputUrl;
|
||||||
var registerToken = await GetRunnerTokenAsync(command, inputUrl, "registration");
|
registerToken = await GetRunnerTokenAsync(command, inputUrl, "registration");
|
||||||
GitHubAuthResult authResult = await GetTenantCredential(inputUrl, registerToken, Constants.RunnerEvent.Register);
|
GitHubAuthResult authResult = await GetTenantCredential(inputUrl, registerToken, Constants.RunnerEvent.Register);
|
||||||
runnerSettings.ServerUrl = authResult.TenantUrl;
|
runnerSettings.ServerUrl = authResult.TenantUrl;
|
||||||
|
runnerSettings.UseV2Flow = authResult.UseV2Flow;
|
||||||
|
_term.WriteLine($"Using V2 flow: {runnerSettings.UseV2Flow}");
|
||||||
creds = authResult.ToVssCredentials();
|
creds = authResult.ToVssCredentials();
|
||||||
Trace.Info("cred retrieved via GitHub auth");
|
Trace.Info("cred retrieved via GitHub auth");
|
||||||
}
|
}
|
||||||
@@ -176,9 +181,11 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
// We want to use the native CSP of the platform for storage, so we use the RSACSP directly
|
// We want to use the native CSP of the platform for storage, so we use the RSACSP directly
|
||||||
RSAParameters publicKey;
|
RSAParameters publicKey;
|
||||||
var keyManager = HostContext.GetService<IRSAKeyManager>();
|
var keyManager = HostContext.GetService<IRSAKeyManager>();
|
||||||
|
string publicKeyXML;
|
||||||
using (var rsa = keyManager.CreateKey())
|
using (var rsa = keyManager.CreateKey())
|
||||||
{
|
{
|
||||||
publicKey = rsa.ExportParameters(false);
|
publicKey = rsa.ExportParameters(false);
|
||||||
|
publicKeyXML = rsa.ToXmlString(includePrivateParameters: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_term.WriteSection("Runner Registration");
|
_term.WriteSection("Runner Registration");
|
||||||
@@ -186,9 +193,17 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
// If we have more than one runner group available, allow the user to specify which one to be added into
|
// If we have more than one runner group available, allow the user to specify which one to be added into
|
||||||
string poolName = null;
|
string poolName = null;
|
||||||
TaskAgentPool agentPool = null;
|
TaskAgentPool agentPool = null;
|
||||||
List<TaskAgentPool> agentPools = await _runnerServer.GetAgentPoolsAsync();
|
List<TaskAgentPool> agentPools;
|
||||||
TaskAgentPool defaultPool = agentPools?.Where(x => x.IsInternal).FirstOrDefault();
|
if (runnerSettings.UseV2Flow)
|
||||||
|
{
|
||||||
|
agentPools = await _dotcomServer.GetRunnerGroupsAsync(runnerSettings.GitHubUrl, registerToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
agentPools = await _runnerServer.GetAgentPoolsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskAgentPool defaultPool = agentPools?.Where(x => x.IsInternal).FirstOrDefault();
|
||||||
if (agentPools?.Where(x => !x.IsHosted).Count() > 0)
|
if (agentPools?.Where(x => !x.IsHosted).Count() > 0)
|
||||||
{
|
{
|
||||||
poolName = command.GetRunnerGroupName(defaultPool?.Name);
|
poolName = command.GetRunnerGroupName(defaultPool?.Name);
|
||||||
@@ -226,8 +241,16 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
|
|
||||||
var userLabels = command.GetLabels();
|
var userLabels = command.GetLabels();
|
||||||
_term.WriteLine();
|
_term.WriteLine();
|
||||||
|
List<TaskAgent> agents;
|
||||||
|
if (runnerSettings.UseV2Flow)
|
||||||
|
{
|
||||||
|
agents = await _dotcomServer.GetRunnersAsync(runnerSettings.PoolId, runnerSettings.GitHubUrl, registerToken, runnerSettings.AgentName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
agents = await _runnerServer.GetAgentsAsync(runnerSettings.PoolId, runnerSettings.AgentName);
|
||||||
|
}
|
||||||
|
|
||||||
var agents = await _runnerServer.GetAgentsAsync(runnerSettings.PoolId, runnerSettings.AgentName);
|
|
||||||
Trace.Verbose("Returns {0} agents", agents.Count);
|
Trace.Verbose("Returns {0} agents", agents.Count);
|
||||||
agent = agents.FirstOrDefault();
|
agent = agents.FirstOrDefault();
|
||||||
if (agent != null)
|
if (agent != null)
|
||||||
@@ -274,7 +297,23 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
agent = await _runnerServer.AddAgentAsync(runnerSettings.PoolId, agent);
|
if (runnerSettings.UseV2Flow)
|
||||||
|
{
|
||||||
|
var runner = await _dotcomServer.AddRunnerAsync(runnerSettings.PoolId, agent, runnerSettings.GitHubUrl, registerToken, publicKeyXML);
|
||||||
|
runnerSettings.ServerUrlV2 = runner.RunnerAuthorization.ServerUrl;
|
||||||
|
|
||||||
|
agent.Id = runner.Id;
|
||||||
|
agent.Authorization = new TaskAgentAuthorization()
|
||||||
|
{
|
||||||
|
AuthorizationUrl = runner.RunnerAuthorization.AuthorizationUrl,
|
||||||
|
ClientId = new Guid(runner.RunnerAuthorization.ClientId)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
agent = await _runnerServer.AddAgentAsync(runnerSettings.PoolId, agent);
|
||||||
|
}
|
||||||
|
|
||||||
if (command.DisableUpdate &&
|
if (command.DisableUpdate &&
|
||||||
command.DisableUpdate != agent.DisableUpdate)
|
command.DisableUpdate != agent.DisableUpdate)
|
||||||
{
|
{
|
||||||
@@ -325,24 +364,28 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Testing agent connection, detect any potential connection issue, like local clock skew that cause OAuth token expired.
|
// Testing agent connection, detect any potential connection issue, like local clock skew that cause OAuth token expired.
|
||||||
var credMgr = HostContext.GetService<ICredentialManager>();
|
|
||||||
VssCredentials credential = credMgr.LoadCredentials();
|
if (!runnerSettings.UseV2Flow)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), credential);
|
var credMgr = HostContext.GetService<ICredentialManager>();
|
||||||
// ConnectAsync() hits _apis/connectionData which is an anonymous endpoint
|
VssCredentials credential = credMgr.LoadCredentials();
|
||||||
// Need to hit an authenticate endpoint to trigger OAuth token exchange.
|
try
|
||||||
await _runnerServer.GetAgentPoolsAsync();
|
{
|
||||||
_term.WriteSuccessMessage("Runner connection is good");
|
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), credential);
|
||||||
}
|
// ConnectAsync() hits _apis/connectionData which is an anonymous endpoint
|
||||||
catch (VssOAuthTokenRequestException ex) when (ex.Message.Contains("Current server time is"))
|
// Need to hit an authenticate endpoint to trigger OAuth token exchange.
|
||||||
{
|
await _runnerServer.GetAgentPoolsAsync();
|
||||||
// there are two exception messages server send that indicate clock skew.
|
_term.WriteSuccessMessage("Runner connection is good");
|
||||||
// 1. The bearer token expired on {jwt.ValidTo}. Current server time is {DateTime.UtcNow}.
|
}
|
||||||
// 2. The bearer token is not valid until {jwt.ValidFrom}. Current server time is {DateTime.UtcNow}.
|
catch (VssOAuthTokenRequestException ex) when (ex.Message.Contains("Current server time is"))
|
||||||
Trace.Error("Catch exception during test agent connection.");
|
{
|
||||||
Trace.Error(ex);
|
// there are two exception messages server send that indicate clock skew.
|
||||||
throw new Exception("The local machine's clock may be out of sync with the server time by more than five minutes. Please sync your clock with your domain or internet time and try again.");
|
// 1. The bearer token expired on {jwt.ValidTo}. Current server time is {DateTime.UtcNow}.
|
||||||
|
// 2. The bearer token is not valid until {jwt.ValidFrom}. Current server time is {DateTime.UtcNow}.
|
||||||
|
Trace.Error("Catch exception during test agent connection.");
|
||||||
|
Trace.Error(ex);
|
||||||
|
throw new Exception("The local machine's clock may be out of sync with the server time by more than five minutes. Please sync your clock with your domain or internet time and try again.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_term.WriteSection("Runner settings");
|
_term.WriteSection("Runner settings");
|
||||||
@@ -652,7 +695,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
{
|
{
|
||||||
var response = await httpClient.PostAsync(githubApiUrl, new StringContent(string.Empty));
|
var response = await httpClient.PostAsync(githubApiUrl, new StringContent(string.Empty));
|
||||||
responseStatus = response.StatusCode;
|
responseStatus = response.StatusCode;
|
||||||
var githubRequestId = GetGitHubRequestId(response.Headers);
|
var githubRequestId = _dotcomServer.GetGitHubRequestId(response.Headers);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -715,7 +758,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
{
|
{
|
||||||
var response = await httpClient.PostAsync(githubApiUrl, new StringContent(StringUtil.ConvertToJson(bodyObject), null, "application/json"));
|
var response = await httpClient.PostAsync(githubApiUrl, new StringContent(StringUtil.ConvertToJson(bodyObject), null, "application/json"));
|
||||||
responseStatus = response.StatusCode;
|
responseStatus = response.StatusCode;
|
||||||
var githubRequestId = GetGitHubRequestId(response.Headers);
|
var githubRequestId = _dotcomServer.GetGitHubRequestId(response.Headers);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -744,14 +787,5 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetGitHubRequestId(HttpResponseHeaders headers)
|
|
||||||
{
|
|
||||||
if (headers.TryGetValues("x-github-request-id", out var headerValues))
|
|
||||||
{
|
|
||||||
return headerValues.FirstOrDefault();
|
|
||||||
}
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
@@ -20,8 +20,8 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
{
|
{
|
||||||
public static readonly Dictionary<string, Type> CredentialTypes = new(StringComparer.OrdinalIgnoreCase)
|
public static readonly Dictionary<string, Type> CredentialTypes = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ Constants.Configuration.OAuth, typeof(OAuthCredential)},
|
{ Constants.Configuration.OAuth, typeof(OAuthCredential) },
|
||||||
{ Constants.Configuration.OAuthAccessToken, typeof(OAuthAccessTokenCredential)},
|
{ Constants.Configuration.OAuthAccessToken, typeof(OAuthAccessTokenCredential) },
|
||||||
};
|
};
|
||||||
|
|
||||||
public ICredentialProvider GetCredentialProvider(string credType)
|
public ICredentialProvider GetCredentialProvider(string credType)
|
||||||
@@ -93,6 +93,9 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
[DataMember(Name = "token")]
|
[DataMember(Name = "token")]
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
|
|
||||||
|
[DataMember(Name = "use_v2_flow")]
|
||||||
|
public bool UseV2Flow { get; set; }
|
||||||
|
|
||||||
public VssCredentials ToVssCredentials()
|
public VssCredentials ToVssCredentials()
|
||||||
{
|
{
|
||||||
ArgUtil.NotNullOrEmpty(TokenSchema, nameof(TokenSchema));
|
ArgUtil.NotNullOrEmpty(TokenSchema, nameof(TokenSchema));
|
||||||
|
|||||||
3
src/Runner.Listener/InternalsVisibleTo.cs
Normal file
3
src/Runner.Listener/InternalsVisibleTo.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Test")]
|
||||||
@@ -7,6 +7,7 @@ using System.Text;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.Pipelines;
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
@@ -58,6 +59,8 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
public event EventHandler<JobStatusEventArgs> JobStatus;
|
public event EventHandler<JobStatusEventArgs> JobStatus;
|
||||||
|
|
||||||
|
private bool _isRunServiceJob;
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
public override void Initialize(IHostContext hostContext)
|
||||||
{
|
{
|
||||||
base.Initialize(hostContext);
|
base.Initialize(hostContext);
|
||||||
@@ -86,6 +89,8 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
Trace.Info($"Job request {jobRequestMessage.RequestId} for plan {jobRequestMessage.Plan.PlanId} job {jobRequestMessage.JobId} received.");
|
Trace.Info($"Job request {jobRequestMessage.RequestId} for plan {jobRequestMessage.Plan.PlanId} job {jobRequestMessage.JobId} received.");
|
||||||
|
|
||||||
|
_isRunServiceJob = MessageUtil.IsRunServiceJob(jobRequestMessage.MessageType);
|
||||||
|
|
||||||
WorkerDispatcher currentDispatch = null;
|
WorkerDispatcher currentDispatch = null;
|
||||||
if (_jobDispatchedQueue.Count > 0)
|
if (_jobDispatchedQueue.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -239,6 +244,13 @@ namespace GitHub.Runner.Listener
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._isRunServiceJob)
|
||||||
|
{
|
||||||
|
Trace.Error($"We are not yet checking the state of jobrequest {jobDispatch.JobId} status. Cancel running worker right away.");
|
||||||
|
jobDispatch.WorkerCancellationTokenSource.Cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// based on the current design, server will only send one job for a given runner at a time.
|
// based on the current design, server will only send one job for a given runner at a time.
|
||||||
// if the runner received a new job request while a previous job request is still running, this typically indicates two situations
|
// if the runner received a new job request while a previous job request is still running, this typically indicates two situations
|
||||||
// 1. a runner bug caused a server and runner mismatch on the state of the job request, e.g. the runner didn't renew the jobrequest
|
// 1. a runner bug caused a server and runner mismatch on the state of the job request, e.g. the runner didn't renew the jobrequest
|
||||||
@@ -367,9 +379,11 @@ namespace GitHub.Runner.Listener
|
|||||||
long requestId = message.RequestId;
|
long requestId = message.RequestId;
|
||||||
Guid lockToken = Guid.Empty; // lockToken has never been used, keep this here of compat
|
Guid lockToken = Guid.Empty; // lockToken has never been used, keep this here of compat
|
||||||
|
|
||||||
|
var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
// start renew job request
|
// start renew job request
|
||||||
Trace.Info($"Start renew job request {requestId} for job {message.JobId}.");
|
Trace.Info($"Start renew job request {requestId} for job {message.JobId}.");
|
||||||
Task renewJobRequest = RenewJobRequestAsync(_poolId, requestId, lockToken, orchestrationId, firstJobRequestRenewed, lockRenewalTokenSource.Token);
|
Task renewJobRequest = RenewJobRequestAsync(message, systemConnection, _poolId, requestId, lockToken, orchestrationId, firstJobRequestRenewed, lockRenewalTokenSource.Token);
|
||||||
|
|
||||||
// wait till first renew succeed or job request is cancelled
|
// wait till first renew succeed or job request is cancelled
|
||||||
// not even start worker if the first renew fail
|
// not even start worker if the first renew fail
|
||||||
@@ -426,7 +440,7 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
workerOutput.Add(stdout.Data);
|
workerOutput.Add(stdout.Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (printToStdout)
|
if (printToStdout)
|
||||||
{
|
{
|
||||||
term.WriteLine(stdout.Data, skipTracing: true);
|
term.WriteLine(stdout.Data, skipTracing: true);
|
||||||
@@ -508,7 +522,6 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
// we get first jobrequest renew succeed and start the worker process with the job message.
|
// we get first jobrequest renew succeed and start the worker process with the job message.
|
||||||
// send notification to machine provisioner.
|
// send notification to machine provisioner.
|
||||||
var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
|
||||||
var accessToken = systemConnection?.Authorization?.Parameters["AccessToken"];
|
var accessToken = systemConnection?.Authorization?.Parameters["AccessToken"];
|
||||||
notification.JobStarted(message.JobId, accessToken, systemConnection.Url);
|
notification.JobStarted(message.JobId, accessToken, systemConnection.Url);
|
||||||
|
|
||||||
@@ -531,11 +544,8 @@ namespace GitHub.Runner.Listener
|
|||||||
detailInfo = string.Join(Environment.NewLine, workerOutput);
|
detailInfo = string.Join(Environment.NewLine, workerOutput);
|
||||||
Trace.Info($"Return code {returnCode} indicate worker encounter an unhandled exception or app crash, attach worker stdout/stderr to JobRequest result.");
|
Trace.Info($"Return code {returnCode} indicate worker encounter an unhandled exception or app crash, attach worker stdout/stderr to JobRequest result.");
|
||||||
|
|
||||||
var jobServer = HostContext.GetService<IJobServer>();
|
|
||||||
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
|
|
||||||
VssConnection jobConnection = VssUtil.CreateConnection(systemConnection.Url, jobServerCredential);
|
|
||||||
await jobServer.ConnectAsync(jobConnection);
|
|
||||||
|
|
||||||
|
var jobServer = await InitializeJobServerAsync(systemConnection);
|
||||||
await LogWorkerProcessUnhandledException(jobServer, message, detailInfo);
|
await LogWorkerProcessUnhandledException(jobServer, message, detailInfo);
|
||||||
|
|
||||||
// Go ahead to finish the job with result 'Failed' if the STDERR from worker is System.IO.IOException, since it typically means we are running out of disk space.
|
// Go ahead to finish the job with result 'Failed' if the STDERR from worker is System.IO.IOException, since it typically means we are running out of disk space.
|
||||||
@@ -675,9 +685,128 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RenewJobRequestAsync(int poolId, long requestId, Guid lockToken, string orchestrationId, TaskCompletionSource<int> firstJobRequestRenewed, CancellationToken token)
|
internal async Task RenewJobRequestAsync(Pipelines.AgentJobRequestMessage message, ServiceEndpoint systemConnection, int poolId, long requestId, Guid lockToken, string orchestrationId, TaskCompletionSource<int> firstJobRequestRenewed, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (this._isRunServiceJob)
|
||||||
|
{
|
||||||
|
var runServer = await GetRunServerAsync(systemConnection);
|
||||||
|
await RenewJobRequestAsync(runServer, message.Plan.PlanId, message.JobId, firstJobRequestRenewed, token);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var runnerServer = HostContext.GetService<IRunnerServer>();
|
||||||
|
await RenewJobRequestAsync(runnerServer, poolId, requestId, lockToken, orchestrationId, firstJobRequestRenewed, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RenewJobRequestAsync(IRunServer runServer, Guid planId, Guid jobId, TaskCompletionSource<int> firstJobRequestRenewed, CancellationToken token)
|
||||||
|
{
|
||||||
|
TaskAgentJobRequest request = null;
|
||||||
|
int firstRenewRetryLimit = 5;
|
||||||
|
int encounteringError = 0;
|
||||||
|
|
||||||
|
// renew lock during job running.
|
||||||
|
// stop renew only if cancellation token for lock renew task been signal or exception still happen after retry.
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var renewResponse = await runServer.RenewJobAsync(planId, jobId, token);
|
||||||
|
Trace.Info($"Successfully renew job {jobId}, job is valid till {renewResponse.LockedUntil}");
|
||||||
|
|
||||||
|
if (!firstJobRequestRenewed.Task.IsCompleted)
|
||||||
|
{
|
||||||
|
// fire first renew succeed event.
|
||||||
|
firstJobRequestRenewed.TrySetResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encounteringError > 0)
|
||||||
|
{
|
||||||
|
encounteringError = 0;
|
||||||
|
HostContext.WritePerfCounter("JobRenewRecovered");
|
||||||
|
}
|
||||||
|
|
||||||
|
// renew again after 60 sec delay
|
||||||
|
await HostContext.Delay(TimeSpan.FromSeconds(60), token);
|
||||||
|
}
|
||||||
|
catch (TaskOrchestrationJobNotFoundException)
|
||||||
|
{
|
||||||
|
// no need for retry. the job is not valid anymore.
|
||||||
|
Trace.Info($"TaskAgentJobNotFoundException received when renew job {jobId}, job is no longer valid, stop renew job request.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// OperationCanceledException may caused by http timeout or _lockRenewalTokenSource.Cance();
|
||||||
|
// Stop renew only on cancellation token fired.
|
||||||
|
Trace.Info($"job renew has been cancelled, stop renew job {jobId}.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error($"Catch exception during renew runner job {jobId}.");
|
||||||
|
Trace.Error(ex);
|
||||||
|
encounteringError++;
|
||||||
|
|
||||||
|
// retry
|
||||||
|
TimeSpan remainingTime = TimeSpan.Zero;
|
||||||
|
if (!firstJobRequestRenewed.Task.IsCompleted)
|
||||||
|
{
|
||||||
|
// retry 5 times every 10 sec for the first renew
|
||||||
|
if (firstRenewRetryLimit-- > 0)
|
||||||
|
{
|
||||||
|
remainingTime = TimeSpan.FromSeconds(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// retry till reach lockeduntil + 5 mins extra buffer.
|
||||||
|
remainingTime = request.LockedUntil.Value + TimeSpan.FromMinutes(5) - DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingTime > TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
TimeSpan delayTime;
|
||||||
|
if (!firstJobRequestRenewed.Task.IsCompleted)
|
||||||
|
{
|
||||||
|
Trace.Info($"Retrying lock renewal for job {jobId}. The first job renew request has failed.");
|
||||||
|
delayTime = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Info($"Retrying lock renewal for job {jobId}. Job is valid until {request.LockedUntil.Value}.");
|
||||||
|
if (encounteringError > 5)
|
||||||
|
{
|
||||||
|
delayTime = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(30));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
delayTime = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// back-off before next retry.
|
||||||
|
await HostContext.Delay(delayTime, token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Trace.Info($"job renew has been cancelled, stop renew job {jobId}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Info($"Lock renewal has run out of retry, stop renew lock for job {jobId}.");
|
||||||
|
HostContext.WritePerfCounter("JobRenewReachLimit");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RenewJobRequestAsync(IRunnerServer runnerServer, int poolId, long requestId, Guid lockToken, string orchestrationId, TaskCompletionSource<int> firstJobRequestRenewed, CancellationToken token)
|
||||||
{
|
{
|
||||||
var runnerServer = HostContext.GetService<IRunnerServer>();
|
|
||||||
TaskAgentJobRequest request = null;
|
TaskAgentJobRequest request = null;
|
||||||
int firstRenewRetryLimit = 5;
|
int firstRenewRetryLimit = 5;
|
||||||
int encounteringError = 0;
|
int encounteringError = 0;
|
||||||
@@ -840,90 +969,93 @@ namespace GitHub.Runner.Listener
|
|||||||
var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection));
|
var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection));
|
||||||
ArgUtil.NotNull(systemConnection, nameof(systemConnection));
|
ArgUtil.NotNull(systemConnection, nameof(systemConnection));
|
||||||
|
|
||||||
var jobServer = HostContext.GetService<IJobServer>();
|
var server = await InitializeJobServerAsync(systemConnection);
|
||||||
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
|
|
||||||
VssConnection jobConnection = VssUtil.CreateConnection(systemConnection.Url, jobServerCredential);
|
|
||||||
|
|
||||||
await jobServer.ConnectAsync(jobConnection);
|
if (server is IJobServer jobServer)
|
||||||
|
|
||||||
var timeline = await jobServer.GetTimelineAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, CancellationToken.None);
|
|
||||||
|
|
||||||
var updatedRecords = new List<TimelineRecord>();
|
|
||||||
var logPages = new Dictionary<Guid, Dictionary<int, string>>();
|
|
||||||
var logRecords = new Dictionary<Guid, TimelineRecord>();
|
|
||||||
foreach (var log in logs)
|
|
||||||
{
|
{
|
||||||
var logName = Path.GetFileNameWithoutExtension(log);
|
var timeline = await jobServer.GetTimelineAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, CancellationToken.None);
|
||||||
var logNameParts = logName.Split('_', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
if (logNameParts.Length != 3)
|
|
||||||
{
|
|
||||||
Trace.Warning($"log file '{log}' doesn't follow naming convension 'GUID_GUID_INT'.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var logPageSeperator = logName.IndexOf('_');
|
|
||||||
var logRecordId = Guid.Empty;
|
|
||||||
var pageNumber = 0;
|
|
||||||
|
|
||||||
if (!Guid.TryParse(logNameParts[0], out Guid timelineId) || timelineId != timeline.Id)
|
var updatedRecords = new List<TimelineRecord>();
|
||||||
|
var logPages = new Dictionary<Guid, Dictionary<int, string>>();
|
||||||
|
var logRecords = new Dictionary<Guid, TimelineRecord>();
|
||||||
|
foreach (var log in logs)
|
||||||
{
|
{
|
||||||
Trace.Warning($"log file '{log}' is not belongs to current job");
|
var logName = Path.GetFileNameWithoutExtension(log);
|
||||||
continue;
|
var logNameParts = logName.Split('_', StringSplitOptions.RemoveEmptyEntries);
|
||||||
}
|
if (logNameParts.Length != 3)
|
||||||
|
|
||||||
if (!Guid.TryParse(logNameParts[1], out logRecordId))
|
|
||||||
{
|
|
||||||
Trace.Warning($"log file '{log}' doesn't follow naming convension 'GUID_GUID_INT'.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!int.TryParse(logNameParts[2], out pageNumber))
|
|
||||||
{
|
|
||||||
Trace.Warning($"log file '{log}' doesn't follow naming convension 'GUID_GUID_INT'.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var record = timeline.Records.FirstOrDefault(x => x.Id == logRecordId);
|
|
||||||
if (record != null)
|
|
||||||
{
|
|
||||||
if (!logPages.ContainsKey(record.Id))
|
|
||||||
{
|
{
|
||||||
logPages[record.Id] = new Dictionary<int, string>();
|
Trace.Warning($"log file '{log}' doesn't follow naming convension 'GUID_GUID_INT'.");
|
||||||
logRecords[record.Id] = record;
|
continue;
|
||||||
|
}
|
||||||
|
var logPageSeperator = logName.IndexOf('_');
|
||||||
|
var logRecordId = Guid.Empty;
|
||||||
|
var pageNumber = 0;
|
||||||
|
|
||||||
|
if (!Guid.TryParse(logNameParts[0], out Guid timelineId) || timelineId != timeline.Id)
|
||||||
|
{
|
||||||
|
Trace.Warning($"log file '{log}' is not belongs to current job");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
logPages[record.Id][pageNumber] = log;
|
if (!Guid.TryParse(logNameParts[1], out logRecordId))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var pages in logPages)
|
|
||||||
{
|
|
||||||
var record = logRecords[pages.Key];
|
|
||||||
if (record.Log == null)
|
|
||||||
{
|
|
||||||
// Create the log
|
|
||||||
record.Log = await jobServer.CreateLogAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, new TaskLog(String.Format(@"logs\{0:D}", record.Id)), default(CancellationToken));
|
|
||||||
|
|
||||||
// Need to post timeline record updates to reflect the log creation
|
|
||||||
updatedRecords.Add(record.Clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 1; i <= pages.Value.Count; i++)
|
|
||||||
{
|
|
||||||
var logFile = pages.Value[i];
|
|
||||||
// Upload the contents
|
|
||||||
using (FileStream fs = File.Open(logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
|
||||||
{
|
{
|
||||||
var logUploaded = await jobServer.AppendLogContentAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, record.Log.Id, fs, default(CancellationToken));
|
Trace.Warning($"log file '{log}' doesn't follow naming convension 'GUID_GUID_INT'.");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Trace.Info($"Uploaded unfinished log '{logFile}' for current job.");
|
if (!int.TryParse(logNameParts[2], out pageNumber))
|
||||||
IOUtil.DeleteFile(logFile);
|
{
|
||||||
|
Trace.Warning($"log file '{log}' doesn't follow naming convension 'GUID_GUID_INT'.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var record = timeline.Records.FirstOrDefault(x => x.Id == logRecordId);
|
||||||
|
if (record != null)
|
||||||
|
{
|
||||||
|
if (!logPages.ContainsKey(record.Id))
|
||||||
|
{
|
||||||
|
logPages[record.Id] = new Dictionary<int, string>();
|
||||||
|
logRecords[record.Id] = record;
|
||||||
|
}
|
||||||
|
|
||||||
|
logPages[record.Id][pageNumber] = log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pages in logPages)
|
||||||
|
{
|
||||||
|
var record = logRecords[pages.Key];
|
||||||
|
if (record.Log == null)
|
||||||
|
{
|
||||||
|
// Create the log
|
||||||
|
record.Log = await jobServer.CreateLogAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, new TaskLog(String.Format(@"logs\{0:D}", record.Id)), default(CancellationToken));
|
||||||
|
|
||||||
|
// Need to post timeline record updates to reflect the log creation
|
||||||
|
updatedRecords.Add(record.Clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 1; i <= pages.Value.Count; i++)
|
||||||
|
{
|
||||||
|
var logFile = pages.Value[i];
|
||||||
|
// Upload the contents
|
||||||
|
using (FileStream fs = File.Open(logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||||
|
{
|
||||||
|
var logUploaded = await jobServer.AppendLogContentAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, record.Log.Id, fs, default(CancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Info($"Uploaded unfinished log '{logFile}' for current job.");
|
||||||
|
IOUtil.DeleteFile(logFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedRecords.Count > 0)
|
||||||
|
{
|
||||||
|
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, updatedRecords, CancellationToken.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (updatedRecords.Count > 0)
|
|
||||||
{
|
{
|
||||||
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, updatedRecords, CancellationToken.None);
|
Trace.Info("Job server does not support log upload yet.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -943,6 +1075,12 @@ namespace GitHub.Runner.Listener
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._isRunServiceJob)
|
||||||
|
{
|
||||||
|
Trace.Verbose($"Skip FinishAgentRequest call from Listener because MessageType is {message.MessageType}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var runnerServer = HostContext.GetService<IRunnerServer>();
|
var runnerServer = HostContext.GetService<IRunnerServer>();
|
||||||
int completeJobRequestRetryLimit = 5;
|
int completeJobRequestRetryLimit = 5;
|
||||||
List<Exception> exceptions = new();
|
List<Exception> exceptions = new();
|
||||||
@@ -979,66 +1117,117 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
|
|
||||||
// log an error issue to job level timeline record
|
// log an error issue to job level timeline record
|
||||||
private async Task LogWorkerProcessUnhandledException(IJobServer jobServer, Pipelines.AgentJobRequestMessage message, string errorMessage)
|
private async Task LogWorkerProcessUnhandledException(IRunnerService server, Pipelines.AgentJobRequestMessage message, string errorMessage)
|
||||||
{
|
{
|
||||||
try
|
if (server is IJobServer jobServer)
|
||||||
{
|
{
|
||||||
var timeline = await jobServer.GetTimelineAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, CancellationToken.None);
|
|
||||||
ArgUtil.NotNull(timeline, nameof(timeline));
|
|
||||||
|
|
||||||
TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job");
|
|
||||||
ArgUtil.NotNull(jobRecord, nameof(jobRecord));
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(errorMessage) &&
|
var timeline = await jobServer.GetTimelineAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, CancellationToken.None);
|
||||||
message.Variables.TryGetValue("DistributedTask.EnableRunnerIPCDebug", out var enableRunnerIPCDebug) &&
|
ArgUtil.NotNull(timeline, nameof(timeline));
|
||||||
StringUtil.ConvertToBoolean(enableRunnerIPCDebug.Value))
|
|
||||||
|
TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job");
|
||||||
|
ArgUtil.NotNull(jobRecord, nameof(jobRecord));
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
// the trace should be best effort and not affect any job result
|
if (!string.IsNullOrEmpty(errorMessage) &&
|
||||||
var match = _invalidJsonRegex.Match(errorMessage);
|
message.Variables.TryGetValue("DistributedTask.EnableRunnerIPCDebug", out var enableRunnerIPCDebug) &&
|
||||||
if (match.Success &&
|
StringUtil.ConvertToBoolean(enableRunnerIPCDebug.Value))
|
||||||
match.Groups.Count == 2)
|
|
||||||
{
|
{
|
||||||
var jsonPosition = int.Parse(match.Groups[1].Value);
|
// the trace should be best effort and not affect any job result
|
||||||
var serializedJobMessage = JsonUtility.ToString(message);
|
var match = _invalidJsonRegex.Match(errorMessage);
|
||||||
var originalJson = serializedJobMessage.Substring(jsonPosition - 10, 20);
|
if (match.Success &&
|
||||||
errorMessage = $"Runner sent Json at position '{jsonPosition}': {originalJson} ({Convert.ToBase64String(Encoding.UTF8.GetBytes(originalJson))})\n{errorMessage}";
|
match.Groups.Count == 2)
|
||||||
|
{
|
||||||
|
var jsonPosition = int.Parse(match.Groups[1].Value);
|
||||||
|
var serializedJobMessage = JsonUtility.ToString(message);
|
||||||
|
var originalJson = serializedJobMessage.Substring(jsonPosition - 10, 20);
|
||||||
|
errorMessage = $"Runner sent Json at position '{jsonPosition}': {originalJson} ({Convert.ToBase64String(Encoding.UTF8.GetBytes(originalJson))})\n{errorMessage}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error(ex);
|
||||||
|
errorMessage = $"Fail to check json IPC error: {ex.Message}\n{errorMessage}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = errorMessage };
|
||||||
|
unhandledExceptionIssue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
|
||||||
|
jobRecord.ErrorCount++;
|
||||||
|
jobRecord.Issues.Add(unhandledExceptionIssue);
|
||||||
|
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Trace.Error("Fail to report unhandled exception from Runner.Worker process");
|
||||||
Trace.Error(ex);
|
Trace.Error(ex);
|
||||||
errorMessage = $"Fail to check json IPC error: {ex.Message}\n{errorMessage}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = errorMessage };
|
|
||||||
unhandledExceptionIssue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
|
|
||||||
jobRecord.ErrorCount++;
|
|
||||||
jobRecord.Issues.Add(unhandledExceptionIssue);
|
|
||||||
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else
|
||||||
{
|
{
|
||||||
Trace.Error("Fail to report unhandled exception from Runner.Worker process");
|
Trace.Info("Job server does not support handling unhandled exception yet, error message: {0}", errorMessage);
|
||||||
Trace.Error(ex);
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// raise job completed event to fail the job.
|
// raise job completed event to fail the job.
|
||||||
private async Task ForceFailJob(IJobServer jobServer, Pipelines.AgentJobRequestMessage message)
|
private async Task ForceFailJob(IRunnerService server, Pipelines.AgentJobRequestMessage message)
|
||||||
{
|
{
|
||||||
try
|
if (server is IJobServer jobServer)
|
||||||
{
|
{
|
||||||
var jobCompletedEvent = new JobCompletedEvent(message.RequestId, message.JobId, TaskResult.Failed);
|
try
|
||||||
await jobServer.RaisePlanEventAsync<JobCompletedEvent>(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, jobCompletedEvent, CancellationToken.None);
|
{
|
||||||
|
var jobCompletedEvent = new JobCompletedEvent(message.RequestId, message.JobId, TaskResult.Failed);
|
||||||
|
await jobServer.RaisePlanEventAsync<JobCompletedEvent>(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, jobCompletedEvent, CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error("Fail to raise JobCompletedEvent back to service.");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else if (server is IRunServer runServer)
|
||||||
{
|
{
|
||||||
Trace.Error("Fail to raise JobCompletedEvent back to service.");
|
try
|
||||||
Trace.Error(ex);
|
{
|
||||||
|
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error("Fail to raise job completion back to service.");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"Server type {server.GetType().FullName} is not supported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IRunnerService> InitializeJobServerAsync(ServiceEndpoint systemConnection)
|
||||||
|
{
|
||||||
|
if (this._isRunServiceJob)
|
||||||
|
{
|
||||||
|
return await GetRunServerAsync(systemConnection);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var jobServer = HostContext.GetService<IJobServer>();
|
||||||
|
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
|
||||||
|
VssConnection jobConnection = VssUtil.CreateConnection(systemConnection.Url, jobServerCredential);
|
||||||
|
await jobServer.ConnectAsync(jobConnection);
|
||||||
|
return jobServer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IRunServer> GetRunServerAsync(ServiceEndpoint systemConnection)
|
||||||
|
{
|
||||||
|
var runServer = HostContext.GetService<IRunServer>();
|
||||||
|
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
|
||||||
|
await runServer.ConnectAsync(systemConnection.Url, jobServerCredential);
|
||||||
|
return runServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class WorkerDispatcher : IDisposable
|
private class WorkerDispatcher : IDisposable
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ namespace GitHub.Runner.Listener
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_getMessagesTokenSource?.Cancel();
|
_getMessagesTokenSource?.Cancel();
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException)
|
catch (ObjectDisposedException)
|
||||||
{
|
{
|
||||||
Trace.Info("_getMessagesTokenSource is already disposed.");
|
Trace.Info("_getMessagesTokenSource is already disposed.");
|
||||||
@@ -245,6 +245,10 @@ namespace GitHub.Runner.Listener
|
|||||||
_accessTokenRevoked = true;
|
_accessTokenRevoked = true;
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
catch (AccessDeniedException e) when (e.InnerException is InvalidTaskAgentVersionException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Trace.Error("Catch exception during get next message.");
|
Trace.Error("Catch exception during get next message.");
|
||||||
@@ -289,7 +293,7 @@ namespace GitHub.Runner.Listener
|
|||||||
await HostContext.Delay(_getNextMessageRetryInterval, token);
|
await HostContext.Delay(_getNextMessageRetryInterval, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_getMessagesTokenSource.Dispose();
|
_getMessagesTokenSource.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.IO;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
|
||||||
namespace GitHub.Runner.Listener
|
namespace GitHub.Runner.Listener
|
||||||
{
|
{
|
||||||
@@ -58,7 +59,7 @@ namespace GitHub.Runner.Listener
|
|||||||
terminal.WriteLine("This runner version is built for Windows. Please install a correct build for your OS.");
|
terminal.WriteLine("This runner version is built for Windows. Please install a correct build for your OS.");
|
||||||
return Constants.Runner.ReturnCode.TerminatedError;
|
return Constants.Runner.ReturnCode.TerminatedError;
|
||||||
}
|
}
|
||||||
#if ARM64
|
#if ARM64
|
||||||
// A little hacky, but windows gives no way to differentiate between windows 10 and 11.
|
// A little hacky, but windows gives no way to differentiate between windows 10 and 11.
|
||||||
// By default only 11 supports native x64 app emulation on arm, so we only want to support windows 11
|
// By default only 11 supports native x64 app emulation on arm, so we only want to support windows 11
|
||||||
// https://docs.microsoft.com/en-us/windows/arm/overview#build-windows-apps-that-run-on-arm
|
// https://docs.microsoft.com/en-us/windows/arm/overview#build-windows-apps-that-run-on-arm
|
||||||
@@ -69,7 +70,7 @@ namespace GitHub.Runner.Listener
|
|||||||
terminal.WriteLine("Win-arm64 runners require windows 11 or later. Please upgrade your operating system.");
|
terminal.WriteLine("Win-arm64 runners require windows 11 or later. Please upgrade your operating system.");
|
||||||
return Constants.Runner.ReturnCode.TerminatedError;
|
return Constants.Runner.ReturnCode.TerminatedError;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
terminal.WriteLine($"Running the runner on this platform is not supported. The current platform is {RuntimeInformation.OSDescription} and it was built for {Constants.Runner.Platform.ToString()}.");
|
terminal.WriteLine($"Running the runner on this platform is not supported. The current platform is {RuntimeInformation.OSDescription} and it was built for {Constants.Runner.Platform.ToString()}.");
|
||||||
@@ -137,6 +138,12 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
catch (AccessDeniedException e) when (e.InnerException is InvalidTaskAgentVersionException)
|
||||||
|
{
|
||||||
|
terminal.WriteError($"An error occured: {e.Message}");
|
||||||
|
trace.Error(e);
|
||||||
|
return Constants.Runner.ReturnCode.TerminatedError;
|
||||||
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
terminal.WriteError($"An error occurred: {e.Message}");
|
terminal.WriteError($"An error occurred: {e.Message}");
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Listener.Check;
|
using GitHub.Runner.Listener.Check;
|
||||||
using GitHub.Runner.Listener.Configuration;
|
using GitHub.Runner.Listener.Configuration;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
@@ -136,7 +138,7 @@ namespace GitHub.Runner.Listener
|
|||||||
if (command.Remove)
|
if (command.Remove)
|
||||||
{
|
{
|
||||||
// only remove local config files and exit
|
// only remove local config files and exit
|
||||||
if(command.RemoveLocalConfig)
|
if (command.RemoveLocalConfig)
|
||||||
{
|
{
|
||||||
configManager.DeleteLocalRunnerConfig();
|
configManager.DeleteLocalRunnerConfig();
|
||||||
return Constants.Runner.ReturnCode.Success;
|
return Constants.Runner.ReturnCode.Success;
|
||||||
@@ -209,10 +211,16 @@ namespace GitHub.Runner.Listener
|
|||||||
foreach (var config in jitConfig)
|
foreach (var config in jitConfig)
|
||||||
{
|
{
|
||||||
var configFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), config.Key);
|
var configFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), config.Key);
|
||||||
var configContent = Encoding.UTF8.GetString(Convert.FromBase64String(config.Value));
|
var configContent = Convert.FromBase64String(config.Value);
|
||||||
File.WriteAllText(configFile, configContent, Encoding.UTF8);
|
#if OS_WINDOWS
|
||||||
|
if (configFile == HostContext.GetConfigFile(WellKnownConfigFile.RSACredentials))
|
||||||
|
{
|
||||||
|
configContent = ProtectedData.Protect(configContent, null, DataProtectionScope.LocalMachine);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
File.WriteAllBytes(configFile, configContent);
|
||||||
File.SetAttributes(configFile, File.GetAttributes(configFile) | FileAttributes.Hidden);
|
File.SetAttributes(configFile, File.GetAttributes(configFile) | FileAttributes.Hidden);
|
||||||
Trace.Info($"Save {configContent.Length} chars to '{configFile}'.");
|
Trace.Info($"Saved {configContent.Length} bytes to '{configFile}'.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -331,13 +339,26 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IMessageListener GetMesageListener(RunnerSettings settings)
|
||||||
|
{
|
||||||
|
if (settings.UseV2Flow)
|
||||||
|
{
|
||||||
|
Trace.Info($"Using BrokerMessageListener");
|
||||||
|
var brokerListener = new BrokerMessageListener();
|
||||||
|
brokerListener.Initialize(HostContext);
|
||||||
|
return brokerListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HostContext.GetService<IMessageListener>();
|
||||||
|
}
|
||||||
|
|
||||||
//create worker manager, create message listener and start listening to the queue
|
//create worker manager, create message listener and start listening to the queue
|
||||||
private async Task<int> RunAsync(RunnerSettings settings, bool runOnce = false)
|
private async Task<int> RunAsync(RunnerSettings settings, bool runOnce = false)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Trace.Info(nameof(RunAsync));
|
Trace.Info(nameof(RunAsync));
|
||||||
_listener = HostContext.GetService<IMessageListener>();
|
_listener = GetMesageListener(settings);
|
||||||
if (!await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken))
|
if (!await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken))
|
||||||
{
|
{
|
||||||
return Constants.Runner.ReturnCode.TerminatedError;
|
return Constants.Runner.ReturnCode.TerminatedError;
|
||||||
@@ -502,7 +523,7 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Broker flow
|
// Broker flow
|
||||||
else if (string.Equals(message.MessageType, JobRequestMessageTypes.RunnerJobRequest, StringComparison.OrdinalIgnoreCase))
|
else if (MessageUtil.IsRunServiceJob(message.MessageType))
|
||||||
{
|
{
|
||||||
if (autoUpdateInProgress || runOnceJobReceived)
|
if (autoUpdateInProgress || runOnceJobReceived)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ namespace GitHub.Runner.Sdk
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_noProxyList.Add(noProxyInfo);
|
_noProxyList.Add(noProxyInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,6 +206,11 @@ namespace GitHub.Runner.Sdk
|
|||||||
{
|
{
|
||||||
foreach (var noProxy in _noProxyList)
|
foreach (var noProxy in _noProxyList)
|
||||||
{
|
{
|
||||||
|
// bypass on wildcard no_proxy
|
||||||
|
if (string.Equals(noProxy.Host, "*", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
var matchHost = false;
|
var matchHost = false;
|
||||||
var matchPort = false;
|
var matchPort = false;
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,19 @@ namespace GitHub.Runner.Sdk
|
|||||||
File.WriteAllText(path, StringUtil.ConvertToJson(obj), Encoding.UTF8);
|
File.WriteAllText(path, StringUtil.ConvertToJson(obj), Encoding.UTF8);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T LoadObject<T>(string path)
|
public static T LoadObject<T>(string path, bool required = false)
|
||||||
{
|
{
|
||||||
string json = File.ReadAllText(path, Encoding.UTF8);
|
string json = File.ReadAllText(path, Encoding.UTF8);
|
||||||
return StringUtil.ConvertFromJson<T>(json);
|
if (required && string.IsNullOrEmpty(json))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException($"File {path} is empty");
|
||||||
|
}
|
||||||
|
T result = StringUtil.ConvertFromJson<T>(json);
|
||||||
|
if (required && result == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Converting json to object resulted in a null value");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetSha256Hash(string path)
|
public static string GetSha256Hash(string path)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -81,7 +81,7 @@ namespace GitHub.Runner.Worker
|
|||||||
// logging
|
// logging
|
||||||
long Write(string tag, string message);
|
long Write(string tag, string message);
|
||||||
void QueueAttachFile(string type, string name, string filePath);
|
void QueueAttachFile(string type, string name, string filePath);
|
||||||
void QueueSummaryFile(string name, string filePath, Guid stepRecordId);
|
void QueueSummaryFile(string name, string filePath, Guid stepRecordId);
|
||||||
|
|
||||||
// timeline record update methods
|
// timeline record update methods
|
||||||
void Start(string currentOperation = null);
|
void Start(string currentOperation = null);
|
||||||
@@ -871,8 +871,7 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
throw new FileNotFoundException($"Can't upload (name:{name}) file: {filePath}. File does not exist.");
|
throw new FileNotFoundException($"Can't upload (name:{name}) file: {filePath}. File does not exist.");
|
||||||
}
|
}
|
||||||
|
_jobServerQueue.QueueResultsUpload(stepRecordId, name, filePath, ChecksAttachmentType.StepSummary, deleteSource: false, finalize: true, firstBlock: true, totalLines: 0);
|
||||||
_jobServerQueue.QueueSummaryUpload(stepRecordId, name, filePath, deleteSource: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add OnMatcherChanged
|
// Add OnMatcherChanged
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Net.Http;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.Pipelines;
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
@@ -19,7 +20,7 @@ namespace GitHub.Runner.Worker
|
|||||||
[ServiceLocator(Default = typeof(JobRunner))]
|
[ServiceLocator(Default = typeof(JobRunner))]
|
||||||
public interface IJobRunner : IRunnerService
|
public interface IJobRunner : IRunnerService
|
||||||
{
|
{
|
||||||
Task<TaskResult> RunAsync(Pipelines.AgentJobRequestMessage message, CancellationToken jobRequestCancellationToken);
|
Task<TaskResult> RunAsync(AgentJobRequestMessage message, CancellationToken jobRequestCancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class JobRunner : RunnerService, IJobRunner
|
public sealed class JobRunner : RunnerService, IJobRunner
|
||||||
@@ -28,7 +29,7 @@ namespace GitHub.Runner.Worker
|
|||||||
private RunnerSettings _runnerSettings;
|
private RunnerSettings _runnerSettings;
|
||||||
private ITempDirectoryManager _tempDirectoryManager;
|
private ITempDirectoryManager _tempDirectoryManager;
|
||||||
|
|
||||||
public async Task<TaskResult> RunAsync(Pipelines.AgentJobRequestMessage message, CancellationToken jobRequestCancellationToken)
|
public async Task<TaskResult> RunAsync(AgentJobRequestMessage message, CancellationToken jobRequestCancellationToken)
|
||||||
{
|
{
|
||||||
// Validate parameters.
|
// Validate parameters.
|
||||||
Trace.Entering();
|
Trace.Entering();
|
||||||
@@ -42,14 +43,14 @@ namespace GitHub.Runner.Worker
|
|||||||
IRunnerService server = null;
|
IRunnerService server = null;
|
||||||
|
|
||||||
ServiceEndpoint systemConnection = message.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
ServiceEndpoint systemConnection = message.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||||
if (string.Equals(message.MessageType, JobRequestMessageTypes.RunnerJobRequest, StringComparison.OrdinalIgnoreCase))
|
if (MessageUtil.IsRunServiceJob(message.MessageType))
|
||||||
{
|
{
|
||||||
var runServer = HostContext.GetService<IRunServer>();
|
var runServer = HostContext.GetService<IRunServer>();
|
||||||
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
|
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
|
||||||
await runServer.ConnectAsync(systemConnection.Url, jobServerCredential);
|
await runServer.ConnectAsync(systemConnection.Url, jobServerCredential);
|
||||||
server = runServer;
|
server = runServer;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Setup the job server and job server queue.
|
// Setup the job server and job server queue.
|
||||||
var jobServer = HostContext.GetService<IJobServer>();
|
var jobServer = HostContext.GetService<IJobServer>();
|
||||||
@@ -65,7 +66,7 @@ namespace GitHub.Runner.Worker
|
|||||||
_jobServerQueue.Start(message);
|
_jobServerQueue.Start(message);
|
||||||
server = jobServer;
|
server = jobServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
HostContext.WritePerfCounter($"WorkerJobServerQueueStarted_{message.RequestId.ToString()}");
|
HostContext.WritePerfCounter($"WorkerJobServerQueueStarted_{message.RequestId.ToString()}");
|
||||||
|
|
||||||
|
|||||||
@@ -184,9 +184,33 @@ namespace GitHub.Services.Common
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum size allowed for response content buffering.
|
||||||
|
/// </summary>
|
||||||
|
[DefaultValue(c_defaultContentBufferSize)]
|
||||||
|
public Int32 MaxContentBufferSize
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return m_maxContentBufferSize;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
ArgumentUtility.CheckForOutOfRange(value, nameof(value), 0, c_maxAllowedContentBufferSize);
|
||||||
|
m_maxContentBufferSize = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Lazy<RawClientHttpRequestSettings> s_defaultSettings
|
private static Lazy<RawClientHttpRequestSettings> s_defaultSettings
|
||||||
= new Lazy<RawClientHttpRequestSettings>(ConstructDefaultSettings);
|
= new Lazy<RawClientHttpRequestSettings>(ConstructDefaultSettings);
|
||||||
|
|
||||||
|
private Int32 m_maxContentBufferSize;
|
||||||
|
// We will buffer a maximum of 1024MB in the message handler
|
||||||
|
private const Int32 c_maxAllowedContentBufferSize = 1024 * 1024 * 1024;
|
||||||
|
|
||||||
|
// We will buffer, by default, up to 512MB in the message handler
|
||||||
|
private const Int32 c_defaultContentBufferSize = 1024 * 1024 * 512;
|
||||||
|
|
||||||
private const Int32 c_defaultMaxRetry = 3;
|
private const Int32 c_defaultMaxRetry = 3;
|
||||||
private static readonly TimeSpan s_defaultTimeout = TimeSpan.FromSeconds(100); //default WebAPI timeout
|
private static readonly TimeSpan s_defaultTimeout = TimeSpan.FromSeconds(100); //default WebAPI timeout
|
||||||
private ICollection<CultureInfo> m_acceptLanguages = new List<CultureInfo>();
|
private ICollection<CultureInfo> m_acceptLanguages = new List<CultureInfo>();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using GitHub.Services.OAuth;
|
|||||||
|
|
||||||
namespace GitHub.Services.Common
|
namespace GitHub.Services.Common
|
||||||
{
|
{
|
||||||
public class RawHttpMessageHandler: HttpMessageHandler
|
public class RawHttpMessageHandler : HttpMessageHandler
|
||||||
{
|
{
|
||||||
public RawHttpMessageHandler(
|
public RawHttpMessageHandler(
|
||||||
FederatedCredential credentials)
|
FederatedCredential credentials)
|
||||||
@@ -120,6 +120,7 @@ namespace GitHub.Services.Common
|
|||||||
Boolean succeeded = false;
|
Boolean succeeded = false;
|
||||||
HttpResponseMessageWrapper responseWrapper;
|
HttpResponseMessageWrapper responseWrapper;
|
||||||
|
|
||||||
|
Boolean lastResponseDemandedProxyAuth = false;
|
||||||
Int32 retries = m_maxAuthRetries;
|
Int32 retries = m_maxAuthRetries;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -138,7 +139,13 @@ namespace GitHub.Services.Common
|
|||||||
|
|
||||||
// Let's start with sending a token
|
// Let's start with sending a token
|
||||||
IssuedToken token = await m_tokenProvider.GetTokenAsync(null, tokenSource.Token).ConfigureAwait(false);
|
IssuedToken token = await m_tokenProvider.GetTokenAsync(null, tokenSource.Token).ConfigureAwait(false);
|
||||||
ApplyToken(request, token);
|
ApplyToken(request, token, applyICredentialsToWebProxy: lastResponseDemandedProxyAuth);
|
||||||
|
|
||||||
|
// The WinHttpHandler will chunk any content that does not have a computed length which is
|
||||||
|
// not what we want. By loading into a buffer up-front we bypass this behavior and there is
|
||||||
|
// no difference in the normal HttpClientHandler behavior here since this is what they were
|
||||||
|
// already doing.
|
||||||
|
await BufferRequestContentAsync(request, tokenSource.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
// ConfigureAwait(false) enables the continuation to be run outside any captured
|
// ConfigureAwait(false) enables the continuation to be run outside any captured
|
||||||
// SyncronizationContext (such as ASP.NET's) which keeps things from deadlocking...
|
// SyncronizationContext (such as ASP.NET's) which keeps things from deadlocking...
|
||||||
@@ -147,7 +154,8 @@ namespace GitHub.Services.Common
|
|||||||
responseWrapper = new HttpResponseMessageWrapper(response);
|
responseWrapper = new HttpResponseMessageWrapper(response);
|
||||||
|
|
||||||
var isUnAuthorized = responseWrapper.StatusCode == HttpStatusCode.Unauthorized;
|
var isUnAuthorized = responseWrapper.StatusCode == HttpStatusCode.Unauthorized;
|
||||||
if (!isUnAuthorized)
|
lastResponseDemandedProxyAuth = responseWrapper.StatusCode == HttpStatusCode.ProxyAuthenticationRequired;
|
||||||
|
if (!isUnAuthorized && !lastResponseDemandedProxyAuth)
|
||||||
{
|
{
|
||||||
// Validate the token after it has been successfully authenticated with the server.
|
// Validate the token after it has been successfully authenticated with the server.
|
||||||
m_tokenProvider?.ValidateToken(token, responseWrapper);
|
m_tokenProvider?.ValidateToken(token, responseWrapper);
|
||||||
@@ -211,15 +219,42 @@ namespace GitHub.Services.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task BufferRequestContentAsync(
|
||||||
|
HttpRequestMessage request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (request.Content != null &&
|
||||||
|
request.Headers.TransferEncodingChunked != true)
|
||||||
|
{
|
||||||
|
Int64? contentLength = request.Content.Headers.ContentLength;
|
||||||
|
if (contentLength == null)
|
||||||
|
{
|
||||||
|
await request.Content.LoadIntoBufferAsync().EnforceCancellation(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly turn off chunked encoding since we have computed the request content size
|
||||||
|
request.Headers.TransferEncodingChunked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ApplyToken(
|
private void ApplyToken(
|
||||||
HttpRequestMessage request,
|
HttpRequestMessage request,
|
||||||
IssuedToken token)
|
IssuedToken token,
|
||||||
|
bool applyICredentialsToWebProxy = false)
|
||||||
{
|
{
|
||||||
switch (token)
|
switch (token)
|
||||||
{
|
{
|
||||||
case null:
|
case null:
|
||||||
return;
|
return;
|
||||||
case ICredentials credentialsToken:
|
case ICredentials credentialsToken:
|
||||||
|
if (applyICredentialsToWebProxy)
|
||||||
|
{
|
||||||
|
HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler;
|
||||||
|
if (httpClientHandler != null && httpClientHandler.Proxy != null)
|
||||||
|
{
|
||||||
|
httpClientHandler.Proxy.Credentials = credentialsToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
m_credentialWrapper.InnerCredentials = credentialsToken;
|
m_credentialWrapper.InnerCredentials = credentialsToken;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using GitHub.DistributedTask.Logging;
|
using GitHub.DistributedTask.Logging;
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions2.Sdk
|
namespace GitHub.DistributedTask.Expressions2.Sdk
|
||||||
@@ -55,7 +55,7 @@ namespace GitHub.DistributedTask.Expressions2.Sdk
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Evaluate
|
// Evaluate
|
||||||
secretMasker = secretMasker?.Clone() ?? new SecretMasker();
|
secretMasker = secretMasker?.Clone() ?? new SecretMasker(Enumerable.Empty<ValueEncoder>());
|
||||||
trace = new EvaluationTraceWriter(trace, secretMasker);
|
trace = new EvaluationTraceWriter(trace, secretMasker);
|
||||||
var context = new EvaluationContext(trace, secretMasker, state, options, this);
|
var context = new EvaluationContext(trace, secretMasker, state, options, this);
|
||||||
trace.Info($"Evaluating: {ConvertToExpression()}");
|
trace.Info($"Evaluating: {ConvertToExpression()}");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Logging
|
namespace GitHub.DistributedTask.Logging
|
||||||
@@ -8,7 +8,6 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
{
|
{
|
||||||
void AddRegex(String pattern);
|
void AddRegex(String pattern);
|
||||||
void AddValue(String value);
|
void AddValue(String value);
|
||||||
void AddValueEncoder(ValueEncoder encoder);
|
|
||||||
ISecretMasker Clone();
|
ISecretMasker Clone();
|
||||||
String MaskSecrets(String input);
|
String MaskSecrets(String input);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -10,11 +10,11 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public sealed class SecretMasker : ISecretMasker, IDisposable
|
public sealed class SecretMasker : ISecretMasker, IDisposable
|
||||||
{
|
{
|
||||||
public SecretMasker()
|
public SecretMasker(IEnumerable<ValueEncoder> encoders)
|
||||||
{
|
{
|
||||||
m_originalValueSecrets = new HashSet<ValueSecret>();
|
m_originalValueSecrets = new HashSet<ValueSecret>();
|
||||||
m_regexSecrets = new HashSet<RegexSecret>();
|
m_regexSecrets = new HashSet<RegexSecret>();
|
||||||
m_valueEncoders = new HashSet<ValueEncoder>();
|
m_valueEncoders = new HashSet<ValueEncoder>(encoders ?? Enumerable.Empty<ValueEncoder>());
|
||||||
m_valueSecrets = new HashSet<ValueSecret>();
|
m_valueSecrets = new HashSet<ValueSecret>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,15 +104,11 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute the encoded values.
|
var secretVariations = valueEncoders.SelectMany(encoder => encoder(value))
|
||||||
foreach (ValueEncoder valueEncoder in valueEncoders)
|
.Where(variation => !string.IsNullOrEmpty(variation))
|
||||||
{
|
.Distinct()
|
||||||
String encodedValue = valueEncoder(value);
|
.Select(variation => new ValueSecret(variation));
|
||||||
if (!String.IsNullOrEmpty(encodedValue))
|
valueSecrets.AddRange(secretVariations);
|
||||||
{
|
|
||||||
valueSecrets.Add(new ValueSecret(encodedValue));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write section.
|
// Write section.
|
||||||
try
|
try
|
||||||
@@ -135,69 +131,6 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
|
|
||||||
/// </summary>
|
|
||||||
public void AddValueEncoder(ValueEncoder encoder)
|
|
||||||
{
|
|
||||||
ValueSecret[] originalSecrets;
|
|
||||||
|
|
||||||
// Read section.
|
|
||||||
try
|
|
||||||
{
|
|
||||||
m_lock.EnterReadLock();
|
|
||||||
|
|
||||||
// Test whether already added.
|
|
||||||
if (m_valueEncoders.Contains(encoder))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the original value secrets.
|
|
||||||
originalSecrets = m_originalValueSecrets.ToArray();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (m_lock.IsReadLockHeld)
|
|
||||||
{
|
|
||||||
m_lock.ExitReadLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the encoded values.
|
|
||||||
var encodedSecrets = new List<ValueSecret>();
|
|
||||||
foreach (ValueSecret originalSecret in originalSecrets)
|
|
||||||
{
|
|
||||||
String encodedValue = encoder(originalSecret.m_value);
|
|
||||||
if (!String.IsNullOrEmpty(encodedValue))
|
|
||||||
{
|
|
||||||
encodedSecrets.Add(new ValueSecret(encodedValue));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write section.
|
|
||||||
try
|
|
||||||
{
|
|
||||||
m_lock.EnterWriteLock();
|
|
||||||
|
|
||||||
// Add the encoder.
|
|
||||||
m_valueEncoders.Add(encoder);
|
|
||||||
|
|
||||||
// Add the values.
|
|
||||||
foreach (ValueSecret encodedSecret in encodedSecrets)
|
|
||||||
{
|
|
||||||
m_valueSecrets.Add(encodedSecret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (m_lock.IsWriteLockHeld)
|
|
||||||
{
|
|
||||||
m_lock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ISecretMasker Clone() => new SecretMasker(this);
|
public ISecretMasker Clone() => new SecretMasker(this);
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -1,73 +1,97 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Security;
|
using System.Security;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Logging
|
namespace GitHub.DistributedTask.Logging
|
||||||
{
|
{
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public delegate String ValueEncoder(String value);
|
public delegate IEnumerable<string> ValueEncoder(string value);
|
||||||
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public static class ValueEncoders
|
public static class ValueEncoders
|
||||||
{
|
{
|
||||||
public static String Base64StringEscape(String value)
|
|
||||||
{
|
|
||||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base64 is 6 bits -> char
|
public static IEnumerable<string> EnumerateBase64Variations(string value)
|
||||||
// A byte is 8 bits
|
|
||||||
// When end user doing somthing like base64(user:password)
|
|
||||||
// The length of the leading content will cause different base64 encoding result on the password
|
|
||||||
// So we add base64(value shifted 1 and two bytes) as secret as well.
|
|
||||||
// B1 B2 B3 B4 B5 B6 B7
|
|
||||||
// 000000|00 0000|0000 00|000000| 000000|00 0000|0000 00|000000|
|
|
||||||
// Char1 Char2 Char3 Char4
|
|
||||||
// See the above, the first byte has a character beginning at index 0, the second byte has a character beginning at index 4, the third byte has a character beginning at index 2 and then the pattern repeats
|
|
||||||
// We register byte offsets for all these possible values
|
|
||||||
public static String Base64StringEscapeShift1(String value)
|
|
||||||
{
|
{
|
||||||
return Base64StringEscapeShift(value, 1);
|
if (!string.IsNullOrEmpty(value))
|
||||||
}
|
{
|
||||||
|
// A byte is 8 bits. A Base64 "digit" can hold a maximum of 6 bits (2^64 - 1, or values 0 to 63).
|
||||||
|
// As a result, many Unicode characters (including single-byte letters) cannot be represented using a single Base64 digit.
|
||||||
|
// Furthermore, on average a Base64 string will be about 33% longer than the original text.
|
||||||
|
// This is because it generally requires 4 Base64 digits to represent 3 Unicode bytes. (4 / 3 ~ 1.33)
|
||||||
|
//
|
||||||
|
// Because of this 4:3 ratio (or, more precisely, 8 bits : 6 bits ratio), there's a cyclical pattern
|
||||||
|
// to when a byte boundary aligns with a Base64 digit boundary.
|
||||||
|
// The pattern repeats every 24 bits (the lowest common multiple of 8 and 6).
|
||||||
|
//
|
||||||
|
// |-----------24 bits-------------|-----------24 bits------------|
|
||||||
|
// Base64 Digits: |digit 0|digit 1|digit 2|digit 3|digit 4|digit 5|digit 6|digit7|
|
||||||
|
// Allocated Bits: aaaaaa aaBBBB BBBBcc cccccc DDDDDD DDeeee eeeeFF FFFFFF
|
||||||
|
// Unicode chars: |0th char |1st char |2nd char |3rd char |4th char |5th char |
|
||||||
|
|
||||||
public static String Base64StringEscapeShift2(String value)
|
// Depending on alignment, the Base64-encoded secret can take any of 3 basic forms.
|
||||||
{
|
// For example, the Base64 digits representing "abc" could appear as any of the following:
|
||||||
return Base64StringEscapeShift(value, 2);
|
// "YWJj" when aligned
|
||||||
|
// ".!FiYw==" when preceded by 3x + 1 bytes
|
||||||
|
// "..!hYmM=" when preceded by 3x + 2 bytes
|
||||||
|
// (where . represents an unrelated Base64 digit, ! represents a Base64 digit that should be masked, and x represents any non-negative integer)
|
||||||
|
|
||||||
|
var rawBytes = Encoding.UTF8.GetBytes(value);
|
||||||
|
|
||||||
|
for (var offset = 0; offset <= 2; offset++)
|
||||||
|
{
|
||||||
|
var prunedBytes = rawBytes.Skip(offset).ToArray();
|
||||||
|
if (prunedBytes.Length > 0)
|
||||||
|
{
|
||||||
|
// Don't include Base64 padding characters (=) in Base64 representations of the secret.
|
||||||
|
// They don't represent anything interesting, so they don't need to be masked.
|
||||||
|
// (Some clients omit the padding, so we want to be sure we recognize the secret regardless of whether the padding is present or not.)
|
||||||
|
var buffer = new StringBuilder(Convert.ToBase64String(prunedBytes).TrimEnd(BASE64_PADDING_SUFFIX));
|
||||||
|
yield return buffer.ToString();
|
||||||
|
|
||||||
|
// Also, yield the RFC4648-equivalent RegEx.
|
||||||
|
buffer.Replace('+', '-');
|
||||||
|
buffer.Replace('/', '_');
|
||||||
|
yield return buffer.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used when we pass environment variables to docker to escape " with \"
|
// Used when we pass environment variables to docker to escape " with \"
|
||||||
public static String CommandLineArgumentEscape(String value)
|
public static IEnumerable<string> CommandLineArgumentEscape(string value)
|
||||||
{
|
{
|
||||||
return value.Replace("\"", "\\\"");
|
yield return value.Replace("\"", "\\\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String ExpressionStringEscape(String value)
|
public static IEnumerable<string> ExpressionStringEscape(string value)
|
||||||
{
|
{
|
||||||
return Expressions2.Sdk.ExpressionUtility.StringEscape(value);
|
yield return Expressions2.Sdk.ExpressionUtility.StringEscape(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String JsonStringEscape(String value)
|
public static IEnumerable<string> JsonStringEscape(string value)
|
||||||
{
|
{
|
||||||
// Convert to a JSON string and then remove the leading/trailing double-quote.
|
// Convert to a JSON string and then remove the leading/trailing double-quote.
|
||||||
String jsonString = JsonConvert.ToString(value);
|
String jsonString = JsonConvert.ToString(value);
|
||||||
String jsonEscapedValue = jsonString.Substring(startIndex: 1, length: jsonString.Length - 2);
|
String jsonEscapedValue = jsonString.Substring(startIndex: 1, length: jsonString.Length - 2);
|
||||||
return jsonEscapedValue;
|
yield return jsonEscapedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String UriDataEscape(String value)
|
public static IEnumerable<string> UriDataEscape(string value)
|
||||||
{
|
{
|
||||||
return UriDataEscape(value, 65519);
|
yield return UriDataEscape(value, 65519);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String XmlDataEscape(String value)
|
public static IEnumerable<string> XmlDataEscape(string value)
|
||||||
{
|
{
|
||||||
return SecurityElement.Escape(value);
|
yield return SecurityElement.Escape(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String TrimDoubleQuotes(String value)
|
public static IEnumerable<string> TrimDoubleQuotes(string value)
|
||||||
{
|
{
|
||||||
var trimmed = string.Empty;
|
var trimmed = string.Empty;
|
||||||
if (!string.IsNullOrEmpty(value) &&
|
if (!string.IsNullOrEmpty(value) &&
|
||||||
@@ -78,17 +102,17 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
trimmed = value.Substring(1, value.Length - 2);
|
trimmed = value.Substring(1, value.Length - 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return trimmed;
|
yield return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String PowerShellPreAmpersandEscape(String value)
|
public static IEnumerable<string> PowerShellPreAmpersandEscape(string value)
|
||||||
{
|
{
|
||||||
// if the secret is passed to PS as a command and it causes an error, sections of it can be surrounded by color codes
|
// if the secret is passed to PS as a command and it causes an error, sections of it can be surrounded by color codes
|
||||||
// or printed individually.
|
// or printed individually.
|
||||||
|
|
||||||
// The secret secretpart1&secretpart2&secretpart3 would be split into 2 sections:
|
// The secret secretpart1&secretpart2&secretpart3 would be split into 2 sections:
|
||||||
// 'secretpart1&secretpart2&' and 'secretpart3'. This method masks for the first section.
|
// 'secretpart1&secretpart2&' and 'secretpart3'. This method masks for the first section.
|
||||||
|
|
||||||
// The secret secretpart1&+secretpart2&secretpart3 would be split into 2 sections:
|
// The secret secretpart1&+secretpart2&secretpart3 would be split into 2 sections:
|
||||||
// 'secretpart1&+' and (no 's') 'ecretpart2&secretpart3'. This method masks for the first section.
|
// 'secretpart1&+' and (no 's') 'ecretpart2&secretpart3'. This method masks for the first section.
|
||||||
|
|
||||||
@@ -112,10 +136,10 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return trimmed;
|
yield return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String PowerShellPostAmpersandEscape(String value)
|
public static IEnumerable<string> PowerShellPostAmpersandEscape(string value)
|
||||||
{
|
{
|
||||||
var trimmed = string.Empty;
|
var trimmed = string.Empty;
|
||||||
if (!string.IsNullOrEmpty(value) && value.Contains("&"))
|
if (!string.IsNullOrEmpty(value) && value.Contains("&"))
|
||||||
@@ -137,27 +161,10 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return trimmed;
|
yield return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Base64StringEscapeShift(String value, int shift)
|
private static string UriDataEscape(string value, Int32 maxSegmentSize)
|
||||||
{
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(value);
|
|
||||||
if (bytes.Length > shift)
|
|
||||||
{
|
|
||||||
var shiftArray = new byte[bytes.Length - shift];
|
|
||||||
Array.Copy(bytes, shift, shiftArray, 0, bytes.Length - shift);
|
|
||||||
return Convert.ToBase64String(shiftArray);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return Convert.ToBase64String(bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String UriDataEscape(
|
|
||||||
String value,
|
|
||||||
Int32 maxSegmentSize)
|
|
||||||
{
|
{
|
||||||
if (value.Length <= maxSegmentSize)
|
if (value.Length <= maxSegmentSize)
|
||||||
{
|
{
|
||||||
@@ -183,5 +190,7 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
|
|
||||||
return result.ToString();
|
return result.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const char BASE64_PADDING_SUFFIX = '=';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -455,7 +455,6 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
|||||||
private readonly String[] s_expressionValueNames = new[]
|
private readonly String[] s_expressionValueNames = new[]
|
||||||
{
|
{
|
||||||
PipelineTemplateConstants.GitHub,
|
PipelineTemplateConstants.GitHub,
|
||||||
PipelineTemplateConstants.Needs,
|
|
||||||
PipelineTemplateConstants.Strategy,
|
PipelineTemplateConstants.Strategy,
|
||||||
PipelineTemplateConstants.Matrix,
|
PipelineTemplateConstants.Matrix,
|
||||||
PipelineTemplateConstants.Needs,
|
PipelineTemplateConstants.Needs,
|
||||||
|
|||||||
@@ -222,6 +222,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"job-outputs": {
|
"job-outputs": {
|
||||||
|
"context": [
|
||||||
|
"matrix"
|
||||||
|
],
|
||||||
"mapping": {
|
"mapping": {
|
||||||
"loose-key-type": "non-empty-string",
|
"loose-key-type": "non-empty-string",
|
||||||
"loose-value-type": "string-runner-context"
|
"loose-value-type": "string-runner-context"
|
||||||
|
|||||||
48
src/Sdk/DTWebApi/WebApi/ListRunnersResponse.cs
Normal file
48
src/Sdk/DTWebApi/WebApi/ListRunnersResponse.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using GitHub.Services.WebApi;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace GitHub.DistributedTask.WebApi
|
||||||
|
{
|
||||||
|
|
||||||
|
public class ListRunnersResponse
|
||||||
|
{
|
||||||
|
public ListRunnersResponse()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListRunnersResponse(ListRunnersResponse responseToBeCloned)
|
||||||
|
{
|
||||||
|
this.TotalCount = responseToBeCloned.TotalCount;
|
||||||
|
this.Runners = responseToBeCloned.Runners;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty("total_count")]
|
||||||
|
public int TotalCount
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty("runners")]
|
||||||
|
public List<Runner> Runners
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListRunnersResponse Clone()
|
||||||
|
{
|
||||||
|
return new ListRunnersResponse(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TaskAgent> ToTaskAgents()
|
||||||
|
{
|
||||||
|
return Runners.Select(runner => new TaskAgent() { Name = runner.Name }).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
63
src/Sdk/DTWebApi/WebApi/Runner.cs
Normal file
63
src/Sdk/DTWebApi/WebApi/Runner.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace GitHub.DistributedTask.WebApi
|
||||||
|
{
|
||||||
|
public class Runner
|
||||||
|
{
|
||||||
|
|
||||||
|
public class Authorization
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The url to refresh tokens
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("authorization_url")]
|
||||||
|
public Uri AuthorizationUrl
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The url to connect to to poll for messages
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("server_url")]
|
||||||
|
public string ServerUrl
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client id to use when connecting to the authorization_url
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("client_id")]
|
||||||
|
public string ClientId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty("name")]
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty("id")]
|
||||||
|
public Int32 Id
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty("authorization")]
|
||||||
|
public Authorization RunnerAuthorization
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
98
src/Sdk/DTWebApi/WebApi/RunnerGroup.cs
Normal file
98
src/Sdk/DTWebApi/WebApi/RunnerGroup.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
using GitHub.Services.WebApi;
|
||||||
|
using System;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace GitHub.DistributedTask.WebApi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An organization-level grouping of runners.
|
||||||
|
/// </summary>
|
||||||
|
[DataContract]
|
||||||
|
public class RunnerGroup
|
||||||
|
{
|
||||||
|
internal RunnerGroup()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public RunnerGroup(String name)
|
||||||
|
{
|
||||||
|
this.Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RunnerGroup(RunnerGroup poolToBeCloned)
|
||||||
|
{
|
||||||
|
this.Id = poolToBeCloned.Id;
|
||||||
|
this.IsHosted = poolToBeCloned.IsHosted;
|
||||||
|
this.Name = poolToBeCloned.Name;
|
||||||
|
this.IsDefault = poolToBeCloned.IsDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
[JsonProperty("id")]
|
||||||
|
public Int32 Id
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
[JsonProperty("name")]
|
||||||
|
public String Name
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether or not this pool is internal and can't be modified by users
|
||||||
|
/// </summary>
|
||||||
|
[DataMember]
|
||||||
|
[JsonProperty("default")]
|
||||||
|
public bool IsDefault
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether or not this pool is managed by the service.
|
||||||
|
/// </summary>
|
||||||
|
[DataMember]
|
||||||
|
[JsonProperty("is_hosted")]
|
||||||
|
public Boolean IsHosted
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RunnerGroupList
|
||||||
|
{
|
||||||
|
public RunnerGroupList()
|
||||||
|
{
|
||||||
|
this.RunnerGroups = new List<RunnerGroup>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TaskAgentPool> ToAgentPoolList()
|
||||||
|
{
|
||||||
|
var agentPools = this.RunnerGroups.Select(x => new TaskAgentPool(x.Name)
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
IsHosted = x.IsHosted,
|
||||||
|
IsInternal = x.IsDefault
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return agentPools;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty("runner_groups")]
|
||||||
|
public List<RunnerGroup> RunnerGroups { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("total_count")]
|
||||||
|
public int Count { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
using GitHub.Services.WebApi;
|
using GitHub.Services.WebApi;
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
@@ -100,6 +100,7 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
public static readonly String Summary = "DistributedTask.Core.Summary";
|
public static readonly String Summary = "DistributedTask.Core.Summary";
|
||||||
public static readonly String FileAttachment = "DistributedTask.Core.FileAttachment";
|
public static readonly String FileAttachment = "DistributedTask.Core.FileAttachment";
|
||||||
public static readonly String DiagnosticLog = "DistributedTask.Core.DiagnosticLog";
|
public static readonly String DiagnosticLog = "DistributedTask.Core.DiagnosticLog";
|
||||||
|
public static readonly String ResultsLog = "Results.Core.Log";
|
||||||
}
|
}
|
||||||
|
|
||||||
[GenerateAllConstants]
|
[GenerateAllConstants]
|
||||||
|
|||||||
15
src/Sdk/RSWebApi/Contracts/RenewJobRequest.cs
Normal file
15
src/Sdk/RSWebApi/Contracts/RenewJobRequest.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace GitHub.Actions.RunService.WebApi
|
||||||
|
{
|
||||||
|
[DataContract]
|
||||||
|
public class RenewJobRequest
|
||||||
|
{
|
||||||
|
[DataMember(Name = "planId", EmitDefaultValue = false)]
|
||||||
|
public Guid PlanID { get; set; }
|
||||||
|
|
||||||
|
[DataMember(Name = "jobId", EmitDefaultValue = false)]
|
||||||
|
public Guid JobID { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/Sdk/RSWebApi/Contracts/RenewJobResponse.cs
Normal file
16
src/Sdk/RSWebApi/Contracts/RenewJobResponse.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace Sdk.RSWebApi.Contracts
|
||||||
|
{
|
||||||
|
[DataContract]
|
||||||
|
public class RenewJobResponse
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public DateTime LockedUntil
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ using GitHub.DistributedTask.WebApi;
|
|||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
using GitHub.Services.OAuth;
|
using GitHub.Services.OAuth;
|
||||||
using GitHub.Services.WebApi;
|
using GitHub.Services.WebApi;
|
||||||
|
using Sdk.RSWebApi.Contracts;
|
||||||
using Sdk.WebApi.WebApi;
|
using Sdk.WebApi.WebApi;
|
||||||
|
|
||||||
namespace GitHub.Actions.RunService.WebApi
|
namespace GitHub.Actions.RunService.WebApi
|
||||||
@@ -98,6 +99,29 @@ namespace GitHub.Actions.RunService.WebApi
|
|||||||
|
|
||||||
var requestContent = new ObjectContent<CompleteJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
|
var requestContent = new ObjectContent<CompleteJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
|
||||||
return SendAsync(
|
return SendAsync(
|
||||||
|
httpMethod,
|
||||||
|
requestUri,
|
||||||
|
content: requestContent,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<RenewJobResponse> RenewJobAsync(
|
||||||
|
Uri requestUri,
|
||||||
|
Guid planId,
|
||||||
|
Guid jobId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
HttpMethod httpMethod = new HttpMethod("POST");
|
||||||
|
var payload = new RenewJobRequest()
|
||||||
|
{
|
||||||
|
PlanID = planId,
|
||||||
|
JobID = jobId
|
||||||
|
};
|
||||||
|
|
||||||
|
requestUri = new Uri(requestUri, "renewjob");
|
||||||
|
|
||||||
|
var requestContent = new ObjectContent<RenewJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
|
||||||
|
return SendAsync<RenewJobResponse>(
|
||||||
httpMethod,
|
httpMethod,
|
||||||
requestUri,
|
requestUri,
|
||||||
content: requestContent,
|
content: requestContent,
|
||||||
|
|||||||
84
src/Sdk/WebApi/WebApi/BrokerHttpClient.cs
Normal file
84
src/Sdk/WebApi/WebApi/BrokerHttpClient.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.Pipelines;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Services.Common;
|
||||||
|
using GitHub.Services.OAuth;
|
||||||
|
using GitHub.Services.WebApi;
|
||||||
|
using Sdk.RSWebApi.Contracts;
|
||||||
|
using Sdk.WebApi.WebApi;
|
||||||
|
|
||||||
|
namespace GitHub.Actions.RunService.WebApi
|
||||||
|
{
|
||||||
|
public class BrokerHttpClient : RawHttpClientBase
|
||||||
|
{
|
||||||
|
public BrokerHttpClient(
|
||||||
|
Uri baseUrl,
|
||||||
|
VssOAuthCredential credentials)
|
||||||
|
: base(baseUrl, credentials)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BrokerHttpClient(
|
||||||
|
Uri baseUrl,
|
||||||
|
VssOAuthCredential credentials,
|
||||||
|
RawClientHttpRequestSettings settings)
|
||||||
|
: base(baseUrl, credentials, settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BrokerHttpClient(
|
||||||
|
Uri baseUrl,
|
||||||
|
VssOAuthCredential credentials,
|
||||||
|
params DelegatingHandler[] handlers)
|
||||||
|
: base(baseUrl, credentials, handlers)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BrokerHttpClient(
|
||||||
|
Uri baseUrl,
|
||||||
|
VssOAuthCredential credentials,
|
||||||
|
RawClientHttpRequestSettings settings,
|
||||||
|
params DelegatingHandler[] handlers)
|
||||||
|
: base(baseUrl, credentials, settings, handlers)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BrokerHttpClient(
|
||||||
|
Uri baseUrl,
|
||||||
|
HttpMessageHandler pipeline,
|
||||||
|
Boolean disposeHandler)
|
||||||
|
: base(baseUrl, pipeline, disposeHandler)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<TaskAgentMessage> GetRunnerMessageAsync(
|
||||||
|
string runnerVersion,
|
||||||
|
TaskAgentStatus? status,
|
||||||
|
CancellationToken cancellationToken = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var requestUri = new Uri(Client.BaseAddress, "message");
|
||||||
|
|
||||||
|
List<KeyValuePair<string, string>> queryParams = new List<KeyValuePair<string, string>>();
|
||||||
|
|
||||||
|
if (status != null)
|
||||||
|
{
|
||||||
|
queryParams.Add("status", status.Value.ToString());
|
||||||
|
}
|
||||||
|
if (runnerVersion != null)
|
||||||
|
{
|
||||||
|
queryParams.Add("runnerVersion", runnerVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SendAsync<TaskAgentMessage>(
|
||||||
|
new HttpMethod("GET"),
|
||||||
|
requestUri: requestUri,
|
||||||
|
queryParameters: queryParams,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
@@ -46,12 +47,126 @@ namespace GitHub.Services.Results.Contracts
|
|||||||
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||||
public class CreateStepSummaryMetadataResponse
|
public class GetSignedJobLogsURLRequest
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public string WorkflowJobRunBackendId;
|
||||||
|
[DataMember]
|
||||||
|
public string WorkflowRunBackendId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||||
|
public class GetSignedJobLogsURLResponse
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public string LogsUrl;
|
||||||
|
[DataMember]
|
||||||
|
public string BlobStorageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||||
|
public class GetSignedStepLogsURLRequest
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public string WorkflowJobRunBackendId;
|
||||||
|
[DataMember]
|
||||||
|
public string WorkflowRunBackendId;
|
||||||
|
[DataMember]
|
||||||
|
public string StepBackendId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||||
|
public class GetSignedStepLogsURLResponse
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public string LogsUrl;
|
||||||
|
[DataMember]
|
||||||
|
public string BlobStorageType;
|
||||||
|
[DataMember]
|
||||||
|
public long SoftSizeLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||||
|
public class JobLogsMetadataCreate
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public string WorkflowRunBackendId;
|
||||||
|
[DataMember]
|
||||||
|
public string WorkflowJobRunBackendId;
|
||||||
|
[DataMember]
|
||||||
|
public string UploadedAt;
|
||||||
|
[DataMember]
|
||||||
|
public long LineCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||||
|
public class StepLogsMetadataCreate
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public string WorkflowRunBackendId;
|
||||||
|
[DataMember]
|
||||||
|
public string WorkflowJobRunBackendId;
|
||||||
|
[DataMember]
|
||||||
|
public string StepBackendId;
|
||||||
|
[DataMember]
|
||||||
|
public string UploadedAt;
|
||||||
|
[DataMember]
|
||||||
|
public long LineCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||||
|
public class CreateMetadataResponse
|
||||||
{
|
{
|
||||||
[DataMember]
|
[DataMember]
|
||||||
public bool Ok;
|
public bool Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||||
|
public class StepsUpdateRequest
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public IEnumerable<Step> Steps;
|
||||||
|
[DataMember]
|
||||||
|
public long ChangeOrder;
|
||||||
|
[DataMember]
|
||||||
|
public string WorkflowJobRunBackendId;
|
||||||
|
[DataMember]
|
||||||
|
public string WorkflowRunBackendId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
|
||||||
|
public class Step
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public string ExternalId;
|
||||||
|
[DataMember]
|
||||||
|
public int Number;
|
||||||
|
[DataMember]
|
||||||
|
public string Name;
|
||||||
|
[DataMember]
|
||||||
|
public Status Status;
|
||||||
|
[DataMember]
|
||||||
|
public string StartedAt;
|
||||||
|
[DataMember]
|
||||||
|
public string CompletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Status
|
||||||
|
{
|
||||||
|
StatusUnknown = 0,
|
||||||
|
StatusInProgress = 3,
|
||||||
|
StatusPending = 5,
|
||||||
|
StatusCompleted = 6
|
||||||
|
}
|
||||||
|
|
||||||
public static class BlobStorageTypes
|
public static class BlobStorageTypes
|
||||||
{
|
{
|
||||||
public static readonly string AzureBlobStorage = "BLOB_STORAGE_TYPE_AZURE";
|
public static readonly string AzureBlobStorage = "BLOB_STORAGE_TYPE_AZURE";
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.Services.Results.Contracts;
|
|
||||||
using System.Net.Http.Formatting;
|
using System.Net.Http.Formatting;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Services.Common;
|
||||||
|
using GitHub.Services.Results.Contracts;
|
||||||
using Sdk.WebApi.WebApi;
|
using Sdk.WebApi.WebApi;
|
||||||
|
|
||||||
namespace GitHub.Services.Results.Client
|
namespace GitHub.Services.Results.Client
|
||||||
@@ -22,70 +27,141 @@ namespace GitHub.Services.Results.Client
|
|||||||
m_token = token;
|
m_token = token;
|
||||||
m_resultsServiceUrl = baseUrl;
|
m_resultsServiceUrl = baseUrl;
|
||||||
m_formatter = new JsonMediaTypeFormatter();
|
m_formatter = new JsonMediaTypeFormatter();
|
||||||
|
m_changeIdCounter = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetSignedStepSummaryURLResponse> GetStepSummaryUploadUrlAsync(string planId, string jobId, string stepId, CancellationToken cancellationToken)
|
// Get Sas URL calls
|
||||||
|
private async Task<T> GetResultsSignedURLResponse<R, T>(Uri uri, CancellationToken cancellationToken, R request)
|
||||||
{
|
{
|
||||||
var request = new GetSignedStepSummaryURLRequest()
|
using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, uri))
|
||||||
{
|
|
||||||
WorkflowJobRunBackendId= jobId,
|
|
||||||
WorkflowRunBackendId= planId,
|
|
||||||
StepBackendId= stepId
|
|
||||||
};
|
|
||||||
|
|
||||||
var stepSummaryUploadRequest = new Uri(m_resultsServiceUrl, "twirp/results.services.receiver.Receiver/GetStepSummarySignedBlobURL");
|
|
||||||
|
|
||||||
using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, stepSummaryUploadRequest))
|
|
||||||
{
|
{
|
||||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", m_token);
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", m_token);
|
||||||
requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
|
requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
|
||||||
|
|
||||||
using (HttpContent content = new ObjectContent<GetSignedStepSummaryURLRequest>(request, m_formatter))
|
using (HttpContent content = new ObjectContent<R>(request, m_formatter))
|
||||||
{
|
{
|
||||||
requestMessage.Content = content;
|
requestMessage.Content = content;
|
||||||
using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken))
|
using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken))
|
||||||
{
|
{
|
||||||
return await ReadJsonContentAsync<GetSignedStepSummaryURLResponse>(response, cancellationToken);
|
return await ReadJsonContentAsync<T>(response, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StepSummaryUploadCompleteAsync(string planId, string jobId, string stepId, long size, CancellationToken cancellationToken)
|
private async Task<GetSignedStepSummaryURLResponse> GetStepSummaryUploadUrlAsync(string planId, string jobId, Guid stepId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss.fffK");
|
var request = new GetSignedStepSummaryURLRequest()
|
||||||
var request = new StepSummaryMetadataCreate()
|
|
||||||
{
|
{
|
||||||
WorkflowJobRunBackendId= jobId,
|
WorkflowJobRunBackendId = jobId,
|
||||||
WorkflowRunBackendId= planId,
|
WorkflowRunBackendId = planId,
|
||||||
StepBackendId = stepId,
|
StepBackendId = stepId.ToString()
|
||||||
Size = size,
|
|
||||||
UploadedAt = timestamp
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var stepSummaryUploadCompleteRequest = new Uri(m_resultsServiceUrl, "twirp/results.services.receiver.Receiver/CreateStepSummaryMetadata");
|
var getStepSummarySignedBlobURLEndpoint = new Uri(m_resultsServiceUrl, Constants.GetStepSummarySignedBlobURL);
|
||||||
|
|
||||||
using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, stepSummaryUploadCompleteRequest))
|
return await GetResultsSignedURLResponse<GetSignedStepSummaryURLRequest, GetSignedStepSummaryURLResponse>(getStepSummarySignedBlobURLEndpoint, cancellationToken, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<GetSignedStepLogsURLResponse> GetStepLogUploadUrlAsync(string planId, string jobId, Guid stepId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var request = new GetSignedStepLogsURLRequest()
|
||||||
|
{
|
||||||
|
WorkflowJobRunBackendId = jobId,
|
||||||
|
WorkflowRunBackendId = planId,
|
||||||
|
StepBackendId = stepId.ToString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var getStepLogsSignedBlobURLEndpoint = new Uri(m_resultsServiceUrl, Constants.GetStepLogsSignedBlobURL);
|
||||||
|
|
||||||
|
return await GetResultsSignedURLResponse<GetSignedStepLogsURLRequest, GetSignedStepLogsURLResponse>(getStepLogsSignedBlobURLEndpoint, cancellationToken, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<GetSignedJobLogsURLResponse> GetJobLogUploadUrlAsync(string planId, string jobId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var request = new GetSignedJobLogsURLRequest()
|
||||||
|
{
|
||||||
|
WorkflowJobRunBackendId = jobId,
|
||||||
|
WorkflowRunBackendId = planId,
|
||||||
|
};
|
||||||
|
|
||||||
|
var getJobLogsSignedBlobURLEndpoint = new Uri(m_resultsServiceUrl, Constants.GetJobLogsSignedBlobURL);
|
||||||
|
|
||||||
|
return await GetResultsSignedURLResponse<GetSignedJobLogsURLRequest, GetSignedJobLogsURLResponse>(getJobLogsSignedBlobURLEndpoint, cancellationToken, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create metadata calls
|
||||||
|
|
||||||
|
private async Task SendRequest<R>(Uri uri, CancellationToken cancellationToken, R request, string timestamp)
|
||||||
|
{
|
||||||
|
using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, uri))
|
||||||
{
|
{
|
||||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", m_token);
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", m_token);
|
||||||
requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
|
requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
|
||||||
|
|
||||||
using (HttpContent content = new ObjectContent<StepSummaryMetadataCreate>(request, m_formatter))
|
using (HttpContent content = new ObjectContent<R>(request, m_formatter))
|
||||||
{
|
{
|
||||||
requestMessage.Content = content;
|
requestMessage.Content = content;
|
||||||
using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken))
|
using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken))
|
||||||
{
|
{
|
||||||
var jsonResponse = await ReadJsonContentAsync<CreateStepSummaryMetadataResponse>(response, cancellationToken);
|
var jsonResponse = await ReadJsonContentAsync<CreateMetadataResponse>(response, cancellationToken);
|
||||||
if (!jsonResponse.Ok)
|
if (!jsonResponse.Ok)
|
||||||
{
|
{
|
||||||
throw new Exception($"Failed to mark step summary upload as complete, status code: {response.StatusCode}, ok: {jsonResponse.Ok}, size: {size}, timestamp: {timestamp}");
|
throw new Exception($"Failed to mark {typeof(R).Name} upload as complete, status code: {response.StatusCode}, ok: {jsonResponse.Ok}, timestamp: {timestamp}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> UploadFileAsync(string url, string blobStorageType, FileStream file, CancellationToken cancellationToken)
|
private async Task StepSummaryUploadCompleteAsync(string planId, string jobId, Guid stepId, long size, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var timestamp = DateTime.UtcNow.ToString(Constants.TimestampFormat);
|
||||||
|
var request = new StepSummaryMetadataCreate()
|
||||||
|
{
|
||||||
|
WorkflowJobRunBackendId = jobId,
|
||||||
|
WorkflowRunBackendId = planId,
|
||||||
|
StepBackendId = stepId.ToString(),
|
||||||
|
Size = size,
|
||||||
|
UploadedAt = timestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
var createStepSummaryMetadataEndpoint = new Uri(m_resultsServiceUrl, Constants.CreateStepSummaryMetadata);
|
||||||
|
await SendRequest<StepSummaryMetadataCreate>(createStepSummaryMetadataEndpoint, cancellationToken, request, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StepLogUploadCompleteAsync(string planId, string jobId, Guid stepId, long lineCount, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var timestamp = DateTime.UtcNow.ToString(Constants.TimestampFormat);
|
||||||
|
var request = new StepLogsMetadataCreate()
|
||||||
|
{
|
||||||
|
WorkflowJobRunBackendId = jobId,
|
||||||
|
WorkflowRunBackendId = planId,
|
||||||
|
StepBackendId = stepId.ToString(),
|
||||||
|
UploadedAt = timestamp,
|
||||||
|
LineCount = lineCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
var createStepLogsMetadataEndpoint = new Uri(m_resultsServiceUrl, Constants.CreateStepLogsMetadata);
|
||||||
|
await SendRequest<StepLogsMetadataCreate>(createStepLogsMetadataEndpoint, cancellationToken, request, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task JobLogUploadCompleteAsync(string planId, string jobId, long lineCount, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var timestamp = DateTime.UtcNow.ToString(Constants.TimestampFormat);
|
||||||
|
var request = new JobLogsMetadataCreate()
|
||||||
|
{
|
||||||
|
WorkflowJobRunBackendId = jobId,
|
||||||
|
WorkflowRunBackendId = planId,
|
||||||
|
UploadedAt = timestamp,
|
||||||
|
LineCount = lineCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
var createJobLogsMetadataEndpoint = new Uri(m_resultsServiceUrl, Constants.CreateJobLogsMetadata);
|
||||||
|
await SendRequest<JobLogsMetadataCreate>(createJobLogsMetadataEndpoint, cancellationToken, request, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> UploadBlockFileAsync(string url, string blobStorageType, FileStream file, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Upload the file to the url
|
// Upload the file to the url
|
||||||
var request = new HttpRequestMessage(HttpMethod.Put, url)
|
var request = new HttpRequestMessage(HttpMethod.Put, url)
|
||||||
@@ -95,7 +171,7 @@ namespace GitHub.Services.Results.Client
|
|||||||
|
|
||||||
if (blobStorageType == BlobStorageTypes.AzureBlobStorage)
|
if (blobStorageType == BlobStorageTypes.AzureBlobStorage)
|
||||||
{
|
{
|
||||||
request.Content.Headers.Add("x-ms-blob-type", "BlockBlob");
|
request.Content.Headers.Add(Constants.AzureBlobTypeHeader, Constants.AzureBlockBlob);
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var response = await SendAsync(request, HttpCompletionOption.ResponseHeadersRead, userState: null, cancellationToken))
|
using (var response = await SendAsync(request, HttpCompletionOption.ResponseHeadersRead, userState: null, cancellationToken))
|
||||||
@@ -108,8 +184,55 @@ namespace GitHub.Services.Results.Client
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> CreateAppendFileAsync(string url, string blobStorageType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Put, url)
|
||||||
|
{
|
||||||
|
Content = new StringContent("")
|
||||||
|
};
|
||||||
|
if (blobStorageType == BlobStorageTypes.AzureBlobStorage)
|
||||||
|
{
|
||||||
|
request.Content.Headers.Add(Constants.AzureBlobTypeHeader, Constants.AzureAppendBlob);
|
||||||
|
request.Content.Headers.Add("Content-Length", "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var response = await SendAsync(request, HttpCompletionOption.ResponseHeadersRead, userState: null, cancellationToken))
|
||||||
|
{
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new Exception($"Failed to create append file, status code: {response.StatusCode}, reason: {response.ReasonPhrase}");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> UploadAppendFileAsync(string url, string blobStorageType, FileStream file, bool finalize, long fileSize, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var comp = finalize ? "&comp=appendblock&seal=true" : "&comp=appendblock";
|
||||||
|
// Upload the file to the url
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Put, url + comp)
|
||||||
|
{
|
||||||
|
Content = new StreamContent(file)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (blobStorageType == BlobStorageTypes.AzureBlobStorage)
|
||||||
|
{
|
||||||
|
request.Content.Headers.Add("Content-Length", fileSize.ToString());
|
||||||
|
request.Content.Headers.Add(Constants.AzureBlobSealedHeader, finalize.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var response = await SendAsync(request, HttpCompletionOption.ResponseHeadersRead, userState: null, cancellationToken))
|
||||||
|
{
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new Exception($"Failed to upload append file, status code: {response.StatusCode}, reason: {response.ReasonPhrase}, object: {response}, fileSize: {fileSize}");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle file upload for step summary
|
// Handle file upload for step summary
|
||||||
public async Task UploadStepSummaryAsync(string planId, string jobId, string stepId, string file, CancellationToken cancellationToken)
|
public async Task UploadStepSummaryAsync(string planId, string jobId, Guid stepId, string file, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Get the upload url
|
// Get the upload url
|
||||||
var uploadUrlResponse = await GetStepSummaryUploadUrlAsync(planId, jobId, stepId, cancellationToken);
|
var uploadUrlResponse = await GetStepSummaryUploadUrlAsync(planId, jobId, stepId, cancellationToken);
|
||||||
@@ -128,15 +251,147 @@ namespace GitHub.Services.Results.Client
|
|||||||
// Upload the file
|
// Upload the file
|
||||||
using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
|
using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
|
||||||
{
|
{
|
||||||
var response = await UploadFileAsync(uploadUrlResponse.SummaryUrl, uploadUrlResponse.BlobStorageType, fileStream, cancellationToken);
|
var response = await UploadBlockFileAsync(uploadUrlResponse.SummaryUrl, uploadUrlResponse.BlobStorageType, fileStream, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send step summary upload complete message
|
// Send step summary upload complete message
|
||||||
await StepSummaryUploadCompleteAsync(planId, jobId, stepId, fileSize, cancellationToken);
|
await StepSummaryUploadCompleteAsync(planId, jobId, stepId, fileSize, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle file upload for step log
|
||||||
|
public async Task UploadResultsStepLogAsync(string planId, string jobId, Guid stepId, string file, bool finalize, bool firstBlock, long lineCount, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Get the upload url
|
||||||
|
var uploadUrlResponse = await GetStepLogUploadUrlAsync(planId, jobId, stepId, cancellationToken);
|
||||||
|
if (uploadUrlResponse == null || uploadUrlResponse.LogsUrl == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to get step log upload url");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Append blob
|
||||||
|
if (firstBlock)
|
||||||
|
{
|
||||||
|
await CreateAppendFileAsync(uploadUrlResponse.LogsUrl, uploadUrlResponse.BlobStorageType, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload content
|
||||||
|
var fileSize = new FileInfo(file).Length;
|
||||||
|
using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
|
||||||
|
{
|
||||||
|
var response = await UploadAppendFileAsync(uploadUrlResponse.LogsUrl, uploadUrlResponse.BlobStorageType, fileStream, finalize, fileSize, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update metadata
|
||||||
|
if (finalize)
|
||||||
|
{
|
||||||
|
// Send step log upload complete message
|
||||||
|
await StepLogUploadCompleteAsync(planId, jobId, stepId, lineCount, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle file upload for job log
|
||||||
|
public async Task UploadResultsJobLogAsync(string planId, string jobId, string file, bool finalize, bool firstBlock, long lineCount, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Get the upload url
|
||||||
|
var uploadUrlResponse = await GetJobLogUploadUrlAsync(planId, jobId, cancellationToken);
|
||||||
|
if (uploadUrlResponse == null || uploadUrlResponse.LogsUrl == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to get job log upload url");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Append blob
|
||||||
|
if (firstBlock)
|
||||||
|
{
|
||||||
|
await CreateAppendFileAsync(uploadUrlResponse.LogsUrl, uploadUrlResponse.BlobStorageType, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload content
|
||||||
|
var fileSize = new FileInfo(file).Length;
|
||||||
|
using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
|
||||||
|
{
|
||||||
|
var response = await UploadAppendFileAsync(uploadUrlResponse.LogsUrl, uploadUrlResponse.BlobStorageType, fileStream, finalize, fileSize, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update metadata
|
||||||
|
if (finalize)
|
||||||
|
{
|
||||||
|
// Send step log upload complete message
|
||||||
|
await JobLogUploadCompleteAsync(planId, jobId, lineCount, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Step ConvertTimelineRecordToStep(TimelineRecord r)
|
||||||
|
{
|
||||||
|
return new Step()
|
||||||
|
{
|
||||||
|
ExternalId = r.Id.ToString(),
|
||||||
|
Number = r.Order.GetValueOrDefault(),
|
||||||
|
Name = r.Name,
|
||||||
|
Status = ConvertStateToStatus(r.State.GetValueOrDefault()),
|
||||||
|
StartedAt = r.StartTime?.ToString(Constants.TimestampFormat),
|
||||||
|
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Status ConvertStateToStatus(TimelineRecordState s)
|
||||||
|
{
|
||||||
|
switch (s)
|
||||||
|
{
|
||||||
|
case TimelineRecordState.Completed:
|
||||||
|
return Status.StatusCompleted;
|
||||||
|
case TimelineRecordState.Pending:
|
||||||
|
return Status.StatusPending;
|
||||||
|
case TimelineRecordState.InProgress:
|
||||||
|
return Status.StatusInProgress;
|
||||||
|
default:
|
||||||
|
return Status.StatusUnknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateWorkflowStepsAsync(Guid planId, IEnumerable<TimelineRecord> records, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var timestamp = DateTime.UtcNow.ToString(Constants.TimestampFormat);
|
||||||
|
var stepRecords = records.Where(r => String.Equals(r.RecordType, "Task", StringComparison.Ordinal));
|
||||||
|
var stepUpdateRequests = stepRecords.GroupBy(r => r.ParentId).Select(sg => new StepsUpdateRequest()
|
||||||
|
{
|
||||||
|
WorkflowRunBackendId = planId.ToString(),
|
||||||
|
WorkflowJobRunBackendId = sg.Key.ToString(),
|
||||||
|
ChangeOrder = m_changeIdCounter++,
|
||||||
|
Steps = sg.Select(ConvertTimelineRecordToStep)
|
||||||
|
});
|
||||||
|
|
||||||
|
var stepUpdateEndpoint = new Uri(m_resultsServiceUrl, Constants.WorkflowStepsUpdate);
|
||||||
|
foreach (var request in stepUpdateRequests)
|
||||||
|
{
|
||||||
|
await SendRequest<StepsUpdateRequest>(stepUpdateEndpoint, cancellationToken, request, timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private MediaTypeFormatter m_formatter;
|
private MediaTypeFormatter m_formatter;
|
||||||
private Uri m_resultsServiceUrl;
|
private Uri m_resultsServiceUrl;
|
||||||
private string m_token;
|
private string m_token;
|
||||||
|
private int m_changeIdCounter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Constants specific to results
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public static readonly string TimestampFormat = "yyyy-MM-dd'T'HH:mm:ss.fffK";
|
||||||
|
|
||||||
|
public static readonly string ResultsReceiverTwirpEndpoint = "twirp/results.services.receiver.Receiver/";
|
||||||
|
public static readonly string GetStepSummarySignedBlobURL = ResultsReceiverTwirpEndpoint + "GetStepSummarySignedBlobURL";
|
||||||
|
public static readonly string CreateStepSummaryMetadata = ResultsReceiverTwirpEndpoint + "CreateStepSummaryMetadata";
|
||||||
|
public static readonly string GetStepLogsSignedBlobURL = ResultsReceiverTwirpEndpoint + "GetStepLogsSignedBlobURL";
|
||||||
|
public static readonly string CreateStepLogsMetadata = ResultsReceiverTwirpEndpoint + "CreateStepLogsMetadata";
|
||||||
|
public static readonly string GetJobLogsSignedBlobURL = ResultsReceiverTwirpEndpoint + "GetJobLogsSignedBlobURL";
|
||||||
|
public static readonly string CreateJobLogsMetadata = ResultsReceiverTwirpEndpoint + "CreateJobLogsMetadata";
|
||||||
|
public static readonly string ResultsProtoApiV1Endpoint = "twirp/github.actions.results.api.v1.WorkflowStepUpdateService/";
|
||||||
|
public static readonly string WorkflowStepsUpdate = ResultsProtoApiV1Endpoint + "WorkflowStepsUpdate";
|
||||||
|
|
||||||
|
public static readonly string AzureBlobSealedHeader = "x-ms-blob-sealed";
|
||||||
|
public static readonly string AzureBlobTypeHeader = "x-ms-blob-type";
|
||||||
|
public static readonly string AzureBlockBlob = "BlockBlob";
|
||||||
|
public static readonly string AzureAppendBlob = "AppendBlob";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@@ -13,6 +12,7 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
{
|
{
|
||||||
private HostContext _hc;
|
private HostContext _hc;
|
||||||
private CancellationTokenSource _tokenSource;
|
private CancellationTokenSource _tokenSource;
|
||||||
|
private const string EXPECTED_SECRET_MASK = "***";
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
@@ -95,11 +95,11 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Pass%20word%20123%21123"));
|
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Pass%20word%20123%21123"));
|
||||||
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Pass<word>123!123"));
|
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Pass<word>123!123"));
|
||||||
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Pass''word''123!123"));
|
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Pass''word''123!123"));
|
||||||
Assert.Equal("OlBh***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($":Password123!"))));
|
Assert.Equal("OlBh***==", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($":Password123!"))));
|
||||||
Assert.Equal("YTpQ***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"a:Password123!"))));
|
Assert.Equal("YTpQ***=", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"a:Password123!"))));
|
||||||
Assert.Equal("YWI6***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"ab:Password123!"))));
|
Assert.Equal("YWI6***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"ab:Password123!"))));
|
||||||
Assert.Equal("YWJjOlBh***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abc:Password123!"))));
|
Assert.Equal("YWJjOlBh***==", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abc:Password123!"))));
|
||||||
Assert.Equal("YWJjZDpQ***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abcd:Password123!"))));
|
Assert.Equal("YWJjZDpQ***=", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abcd:Password123!"))));
|
||||||
Assert.Equal("YWJjZGU6***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abcde:Password123!"))));
|
Assert.Equal("YWJjZGU6***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abcde:Password123!"))));
|
||||||
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Password123!!123"));
|
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Password123!!123"));
|
||||||
Assert.Equal("123short123", _hc.SecretMasker.MaskSecrets("123short123"));
|
Assert.Equal("123short123", _hc.SecretMasker.MaskSecrets("123short123"));
|
||||||
@@ -112,6 +112,116 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void Base64SecretMaskers()
|
||||||
|
{
|
||||||
|
|
||||||
|
// The following are good candidate strings for Base64 encoding because they include
|
||||||
|
// both standard and RFC 4648 Base64 digits in all offset variations.
|
||||||
|
// TeLL? noboDy~ SEcreT?
|
||||||
|
// tElL~ NEVER~ neveR?
|
||||||
|
// TIGht? Tight~ guard~
|
||||||
|
// pRIVAte~ guARd? TIghT~
|
||||||
|
// KeeP~ TIgHT? tIgHT~
|
||||||
|
// LoCk? TiGhT~ TIght~
|
||||||
|
// DIvULGe~ nObODY~ noBOdy?
|
||||||
|
// foreVER~ Tight~ GUaRd?
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Arrange.
|
||||||
|
Setup();
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
_hc.SecretMasker.AddValue("TeLL? noboDy~ SEcreT?");
|
||||||
|
|
||||||
|
// The above string has the following Base64 variations based on the chop leading byte(s) method of Base64 aliasing:
|
||||||
|
var base64Variations = new[]
|
||||||
|
{
|
||||||
|
"VGVMTD8gbm9ib0R5fiBTRWNyZVQ/",
|
||||||
|
"ZUxMPyBub2JvRHl+IFNFY3JlVD8",
|
||||||
|
"TEw/IG5vYm9EeX4gU0VjcmVUPw",
|
||||||
|
|
||||||
|
// RFC 4648 (URL-safe Base64)
|
||||||
|
"VGVMTD8gbm9ib0R5fiBTRWNyZVQ_",
|
||||||
|
"ZUxMPyBub2JvRHl-IFNFY3JlVD8",
|
||||||
|
"TEw_IG5vYm9EeX4gU0VjcmVUPw"
|
||||||
|
};
|
||||||
|
|
||||||
|
var bookends = new[]
|
||||||
|
{
|
||||||
|
(string.Empty, string.Empty),
|
||||||
|
(string.Empty, "="),
|
||||||
|
(string.Empty, "=="),
|
||||||
|
(string.Empty, "==="),
|
||||||
|
("a", "z"),
|
||||||
|
("A", "Z"),
|
||||||
|
("abc", "abc"),
|
||||||
|
("ABC", "ABC"),
|
||||||
|
("0", "0"),
|
||||||
|
("00", "00"),
|
||||||
|
("000", "000"),
|
||||||
|
("123", "789"),
|
||||||
|
("`", "`"),
|
||||||
|
("'", "'"),
|
||||||
|
("\"", "\""),
|
||||||
|
("[", "]"),
|
||||||
|
("(", ")"),
|
||||||
|
("$(", ")"),
|
||||||
|
("{", "}"),
|
||||||
|
("${", "}"),
|
||||||
|
("!", "!"),
|
||||||
|
("!!", "!!"),
|
||||||
|
("%", "%"),
|
||||||
|
("%%", "%%"),
|
||||||
|
("_", "_"),
|
||||||
|
("__", "__"),
|
||||||
|
(":", ":"),
|
||||||
|
("::", "::"),
|
||||||
|
(";", ";"),
|
||||||
|
(";;", ";;"),
|
||||||
|
(":", string.Empty),
|
||||||
|
(";", string.Empty),
|
||||||
|
(string.Empty, ":"),
|
||||||
|
(string.Empty, ";"),
|
||||||
|
("VGVMTD8gbm9ib", "ZUxMPy"),
|
||||||
|
("VGVMTD8gbm9ib", "TEw/IG5vYm9EeX4"),
|
||||||
|
("ZUxMPy", "TEw/IG5vYm9EeX4"),
|
||||||
|
("VGVMTD8gbm9ib", string.Empty),
|
||||||
|
("TEw/IG5vYm9EeX4", string.Empty),
|
||||||
|
("ZUxMPy", string.Empty),
|
||||||
|
(string.Empty, "VGVMTD8gbm9ib"),
|
||||||
|
(string.Empty, "TEw/IG5vYm9EeX4"),
|
||||||
|
(string.Empty, "ZUxMPy"),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var variation in base64Variations)
|
||||||
|
{
|
||||||
|
foreach (var pair in bookends)
|
||||||
|
{
|
||||||
|
var (prefix, suffix) = pair;
|
||||||
|
var expected = string.Format("{0}{1}{2}", prefix, EXPECTED_SECRET_MASK, suffix);
|
||||||
|
var payload = string.Format("{0}{1}{2}", prefix, variation, suffix);
|
||||||
|
Assert.Equal(expected, _hc.SecretMasker.MaskSecrets(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no masking is performed on a partial match.
|
||||||
|
for (int i = 1; i < variation.Length - 1; i++)
|
||||||
|
{
|
||||||
|
var fragment = variation[..i];
|
||||||
|
Assert.Equal(fragment, _hc.SecretMasker.MaskSecrets(fragment));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Cleanup.
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("secret&secret&secret", "secret&secret&\x0033[96msecret\x0033[0m", "***\x0033[96m***\x0033[0m")]
|
[InlineData("secret&secret&secret", "secret&secret&\x0033[96msecret\x0033[0m", "***\x0033[96m***\x0033[0m")]
|
||||||
[InlineData("secret&secret+secret", "secret&\x0033[96msecret+secret\x0033[0m", "***\x0033[96m***\x0033[0m")]
|
[InlineData("secret&secret+secret", "secret&\x0033[96msecret+secret\x0033[0m", "***\x0033[96m***\x0033[0m")]
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
|||||||
public class ConfigurationManagerL0
|
public class ConfigurationManagerL0
|
||||||
{
|
{
|
||||||
private Mock<IRunnerServer> _runnerServer;
|
private Mock<IRunnerServer> _runnerServer;
|
||||||
|
private Mock<IRunnerDotcomServer> _dotcomServer;
|
||||||
private Mock<ILocationServer> _locationServer;
|
private Mock<ILocationServer> _locationServer;
|
||||||
private Mock<ICredentialManager> _credMgr;
|
private Mock<ICredentialManager> _credMgr;
|
||||||
private Mock<IPromptManager> _promptManager;
|
private Mock<IPromptManager> _promptManager;
|
||||||
@@ -55,6 +56,7 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
|||||||
_store = new Mock<IConfigurationStore>();
|
_store = new Mock<IConfigurationStore>();
|
||||||
_extnMgr = new Mock<IExtensionManager>();
|
_extnMgr = new Mock<IExtensionManager>();
|
||||||
_rsaKeyManager = new Mock<IRSAKeyManager>();
|
_rsaKeyManager = new Mock<IRSAKeyManager>();
|
||||||
|
_dotcomServer = new Mock<IRunnerDotcomServer>();
|
||||||
|
|
||||||
#if OS_WINDOWS
|
#if OS_WINDOWS
|
||||||
_serviceControlManager = new Mock<IWindowsServiceControlManager>();
|
_serviceControlManager = new Mock<IWindowsServiceControlManager>();
|
||||||
@@ -71,6 +73,13 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
|||||||
AuthorizationUrl = new Uri("http://localhost:8080/pipelines"),
|
AuthorizationUrl = new Uri("http://localhost:8080/pipelines"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var expectedRunner = new GitHub.DistributedTask.WebApi.Runner() { Name = expectedAgent.Name, Id = 1 };
|
||||||
|
expectedRunner.RunnerAuthorization = new GitHub.DistributedTask.WebApi.Runner.Authorization
|
||||||
|
{
|
||||||
|
ClientId = expectedAgent.Authorization.ClientId.ToString(),
|
||||||
|
AuthorizationUrl = new Uri("http://localhost:8080/pipelines"),
|
||||||
|
};
|
||||||
|
|
||||||
var connectionData = new ConnectionData()
|
var connectionData = new ConnectionData()
|
||||||
{
|
{
|
||||||
InstanceId = Guid.NewGuid(),
|
InstanceId = Guid.NewGuid(),
|
||||||
@@ -106,6 +115,10 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
|||||||
_runnerServer.Setup(x => x.AddAgentAsync(It.IsAny<int>(), It.IsAny<TaskAgent>())).Returns(Task.FromResult(expectedAgent));
|
_runnerServer.Setup(x => x.AddAgentAsync(It.IsAny<int>(), It.IsAny<TaskAgent>())).Returns(Task.FromResult(expectedAgent));
|
||||||
_runnerServer.Setup(x => x.ReplaceAgentAsync(It.IsAny<int>(), It.IsAny<TaskAgent>())).Returns(Task.FromResult(expectedAgent));
|
_runnerServer.Setup(x => x.ReplaceAgentAsync(It.IsAny<int>(), It.IsAny<TaskAgent>())).Returns(Task.FromResult(expectedAgent));
|
||||||
|
|
||||||
|
_dotcomServer.Setup(x => x.GetRunnersAsync(It.IsAny<int>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(expectedAgents));
|
||||||
|
_dotcomServer.Setup(x => x.GetRunnerGroupsAsync(It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(expectedPools));
|
||||||
|
_dotcomServer.Setup(x => x.AddRunnerAsync(It.IsAny<int>(), It.IsAny<TaskAgent>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(expectedRunner));
|
||||||
|
|
||||||
rsa = new RSACryptoServiceProvider(2048);
|
rsa = new RSACryptoServiceProvider(2048);
|
||||||
|
|
||||||
_rsaKeyManager.Setup(x => x.CreateKey()).Returns(rsa);
|
_rsaKeyManager.Setup(x => x.CreateKey()).Returns(rsa);
|
||||||
@@ -119,6 +132,7 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
|||||||
tc.SetSingleton<IConfigurationStore>(_store.Object);
|
tc.SetSingleton<IConfigurationStore>(_store.Object);
|
||||||
tc.SetSingleton<IExtensionManager>(_extnMgr.Object);
|
tc.SetSingleton<IExtensionManager>(_extnMgr.Object);
|
||||||
tc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
tc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
tc.SetSingleton<IRunnerDotcomServer>(_dotcomServer.Object);
|
||||||
tc.SetSingleton<ILocationServer>(_locationServer.Object);
|
tc.SetSingleton<ILocationServer>(_locationServer.Object);
|
||||||
|
|
||||||
#if OS_WINDOWS
|
#if OS_WINDOWS
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||||
|
using GitHub.DistributedTask.Pipelines;
|
||||||
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Listener;
|
using GitHub.Runner.Listener;
|
||||||
|
using GitHub.Runner.Listener.Configuration;
|
||||||
using GitHub.Services.WebApi;
|
using GitHub.Services.WebApi;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using Sdk.RSWebApi.Contracts;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
@@ -18,6 +23,8 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
private Mock<IProcessChannel> _processChannel;
|
private Mock<IProcessChannel> _processChannel;
|
||||||
private Mock<IProcessInvoker> _processInvoker;
|
private Mock<IProcessInvoker> _processInvoker;
|
||||||
private Mock<IRunnerServer> _runnerServer;
|
private Mock<IRunnerServer> _runnerServer;
|
||||||
|
|
||||||
|
private Mock<IRunServer> _runServer;
|
||||||
private Mock<IConfigurationStore> _configurationStore;
|
private Mock<IConfigurationStore> _configurationStore;
|
||||||
|
|
||||||
public JobDispatcherL0()
|
public JobDispatcherL0()
|
||||||
@@ -25,6 +32,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
_processChannel = new Mock<IProcessChannel>();
|
_processChannel = new Mock<IProcessChannel>();
|
||||||
_processInvoker = new Mock<IProcessInvoker>();
|
_processInvoker = new Mock<IProcessInvoker>();
|
||||||
_runnerServer = new Mock<IRunnerServer>();
|
_runnerServer = new Mock<IRunnerServer>();
|
||||||
|
_runServer = new Mock<IRunServer>();
|
||||||
_configurationStore = new Mock<IConfigurationStore>();
|
_configurationStore = new Mock<IConfigurationStore>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +147,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
var jobDispatcher = new JobDispatcher();
|
var jobDispatcher = new JobDispatcher();
|
||||||
jobDispatcher.Initialize(hc);
|
jobDispatcher.Initialize(hc);
|
||||||
|
|
||||||
await jobDispatcher.RenewJobRequestAsync(poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
await jobDispatcher.RenewJobRequestAsync(It.IsAny<AgentJobRequestMessage>(), It.IsAny<ServiceEndpoint>(), poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
||||||
|
|
||||||
Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully);
|
Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully);
|
||||||
_runnerServer.Verify(x => x.RenewAgentRequestAsync(It.IsAny<int>(), It.IsAny<long>(), It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(5));
|
_runnerServer.Verify(x => x.RenewAgentRequestAsync(It.IsAny<int>(), It.IsAny<long>(), It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(5));
|
||||||
@@ -197,7 +205,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
var jobDispatcher = new JobDispatcher();
|
var jobDispatcher = new JobDispatcher();
|
||||||
jobDispatcher.Initialize(hc);
|
jobDispatcher.Initialize(hc);
|
||||||
|
|
||||||
await jobDispatcher.RenewJobRequestAsync(poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
await jobDispatcher.RenewJobRequestAsync(It.IsAny<AgentJobRequestMessage>(), It.IsAny<ServiceEndpoint>(), poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
||||||
|
|
||||||
Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should succeed.");
|
Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should succeed.");
|
||||||
Assert.False(cancellationTokenSource.IsCancellationRequested);
|
Assert.False(cancellationTokenSource.IsCancellationRequested);
|
||||||
@@ -205,6 +213,75 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async void DispatcherRenewJobOnRunServiceStopOnJobNotFoundExceptions()
|
||||||
|
{
|
||||||
|
//Arrange
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
int poolId = 1;
|
||||||
|
Int64 requestId = 1000;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
var trace = hc.GetTrace(nameof(DispatcherRenewJobOnRunServiceStopOnJobNotFoundExceptions));
|
||||||
|
TaskCompletionSource<int> firstJobRequestRenewed = new();
|
||||||
|
CancellationTokenSource cancellationTokenSource = new();
|
||||||
|
|
||||||
|
TaskAgentJobRequest request = new();
|
||||||
|
PropertyInfo lockUntilProperty = request.GetType().GetProperty("LockedUntil", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||||
|
Assert.NotNull(lockUntilProperty);
|
||||||
|
lockUntilProperty.SetValue(request, DateTime.UtcNow.AddMinutes(5));
|
||||||
|
|
||||||
|
hc.SetSingleton<IRunServer>(_runServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configurationStore.Object);
|
||||||
|
_configurationStore.Setup(x => x.GetSettings()).Returns(new RunnerSettings() { PoolId = 1 });
|
||||||
|
_ = _runServer.Setup(x => x.RenewJobAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(() =>
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
if (!firstJobRequestRenewed.Task.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
trace.Info("First renew happens.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count < 5)
|
||||||
|
{
|
||||||
|
var response = new RenewJobResponse()
|
||||||
|
{
|
||||||
|
LockedUntil = request.LockedUntil.Value
|
||||||
|
};
|
||||||
|
return Task.FromResult<RenewJobResponse>(response);
|
||||||
|
}
|
||||||
|
else if (count == 5)
|
||||||
|
{
|
||||||
|
cancellationTokenSource.CancelAfter(10000);
|
||||||
|
throw new TaskOrchestrationJobNotFoundException("");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Should not reach here.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var jobDispatcher = new JobDispatcher();
|
||||||
|
jobDispatcher.Initialize(hc);
|
||||||
|
EnableRunServiceJobForJobDispatcher(jobDispatcher);
|
||||||
|
|
||||||
|
// Set the value of the _isRunServiceJob field to true
|
||||||
|
var isRunServiceJobField = typeof(JobDispatcher).GetField("_isRunServiceJob", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
isRunServiceJobField.SetValue(jobDispatcher, true);
|
||||||
|
|
||||||
|
await jobDispatcher.RenewJobRequestAsync(GetAgentJobRequestMessage(), GetServiceEndpoint(), poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
||||||
|
|
||||||
|
Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should succeed.");
|
||||||
|
Assert.False(cancellationTokenSource.IsCancellationRequested);
|
||||||
|
_runServer.Verify(x => x.RenewJobAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<CancellationToken>()), Times.Exactly(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
@@ -256,7 +333,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
var jobDispatcher = new JobDispatcher();
|
var jobDispatcher = new JobDispatcher();
|
||||||
jobDispatcher.Initialize(hc);
|
jobDispatcher.Initialize(hc);
|
||||||
|
|
||||||
await jobDispatcher.RenewJobRequestAsync(poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
await jobDispatcher.RenewJobRequestAsync(It.IsAny<AgentJobRequestMessage>(), It.IsAny<ServiceEndpoint>(), poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
||||||
|
|
||||||
Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should succeed.");
|
Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should succeed.");
|
||||||
Assert.False(cancellationTokenSource.IsCancellationRequested);
|
Assert.False(cancellationTokenSource.IsCancellationRequested);
|
||||||
@@ -312,8 +389,9 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
var jobDispatcher = new JobDispatcher();
|
var jobDispatcher = new JobDispatcher();
|
||||||
jobDispatcher.Initialize(hc);
|
jobDispatcher.Initialize(hc);
|
||||||
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await jobDispatcher.RenewJobRequestAsync(0, 0, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
await jobDispatcher.RenewJobRequestAsync(It.IsAny<AgentJobRequestMessage>(), It.IsAny<ServiceEndpoint>(), 0, 0, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
_configurationStore.Verify(x => x.SaveSettings(It.Is<RunnerSettings>(settings => settings.AgentName == newName)), Times.Once);
|
_configurationStore.Verify(x => x.SaveSettings(It.Is<RunnerSettings>(settings => settings.AgentName == newName)), Times.Once);
|
||||||
@@ -368,7 +446,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
jobDispatcher.Initialize(hc);
|
jobDispatcher.Initialize(hc);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await jobDispatcher.RenewJobRequestAsync(0, 0, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
await jobDispatcher.RenewJobRequestAsync(It.IsAny<AgentJobRequestMessage>(), It.IsAny<ServiceEndpoint>(), 0, 0, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
_configurationStore.Verify(x => x.SaveSettings(It.IsAny<RunnerSettings>()), Times.Never);
|
_configurationStore.Verify(x => x.SaveSettings(It.IsAny<RunnerSettings>()), Times.Never);
|
||||||
@@ -421,7 +499,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
jobDispatcher.Initialize(hc);
|
jobDispatcher.Initialize(hc);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await jobDispatcher.RenewJobRequestAsync(0, 0, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
await jobDispatcher.RenewJobRequestAsync(It.IsAny<AgentJobRequestMessage>(), It.IsAny<ServiceEndpoint>(), 0, 0, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
_configurationStore.Verify(x => x.SaveSettings(It.IsAny<RunnerSettings>()), Times.Never);
|
_configurationStore.Verify(x => x.SaveSettings(It.IsAny<RunnerSettings>()), Times.Never);
|
||||||
@@ -479,7 +557,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
var jobDispatcher = new JobDispatcher();
|
var jobDispatcher = new JobDispatcher();
|
||||||
jobDispatcher.Initialize(hc);
|
jobDispatcher.Initialize(hc);
|
||||||
|
|
||||||
await jobDispatcher.RenewJobRequestAsync(poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
await jobDispatcher.RenewJobRequestAsync(It.IsAny<AgentJobRequestMessage>(), It.IsAny<ServiceEndpoint>(), poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
||||||
|
|
||||||
Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should succeed.");
|
Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should succeed.");
|
||||||
Assert.True(cancellationTokenSource.IsCancellationRequested);
|
Assert.True(cancellationTokenSource.IsCancellationRequested);
|
||||||
@@ -536,7 +614,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
var jobDispatcher = new JobDispatcher();
|
var jobDispatcher = new JobDispatcher();
|
||||||
jobDispatcher.Initialize(hc);
|
jobDispatcher.Initialize(hc);
|
||||||
|
|
||||||
await jobDispatcher.RenewJobRequestAsync(poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
await jobDispatcher.RenewJobRequestAsync(It.IsAny<AgentJobRequestMessage>(), It.IsAny<ServiceEndpoint>(), poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
||||||
|
|
||||||
Assert.False(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should failed.");
|
Assert.False(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should failed.");
|
||||||
Assert.False(cancellationTokenSource.IsCancellationRequested);
|
Assert.False(cancellationTokenSource.IsCancellationRequested);
|
||||||
@@ -600,7 +678,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
var jobDispatcher = new JobDispatcher();
|
var jobDispatcher = new JobDispatcher();
|
||||||
jobDispatcher.Initialize(hc);
|
jobDispatcher.Initialize(hc);
|
||||||
|
|
||||||
await jobDispatcher.RenewJobRequestAsync(poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
await jobDispatcher.RenewJobRequestAsync(It.IsAny<AgentJobRequestMessage>(), It.IsAny<ServiceEndpoint>(), poolId, requestId, Guid.Empty, Guid.NewGuid().ToString(), firstJobRequestRenewed, cancellationTokenSource.Token);
|
||||||
|
|
||||||
Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should succeed.");
|
Assert.True(firstJobRequestRenewed.Task.IsCompletedSuccessfully, "First renew should succeed.");
|
||||||
Assert.False(cancellationTokenSource.IsCancellationRequested);
|
Assert.False(cancellationTokenSource.IsCancellationRequested);
|
||||||
@@ -659,5 +737,78 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
Assert.True(jobDispatcher.RunOnceJobCompleted.Task.Result, "JobDispatcher should set task complete token to 'TRUE' for one time agent.");
|
Assert.True(jobDispatcher.RunOnceJobCompleted.Task.Result, "JobDispatcher should set task complete token to 'TRUE' for one time agent.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void EnableRunServiceJobForJobDispatcher(JobDispatcher jobDispatcher)
|
||||||
|
{
|
||||||
|
// Set the value of the _isRunServiceJob field to true
|
||||||
|
var isRunServiceJobField = typeof(JobDispatcher).GetField("_isRunServiceJob", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
isRunServiceJobField.SetValue(jobDispatcher, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServiceEndpoint GetServiceEndpoint()
|
||||||
|
{
|
||||||
|
var serviceEndpoint = new ServiceEndpoint
|
||||||
|
{
|
||||||
|
Authorization = new EndpointAuthorization
|
||||||
|
{
|
||||||
|
Scheme = EndpointAuthorizationSchemes.OAuth
|
||||||
|
}
|
||||||
|
};
|
||||||
|
serviceEndpoint.Authorization.Parameters.Add("AccessToken", "token");
|
||||||
|
return serviceEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AgentJobRequestMessage GetAgentJobRequestMessage()
|
||||||
|
{
|
||||||
|
var message = new AgentJobRequestMessage(
|
||||||
|
new TaskOrchestrationPlanReference()
|
||||||
|
{
|
||||||
|
PlanType = "Build",
|
||||||
|
PlanId = Guid.NewGuid(),
|
||||||
|
Version = 1
|
||||||
|
},
|
||||||
|
new TimelineReference()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid()
|
||||||
|
},
|
||||||
|
Guid.NewGuid(),
|
||||||
|
"jobDisplayName",
|
||||||
|
"jobName",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new List<TemplateToken>(),
|
||||||
|
new Dictionary<string, VariableValue>()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"variables",
|
||||||
|
new VariableValue()
|
||||||
|
{
|
||||||
|
IsSecret = false,
|
||||||
|
Value = "variables"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new List<MaskHint>()
|
||||||
|
{
|
||||||
|
new MaskHint()
|
||||||
|
{
|
||||||
|
Type = MaskType.Variable,
|
||||||
|
Value = "maskHints"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new JobResources(),
|
||||||
|
new DictionaryContextData(),
|
||||||
|
new WorkspaceOptions(),
|
||||||
|
new List<JobStep>(),
|
||||||
|
new List<string>()
|
||||||
|
{
|
||||||
|
"fileTable"
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
new List<TemplateToken>(),
|
||||||
|
new ActionsEnvironmentReference("env")
|
||||||
|
);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
Assert.False(proxy.IsBypassed(new Uri("https://actions.com")));
|
Assert.False(proxy.IsBypassed(new Uri("https://actions.com")));
|
||||||
Assert.False(proxy.IsBypassed(new Uri("https://ggithub.com")));
|
Assert.False(proxy.IsBypassed(new Uri("https://ggithub.com")));
|
||||||
Assert.False(proxy.IsBypassed(new Uri("https://github.comm")));
|
Assert.False(proxy.IsBypassed(new Uri("https://github.comm")));
|
||||||
Assert.False(proxy.IsBypassed(new Uri("https://google.com")));
|
Assert.False(proxy.IsBypassed(new Uri("https://google.com"))); // no_proxy has '.google.com', specifying only subdomains bypass
|
||||||
Assert.False(proxy.IsBypassed(new Uri("https://example.com")));
|
Assert.False(proxy.IsBypassed(new Uri("https://example.com")));
|
||||||
Assert.False(proxy.IsBypassed(new Uri("http://example.com:333")));
|
Assert.False(proxy.IsBypassed(new Uri("http://example.com:333")));
|
||||||
Assert.False(proxy.IsBypassed(new Uri("http://192.168.0.123:123")));
|
Assert.False(proxy.IsBypassed(new Uri("http://192.168.0.123:123")));
|
||||||
@@ -374,6 +374,76 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
CleanProxyEnv();
|
CleanProxyEnv();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void BypassAllOnWildcardNoProxy()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("http_proxy", "http://user1:pass1%40@127.0.0.1:8888");
|
||||||
|
Environment.SetEnvironmentVariable("https_proxy", "http://user2:pass2%40@127.0.0.1:9999");
|
||||||
|
Environment.SetEnvironmentVariable("no_proxy", "example.com, * , example2.com");
|
||||||
|
var proxy = new RunnerWebProxy();
|
||||||
|
|
||||||
|
Assert.True(proxy.IsBypassed(new Uri("http://actions.com")));
|
||||||
|
Assert.True(proxy.IsBypassed(new Uri("http://localhost")));
|
||||||
|
Assert.True(proxy.IsBypassed(new Uri("http://127.0.0.1:8080")));
|
||||||
|
Assert.True(proxy.IsBypassed(new Uri("https://actions.com")));
|
||||||
|
Assert.True(proxy.IsBypassed(new Uri("https://localhost")));
|
||||||
|
Assert.True(proxy.IsBypassed(new Uri("https://127.0.0.1:8080")));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CleanProxyEnv();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void IgnoreWildcardInNoProxySubdomain()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("http_proxy", "http://user1:pass1%40@127.0.0.1:8888");
|
||||||
|
Environment.SetEnvironmentVariable("https_proxy", "http://user2:pass2%40@127.0.0.1:9999");
|
||||||
|
Environment.SetEnvironmentVariable("no_proxy", "*.example.com");
|
||||||
|
var proxy = new RunnerWebProxy();
|
||||||
|
|
||||||
|
Assert.False(proxy.IsBypassed(new Uri("http://sub.example.com")));
|
||||||
|
Assert.False(proxy.IsBypassed(new Uri("http://example.com")));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CleanProxyEnv();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void WildcardNoProxyWorksWhenOtherNoProxyAreAround()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("http_proxy", "http://user1:pass1%40@127.0.0.1:8888");
|
||||||
|
Environment.SetEnvironmentVariable("https_proxy", "http://user2:pass2%40@127.0.0.1:9999");
|
||||||
|
Environment.SetEnvironmentVariable("no_proxy", "example.com,*,example2.com");
|
||||||
|
var proxy = new RunnerWebProxy();
|
||||||
|
|
||||||
|
Assert.True(proxy.IsBypassed(new Uri("http://actions.com")));
|
||||||
|
Assert.True(proxy.IsBypassed(new Uri("http://localhost")));
|
||||||
|
Assert.True(proxy.IsBypassed(new Uri("http://127.0.0.1:8080")));
|
||||||
|
Assert.True(proxy.IsBypassed(new Uri("https://actions.com")));
|
||||||
|
Assert.True(proxy.IsBypassed(new Uri("https://localhost")));
|
||||||
|
Assert.True(proxy.IsBypassed(new Uri("https://127.0.0.1:8080")));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CleanProxyEnv();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -56,9 +56,12 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
var traceListener = new HostTraceListener(TraceFileName);
|
var traceListener = new HostTraceListener(TraceFileName);
|
||||||
_secretMasker = new SecretMasker();
|
var encoders = new List<ValueEncoder>()
|
||||||
_secretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);
|
{
|
||||||
_secretMasker.AddValueEncoder(ValueEncoders.UriDataEscape);
|
ValueEncoders.JsonStringEscape,
|
||||||
|
ValueEncoders.UriDataEscape
|
||||||
|
};
|
||||||
|
_secretMasker = new SecretMasker(encoders);
|
||||||
_traceManager = new TraceManager(traceListener, null, _secretMasker);
|
_traceManager = new TraceManager(traceListener, null, _secretMasker);
|
||||||
_trace = GetTrace(nameof(TestHostContext));
|
_trace = GetTrace(nameof(TestHostContext));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -931,6 +930,36 @@ namespace GitHub.Runner.Common.Tests.Util
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void LoadObject_ThrowsOnRequiredLoadObject()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = new(this))
|
||||||
|
{
|
||||||
|
Tracing trace = hc.GetTrace();
|
||||||
|
|
||||||
|
// Arrange: Create a directory with a file.
|
||||||
|
string directory = Path.Combine(hc.GetDirectory(WellKnownDirectory.Bin), Path.GetRandomFileName());
|
||||||
|
|
||||||
|
string file = Path.Combine(directory, "empty file");
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
|
||||||
|
File.WriteAllText(path: file, contents: "");
|
||||||
|
Assert.Throws<ArgumentNullException>(() => IOUtil.LoadObject<RunnerSettings>(file, true));
|
||||||
|
|
||||||
|
file = Path.Combine(directory, "invalid type file");
|
||||||
|
File.WriteAllText(path: file, contents: " ");
|
||||||
|
Assert.Throws<ArgumentException>(() => IOUtil.LoadObject<RunnerSettings>(file, true));
|
||||||
|
|
||||||
|
// Cleanup.
|
||||||
|
if (Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.Delete(directory, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task CreateDirectoryReparsePoint(IHostContext context, string link, string target)
|
private static async Task CreateDirectoryReparsePoint(IHostContext context, string link, string target)
|
||||||
{
|
{
|
||||||
#if OS_WINDOWS
|
#if OS_WINDOWS
|
||||||
|
|||||||
@@ -25,25 +25,25 @@ runs:
|
|||||||
- run: exit ${{ inputs.exit-code }}
|
- run: exit ${{ inputs.exit-code }}
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- run: echo "::set-output name=default::true"
|
- run: echo "default=true" >> $GITHUB_OUTPUT
|
||||||
id: default-conditional
|
id: default-conditional
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- run: echo "::set-output name=success::true"
|
- run: echo "success=true" >> $GITHUB_OUTPUT
|
||||||
id: success-conditional
|
id: success-conditional
|
||||||
shell: bash
|
shell: bash
|
||||||
if: success()
|
if: success()
|
||||||
|
|
||||||
- run: echo "::set-output name=failure::true"
|
- run: echo "failure=true" >> $GITHUB_OUTPUT
|
||||||
id: failure-conditional
|
id: failure-conditional
|
||||||
shell: bash
|
shell: bash
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|
||||||
- run: echo "::set-output name=always::true"
|
- run: echo "always=true" >> $GITHUB_OUTPUT
|
||||||
id: always-conditional
|
id: always-conditional
|
||||||
shell: bash
|
shell: bash
|
||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
- run: echo "failed"
|
- run: echo "failed"
|
||||||
shell: bash
|
shell: bash
|
||||||
if: ${{ inputs.exit-code == 1 && failure() }}
|
if: ${{ inputs.exit-code == 1 && failure() }}
|
||||||
|
|||||||
11
src/dev.sh
11
src/dev.sh
@@ -203,6 +203,13 @@ function runtest ()
|
|||||||
dotnet msbuild -t:test -p:PackageRuntime="${RUNTIME_ID}" -p:BUILDCONFIG="${BUILD_CONFIG}" -p:RunnerVersion="${RUNNER_VERSION}" ./dir.proj || failed "failed tests"
|
dotnet msbuild -t:test -p:PackageRuntime="${RUNTIME_ID}" -p:BUILDCONFIG="${BUILD_CONFIG}" -p:RunnerVersion="${RUNNER_VERSION}" ./dir.proj || failed "failed tests"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function format()
|
||||||
|
{
|
||||||
|
heading "Formatting..."
|
||||||
|
files="$(git status -s "*.cs" | awk '{print $2}' | tr '\n' ' ')"
|
||||||
|
dotnet format ${SCRIPT_DIR}/ActionsRunner.sln --exclude / --include $files || failed "failed formatting"
|
||||||
|
}
|
||||||
|
|
||||||
function package ()
|
function package ()
|
||||||
{
|
{
|
||||||
if [ ! -d "${LAYOUT_DIR}/bin" ]; then
|
if [ ! -d "${LAYOUT_DIR}/bin" ]; then
|
||||||
@@ -360,7 +367,9 @@ case $DEV_CMD in
|
|||||||
"l") layout;;
|
"l") layout;;
|
||||||
"package") package;;
|
"package") package;;
|
||||||
"p") package;;
|
"p") package;;
|
||||||
*) echo "Invalid cmd. Use build(b), test(t), layout(l) or package(p)";;
|
"format") format;;
|
||||||
|
"f") format;;
|
||||||
|
*) echo "Invalid cmd. Use build(b), test(t), layout(l), package(p), or format(f)";;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.302.1
|
2.303.0
|
||||||
|
|||||||
Reference in New Issue
Block a user