2022-11-02 09:40:19 -04:00
using System ;
2019-10-10 00:52:42 -04:00
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
2022-01-26 13:23:24 -05:00
using System.Net.Http ;
2021-09-29 15:49:31 -04:00
using System.Text ;
2019-10-10 00:52:42 -04:00
using System.Threading ;
using System.Threading.Tasks ;
2023-02-27 11:50:28 -05:00
using GitHub.DistributedTask.Pipelines ;
2022-01-26 13:23:24 -05:00
using GitHub.DistributedTask.WebApi ;
2019-10-10 00:52:42 -04:00
using GitHub.Runner.Common ;
2022-01-26 13:23:24 -05:00
using GitHub.Runner.Common.Util ;
2019-10-10 00:52:42 -04:00
using GitHub.Runner.Sdk ;
2022-01-26 13:23:24 -05:00
using GitHub.Services.Common ;
using GitHub.Services.WebApi ;
using Pipelines = GitHub . DistributedTask . Pipelines ;
2019-10-10 00:52:42 -04:00
namespace GitHub.Runner.Worker
{
[ServiceLocator(Default = typeof(JobRunner))]
public interface IJobRunner : IRunnerService
{
2023-02-27 11:50:28 -05:00
Task < TaskResult > RunAsync ( AgentJobRequestMessage message , CancellationToken jobRequestCancellationToken );
2019-10-10 00:52:42 -04:00
}
public sealed class JobRunner : RunnerService , IJobRunner
{
private IJobServerQueue _jobServerQueue ;
2022-01-26 13:23:24 -05:00
private RunnerSettings _runnerSettings ;
2019-10-10 00:52:42 -04:00
private ITempDirectoryManager _tempDirectoryManager ;
2023-02-27 11:50:28 -05:00
public async Task < TaskResult > RunAsync ( AgentJobRequestMessage message , CancellationToken jobRequestCancellationToken )
2019-10-10 00:52:42 -04:00
{
// Validate parameters.
Trace . Entering ();
ArgUtil . NotNull ( message , nameof ( message ));
ArgUtil . NotNull ( message . Resources , nameof ( message . Resources ));
ArgUtil . NotNull ( message . Variables , nameof ( message . Variables ));
ArgUtil . NotNull ( message . Steps , nameof ( message . Steps ));
Trace . Info ( "Job ID {0}" , message . JobId );
DateTime jobStartTimeUtc = DateTime . UtcNow ;
2023-02-01 16:18:31 -05:00
IRunnerService server = null ;
2019-10-10 00:52:42 -04:00
ServiceEndpoint systemConnection = message . Resources . Endpoints . Single ( x => string . Equals ( x . Name , WellKnownServiceEndpointNames . SystemVssConnection , StringComparison . OrdinalIgnoreCase ));
2023-02-27 11:50:28 -05:00
if ( MessageUtil . IsRunServiceJob ( message . MessageType ))
2023-02-01 16:18:31 -05:00
{
var runServer = HostContext . GetService < IRunServer >();
VssCredentials jobServerCredential = VssUtil . GetVssCredential ( systemConnection );
await runServer . ConnectAsync ( systemConnection . Url , jobServerCredential );
server = runServer ;
}
2023-02-27 11:50:28 -05:00
else
2023-02-01 16:18:31 -05:00
{
// Setup the job server and job server queue.
var jobServer = HostContext . GetService < IJobServer >();
VssCredentials jobServerCredential = VssUtil . GetVssCredential ( systemConnection );
Uri jobServerUrl = systemConnection . Url ;
2019-10-10 00:52:42 -04:00
2023-02-01 16:18:31 -05:00
Trace . Info ( $"Creating job server with URL: {jobServerUrl}" );
// jobServerQueue is the throttling reporter.
_jobServerQueue = HostContext . GetService < IJobServerQueue >();
VssConnection jobConnection = VssUtil . CreateConnection ( jobServerUrl , jobServerCredential , new DelegatingHandler [] { new ThrottlingReportHandler ( _jobServerQueue ) });
await jobServer . ConnectAsync ( jobConnection );
2019-10-10 00:52:42 -04:00
2023-02-01 16:18:31 -05:00
_jobServerQueue . Start ( message );
server = jobServer ;
}
2023-02-27 11:50:28 -05:00
2019-10-10 00:52:42 -04:00
HostContext . WritePerfCounter ( $"WorkerJobServerQueueStarted_{message.RequestId.ToString()}" );
IExecutionContext jobContext = null ;
CancellationTokenRegistration ? runnerShutdownRegistration = null ;
try
{
// Create the job execution context.
jobContext = HostContext . CreateService < IExecutionContext >();
jobContext . InitializeJob ( message , jobRequestCancellationToken );
Trace . Info ( "Starting the job execution context." );
jobContext . Start ();
jobContext . Debug ( $"Starting: {message.JobDisplayName}" );
runnerShutdownRegistration = HostContext . RunnerShutdownToken . Register (() =>
{
// log an issue, then runner get shutdown by Ctrl-C or Ctrl-Break.
// the server will use Ctrl-Break to tells the runner that operating system is shutting down.
string errorMessage ;
switch ( HostContext . RunnerShutdownReason )
{
case ShutdownReason . UserCancelled :
errorMessage = "The runner has received a shutdown signal. This can happen when the runner service is stopped, or a manually started runner is canceled." ;
break ;
case ShutdownReason . OperatingSystemShutdown :
errorMessage = $"Operating system is shutting down for computer '{Environment.MachineName}'" ;
break ;
default :
throw new ArgumentException ( HostContext . RunnerShutdownReason . ToString (), nameof ( HostContext . RunnerShutdownReason ));
}
jobContext . AddIssue ( new Issue () { Type = IssueType . Error , Message = errorMessage });
});
// Validate directory permissions.
string workDirectory = HostContext . GetDirectory ( WellKnownDirectory . Work );
Trace . Info ( $"Validating directory permissions for: '{workDirectory}'" );
try
{
Directory . CreateDirectory ( workDirectory );
IOUtil . ValidateExecutePermission ( workDirectory );
}
catch ( Exception ex )
{
Trace . Error ( ex );
jobContext . Error ( ex );
2023-02-01 16:18:31 -05:00
return await CompleteJobAsync ( server , jobContext , message , TaskResult . Failed );
2019-10-10 00:52:42 -04:00
}
2020-07-19 19:05:47 -04:00
if ( jobContext . Global . WriteDebug )
2020-01-03 21:23:46 -05:00
{
jobContext . SetRunnerContext ( "debug" , "1" );
}
2019-10-10 00:52:42 -04:00
jobContext . SetRunnerContext ( "os" , VarUtil . OS );
2021-10-13 20:49:26 -07:00
jobContext . SetRunnerContext ( "arch" , VarUtil . OSArchitecture );
2019-10-10 00:52:42 -04:00
2022-01-26 13:23:24 -05:00
_runnerSettings = HostContext . GetService < IConfigurationStore >(). GetSettings ();
jobContext . SetRunnerContext ( "name" , _runnerSettings . AgentName );
2021-09-16 15:25:51 +02:00
2019-10-10 00:52:42 -04:00
string toolsDirectory = HostContext . GetDirectory ( WellKnownDirectory . Tools );
Directory . CreateDirectory ( toolsDirectory );
jobContext . SetRunnerContext ( "tool_cache" , toolsDirectory );
// Setup TEMP directories
_tempDirectoryManager = HostContext . GetService < ITempDirectoryManager >();
_tempDirectoryManager . InitializeTempDirectory ( jobContext );
// Get the job extension.
Trace . Info ( "Getting job extension." );
IJobExtension jobExtension = HostContext . CreateService < IJobExtension >();
List < IStep > jobSteps = null ;
try
{
Trace . Info ( "Initialize job. Getting all job steps." );
jobSteps = await jobExtension . InitializeJob ( jobContext , message );
}
catch ( OperationCanceledException ex ) when ( jobContext . CancellationToken . IsCancellationRequested )
{
2022-02-23 15:48:24 +01:00
// set the job to cancelled
2019-10-10 00:52:42 -04:00
// don't log error issue to job ExecutionContext, since server owns the job level issue
2022-02-23 15:48:24 +01:00
Trace . Error ( $"Job is cancelled during initialize." );
2019-10-10 00:52:42 -04:00
Trace . Error ( $"Caught exception: {ex}" );
2023-02-01 16:18:31 -05:00
return await CompleteJobAsync ( server , jobContext , message , TaskResult . Canceled );
2019-10-10 00:52:42 -04:00
}
catch ( Exception ex )
{
// set the job to failed.
// don't log error issue to job ExecutionContext, since server owns the job level issue
Trace . Error ( $"Job initialize failed." );
Trace . Error ( $"Caught exception from {nameof(jobExtension.InitializeJob)}: {ex}" );
2023-02-01 16:18:31 -05:00
return await CompleteJobAsync ( server , jobContext , message , TaskResult . Failed );
2019-10-10 00:52:42 -04:00
}
// trace out all steps
Trace . Info ( $"Total job steps: {jobSteps.Count}." );
Trace . Verbose ( $"Job steps: '{string.Join(" , ", jobSteps.Select(x => x.DisplayName))}'" );
HostContext . WritePerfCounter ( $"WorkerJobInitialized_{message.RequestId.ToString()}" );
2021-09-09 21:55:15 -04:00
if ( systemConnection . Data . TryGetValue ( "GenerateIdTokenUrl" , out var generateIdTokenUrl ) &&
! string . IsNullOrEmpty ( generateIdTokenUrl ))
{
// Server won't issue ID_TOKEN for non-inprogress job.
// If the job is trying to use OIDC feature, we want the job to be marked as in-progress before running any customer's steps as much as we can.
// Timeline record update background process runs every 500ms, so delay 1000ms is enough for most of the cases
Trace . Info ( $"Waiting for job to be marked as started." );
await Task . WhenAny ( _jobServerQueue . JobRecordUpdated . Task , Task . Delay ( 1000 ));
}
2019-10-10 00:52:42 -04:00
// Run all job steps
Trace . Info ( "Run all job steps." );
var stepsRunner = HostContext . GetService < IStepsRunner >();
try
{
foreach ( var step in jobSteps )
{
2020-07-29 16:19:04 -04:00
jobContext . JobSteps . Enqueue ( step );
2019-10-10 00:52:42 -04:00
}
await stepsRunner . RunAsync ( jobContext );
}
catch ( Exception ex )
{
// StepRunner should never throw exception out.
// End up here mean there is a bug in StepRunner
// Log the error and fail the job.
Trace . Error ( $"Caught exception from job steps {nameof(StepsRunner)}: {ex}" );
jobContext . Error ( ex );
2023-02-01 16:18:31 -05:00
return await CompleteJobAsync ( server , jobContext , message , TaskResult . Failed );
2019-10-10 00:52:42 -04:00
}
finally
{
Trace . Info ( "Finalize job." );
2019-12-09 16:11:00 -05:00
jobExtension . FinalizeJob ( jobContext , message , jobStartTimeUtc );
2019-10-10 00:52:42 -04:00
}
Trace . Info ( $"Job result after all job steps finish: {jobContext.Result ?? TaskResult.Succeeded}" );
Trace . Info ( "Completing the job execution context." );
2023-02-01 16:18:31 -05:00
return await CompleteJobAsync ( server , jobContext , message );
2019-10-10 00:52:42 -04:00
}
finally
{
if ( runnerShutdownRegistration != null )
{
runnerShutdownRegistration . Value . Dispose ();
runnerShutdownRegistration = null ;
}
await ShutdownQueue ( throwOnFailure : false );
}
}
2023-02-01 16:18:31 -05:00
private async Task < TaskResult > CompleteJobAsync ( IRunnerService server , IExecutionContext jobContext , Pipelines . AgentJobRequestMessage message , TaskResult ? taskResult = null )
{
if ( server is IRunServer runServer )
{
return await CompleteJobAsync ( runServer , jobContext , message , taskResult );
}
else if ( server is IJobServer jobServer )
{
return await CompleteJobAsync ( jobServer , jobContext , message , taskResult );
}
else
{
throw new NotSupportedException ();
}
}
private async Task < TaskResult > CompleteJobAsync ( IRunServer runServer , IExecutionContext jobContext , Pipelines . AgentJobRequestMessage message , TaskResult ? taskResult = null )
{
jobContext . Debug ( $"Finishing: {message.JobDisplayName}" );
TaskResult result = jobContext . Complete ( taskResult );
if ( jobContext . Global . Variables . TryGetValue ( "Node12ActionsWarnings" , out var node12Warnings ))
{
var actions = string . Join ( ", " , StringUtil . ConvertFromJson < HashSet < string >>( node12Warnings ));
jobContext . Warning ( string . Format ( Constants . Runner . Node12DetectedAfterEndOfLife , actions ));
}
// Make sure to clean temp after file upload since they may be pending fileupload still use the TEMP dir.
_tempDirectoryManager ?. CleanupTempDirectory ();
// Load any upgrade telemetry
LoadFromTelemetryFile ( jobContext . Global . JobTelemetry );
// Make sure we don't submit secrets as telemetry
MaskTelemetrySecrets ( jobContext . Global . JobTelemetry );
Trace . Info ( $"Raising job completed against run service" );
var completeJobRetryLimit = 5 ;
var exceptions = new List < Exception >();
while ( completeJobRetryLimit -- > 0 )
{
try
{
2023-02-07 15:10:53 -05:00
await runServer . CompleteJobAsync ( message . Plan . PlanId , message . JobId , result , jobContext . JobOutputs , jobContext . Global . StepsResult , default );
2023-02-01 16:18:31 -05:00
return result ;
}
catch ( Exception ex )
{
Trace . Error ( $"Catch exception while attempting to complete job {message.JobId}, job request {message.RequestId}." );
Trace . Error ( ex );
exceptions . Add ( ex );
}
// delay 5 seconds before next retry.
await Task . Delay ( TimeSpan . FromSeconds ( 5 ));
}
// rethrow exceptions from all attempts.
throw new AggregateException ( exceptions );
}
2019-10-10 00:52:42 -04:00
private async Task < TaskResult > CompleteJobAsync ( IJobServer jobServer , IExecutionContext jobContext , Pipelines . AgentJobRequestMessage message , TaskResult ? taskResult = null )
{
jobContext . Debug ( $"Finishing: {message.JobDisplayName}" );
TaskResult result = jobContext . Complete ( taskResult );
2022-01-26 13:23:24 -05:00
if ( _runnerSettings . DisableUpdate == true )
{
try
{
var currentVersion = new PackageVersion ( BuildConstants . RunnerPackage . Version );
ServiceEndpoint systemConnection = message . Resources . Endpoints . Single ( x => string . Equals ( x . Name , WellKnownServiceEndpointNames . SystemVssConnection , StringComparison . OrdinalIgnoreCase ));
VssCredentials serverCredential = VssUtil . GetVssCredential ( systemConnection );
var runnerServer = HostContext . GetService < IRunnerServer >();
await runnerServer . ConnectAsync ( systemConnection . Url , serverCredential );
var serverPackages = await runnerServer . GetPackagesAsync ( "agent" , BuildConstants . RunnerPackage . PackageName , 5 , includeToken : false , cancellationToken : CancellationToken . None );
if ( serverPackages . Count > 0 )
{
serverPackages = serverPackages . OrderByDescending ( x => x . Version ). ToList ();
Trace . Info ( $"Newer packages {StringUtil.ConvertToJson(serverPackages.Select(x => x.Version.ToString()))}" );
var warnOnFailedJob = false ; // any minor/patch version behind.
var warnOnOldRunnerVersion = false ; // >= 2 minor version behind
if ( serverPackages . Any ( x => x . Version . CompareTo ( currentVersion ) > 0 ))
{
Trace . Info ( $"Current runner version {currentVersion} is behind the latest runner version {serverPackages[0].Version}." );
warnOnFailedJob = true ;
}
if ( serverPackages . Where ( x => x . Version . Major == currentVersion . Major && x . Version . Minor > currentVersion . Minor ). Count () > 1 )
{
Trace . Info ( $"Current runner version {currentVersion} is way behind the latest runner version {serverPackages[0].Version}." );
warnOnOldRunnerVersion = true ;
}
if ( result == TaskResult . Failed && warnOnFailedJob )
{
jobContext . Warning ( $"This job failure may be caused by using an out of date self-hosted runner. You are currently using runner version {currentVersion}. Please update to the latest version {serverPackages[0].Version}" );
}
else if ( warnOnOldRunnerVersion )
{
jobContext . Warning ( $"This self-hosted runner is currently using runner version {currentVersion}. This version is out of date. Please update to the latest version {serverPackages[0].Version}" );
}
}
}
catch ( Exception ex )
{
// Ignore any error since suggest runner update is best effort.
Trace . Error ( $"Caught exception during runner version check: {ex}" );
}
}
2022-11-02 09:40:19 -04:00
if ( jobContext . Global . Variables . TryGetValue ( "Node12ActionsWarnings" , out var node12Warnings ))
2022-03-22 10:43:25 +01:00
{
2022-11-02 09:40:19 -04:00
var actions = string . Join ( ", " , StringUtil . ConvertFromJson < HashSet < string >>( node12Warnings ));
2022-03-22 10:43:25 +01:00
jobContext . Warning ( string . Format ( Constants . Runner . Node12DetectedAfterEndOfLife , actions ));
}
2019-10-10 00:52:42 -04:00
try
{
await ShutdownQueue ( throwOnFailure : true );
}
catch ( Exception ex )
{
Trace . Error ( $"Caught exception from {nameof(JobServerQueue)}.{nameof(_jobServerQueue.ShutdownAsync)}" );
Trace . Error ( "This indicate a failure during publish output variables. Fail the job to prevent unexpected job outputs." );
Trace . Error ( ex );
result = TaskResultUtil . MergeTaskResults ( result , TaskResult . Failed );
}
// Clean TEMP after finish process jobserverqueue, since there might be a pending fileupload still use the TEMP dir.
_tempDirectoryManager ?. CleanupTempDirectory ();
2020-07-19 19:05:47 -04:00
if (! jobContext . Global . Features . HasFlag ( PlanFeatures . JobCompletedPlanEvent ))
2019-10-10 00:52:42 -04:00
{
Trace . Info ( $"Skip raise job completed event call from worker because Plan version is {message.Plan.Version}" );
return result ;
}
2021-09-29 15:49:31 -04:00
// Load any upgrade telemetry
2022-02-11 16:18:41 -05:00
LoadFromTelemetryFile ( jobContext . Global . JobTelemetry );
2021-09-29 15:49:31 -04:00
2021-09-20 14:44:50 +02:00
// Make sure we don't submit secrets as telemetry
2022-02-11 16:18:41 -05:00
MaskTelemetrySecrets ( jobContext . Global . JobTelemetry );
2021-09-20 14:44:50 +02:00
2022-02-16 12:18:21 -05:00
Trace . Info ( $"Raising job completed event" );
2022-02-11 16:18:41 -05:00
var jobCompletedEvent = new JobCompletedEvent ( message . RequestId , message . JobId , result , jobContext . JobOutputs , jobContext . ActionsEnvironment , jobContext . Global . StepsTelemetry , jobContext . Global . JobTelemetry );
2019-10-10 00:52:42 -04:00
var completeJobRetryLimit = 5 ;
var exceptions = new List < Exception >();
while ( completeJobRetryLimit -- > 0 )
{
try
{
await jobServer . RaisePlanEventAsync ( message . Plan . ScopeIdentifier , message . Plan . PlanType , message . Plan . PlanId , jobCompletedEvent , default ( CancellationToken ));
return result ;
}
catch ( TaskOrchestrationPlanNotFoundException ex )
{
Trace . Error ( $"TaskOrchestrationPlanNotFoundException received, while attempting to raise JobCompletedEvent for job {message.JobId}." );
Trace . Error ( ex );
return TaskResult . Failed ;
}
catch ( TaskOrchestrationPlanSecurityException ex )
{
Trace . Error ( $"TaskOrchestrationPlanSecurityException received, while attempting to raise JobCompletedEvent for job {message.JobId}." );
Trace . Error ( ex );
return TaskResult . Failed ;
}
2020-05-21 11:09:50 -04:00
catch ( TaskOrchestrationPlanTerminatedException ex )
{
Trace . Error ( $"TaskOrchestrationPlanTerminatedException received, while attempting to raise JobCompletedEvent for job {message.JobId}." );
Trace . Error ( ex );
return TaskResult . Failed ;
}
2019-10-10 00:52:42 -04:00
catch ( Exception ex )
{
Trace . Error ( $"Catch exception while attempting to raise JobCompletedEvent for job {message.JobId}, job request {message.RequestId}." );
Trace . Error ( ex );
exceptions . Add ( ex );
}
// delay 5 seconds before next retry.
await Task . Delay ( TimeSpan . FromSeconds ( 5 ));
}
// rethrow exceptions from all attempts.
throw new AggregateException ( exceptions );
}
2021-09-20 14:44:50 +02:00
private void MaskTelemetrySecrets ( List < JobTelemetry > jobTelemetry )
{
foreach ( var telemetryItem in jobTelemetry )
{
telemetryItem . Message = HostContext . SecretMasker . MaskSecrets ( telemetryItem . Message );
}
}
2021-09-29 15:49:31 -04:00
private void LoadFromTelemetryFile ( List < JobTelemetry > jobTelemetry )
{
try
{
var telemetryFilePath = HostContext . GetConfigFile ( WellKnownConfigFile . Telemetry );
if ( File . Exists ( telemetryFilePath ))
{
var telemetryData = File . ReadAllText ( telemetryFilePath , Encoding . UTF8 );
var telemetry = new JobTelemetry
{
Message = $"Runner File Telemetry:\n{telemetryData}" ,
Type = JobTelemetryType . General
};
jobTelemetry . Add ( telemetry );
IOUtil . DeleteFile ( telemetryFilePath );
}
}
catch ( Exception e )
{
Trace . Error ( "Error when trying to load telemetry from telemetry file" );
Trace . Error ( e );
}
}
2019-10-10 00:52:42 -04:00
private async Task ShutdownQueue ( bool throwOnFailure )
{
if ( _jobServerQueue != null )
{
try
{
Trace . Info ( "Shutting down the job server queue." );
await _jobServerQueue . ShutdownAsync ();
}
catch ( Exception ex ) when (! throwOnFailure )
{
Trace . Error ( $"Caught exception from {nameof(JobServerQueue)}.{nameof(_jobServerQueue.ShutdownAsync)}" );
Trace . Error ( ex );
}
finally
{
_jobServerQueue = null ; // Prevent multiple attempts.
}
}
}
}
}