public async Task RunAsync() { // Validate args. Trace.Entering(); ArgUtil.NotNull(ExecutionContext, nameof(ExecutionContext)); ArgUtil.NotNull(ExecutionContext.Variables, nameof(ExecutionContext.Variables)); ArgUtil.NotNull(Task, nameof(Task)); var taskManager = HostContext.GetService <ITaskManager>(); var handlerFactory = HostContext.GetService <IHandlerFactory>(); // Set the task id and display name variable. using (var scope = ExecutionContext.Variables.CreateScope()) { scope.Set(Constants.Variables.Task.DisplayName, DisplayName); scope.Set(WellKnownDistributedTaskVariables.TaskInstanceId, Task.Id.ToString("D")); scope.Set(WellKnownDistributedTaskVariables.TaskDisplayName, DisplayName); scope.Set(WellKnownDistributedTaskVariables.TaskInstanceName, Task.Name); // Load the task definition and choose the handler. // TODO: Add a try catch here to give a better error message. Definition definition = taskManager.Load(Task); ArgUtil.NotNull(definition, nameof(definition)); // Verify task signatures if a fingerprint is configured for the Agent. var configurationStore = HostContext.GetService <IConfigurationStore>(); AgentSettings settings = configurationStore.GetSettings(); SignatureVerificationMode verificationMode = SignatureVerificationMode.None; if (settings.SignatureVerification != null) { verificationMode = settings.SignatureVerification.Mode; } if (verificationMode != SignatureVerificationMode.None) { ISignatureService signatureService = HostContext.CreateService <ISignatureService>(); Boolean verificationSuccessful = await signatureService.VerifyAsync(definition, ExecutionContext.CancellationToken); if (verificationSuccessful) { ExecutionContext.Output(StringUtil.Loc("TaskSignatureVerificationSucceeeded")); // Only extract if it's not the checkout task. if (!String.IsNullOrEmpty(definition.ZipPath)) { taskManager.Extract(ExecutionContext, Task); } } else { String message = StringUtil.Loc("TaskSignatureVerificationFailed"); if (verificationMode == SignatureVerificationMode.Error) { throw new InvalidOperationException(message); } else { ExecutionContext.Warning(message); } } } // Print out task metadata PrintTaskMetaData(definition); ExecutionData currentExecution = null; switch (Stage) { case JobRunStage.PreJob: currentExecution = definition.Data?.PreJobExecution; break; case JobRunStage.Main: currentExecution = definition.Data?.Execution; break; case JobRunStage.PostJob: currentExecution = definition.Data?.PostJobExecution; break; } ; HandlerData handlerData = GetHandlerData(ExecutionContext, currentExecution, PlatformUtil.HostOS); if (handlerData == null) { if (PlatformUtil.RunningOnWindows) { throw new InvalidOperationException(StringUtil.Loc("SupportedTaskHandlerNotFoundWindows", $"{PlatformUtil.HostOS}({PlatformUtil.HostArchitecture})")); } throw new InvalidOperationException(StringUtil.Loc("SupportedTaskHandlerNotFoundLinux")); } Trace.Info($"Handler data is of type {handlerData}"); Variables runtimeVariables = ExecutionContext.Variables; IStepHost stepHost = HostContext.CreateService <IDefaultStepHost>(); var stepTarget = ExecutionContext.StepTarget(); // Setup container stephost and the right runtime variables for running job inside container. if (stepTarget is ContainerInfo containerTarget) { if (handlerData is AgentPluginHandlerData) { // plugin handler always runs on the Host, the runtime variables needs to the variable works on the Host, ex: file path variable System.DefaultWorkingDirectory Dictionary <string, VariableValue> variableCopy = new Dictionary <string, VariableValue>(StringComparer.OrdinalIgnoreCase); foreach (var publicVar in ExecutionContext.Variables.Public) { variableCopy[publicVar.Key] = new VariableValue(stepTarget.TranslateToHostPath(publicVar.Value)); } foreach (var secretVar in ExecutionContext.Variables.Private) { variableCopy[secretVar.Key] = new VariableValue(stepTarget.TranslateToHostPath(secretVar.Value), true); } List <string> expansionWarnings; runtimeVariables = new Variables(HostContext, variableCopy, out expansionWarnings); expansionWarnings?.ForEach(x => ExecutionContext.Warning(x)); } else if (handlerData is NodeHandlerData || handlerData is Node10HandlerData || handlerData is PowerShell3HandlerData) { // Only the node, node10, and powershell3 handlers support running inside container. // Make sure required container is already created. ArgUtil.NotNullOrEmpty(containerTarget.ContainerId, nameof(containerTarget.ContainerId)); var containerStepHost = HostContext.CreateService <IContainerStepHost>(); containerStepHost.Container = containerTarget; stepHost = containerStepHost; } else { throw new NotSupportedException(String.Format("Task '{0}' is using legacy execution handler '{1}' which is not supported in container execution flow.", definition.Data.FriendlyName, handlerData.GetType().ToString())); } } // Load the default input values from the definition. Trace.Verbose("Loading default inputs."); var inputs = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); foreach (var input in (definition.Data?.Inputs ?? new TaskInputDefinition[0])) { string key = input?.Name?.Trim() ?? string.Empty; if (!string.IsNullOrEmpty(key)) { inputs[key] = input.DefaultValue?.Trim() ?? string.Empty; } } // Merge the instance inputs. Trace.Verbose("Loading instance inputs."); foreach (var input in (Task.Inputs as IEnumerable <KeyValuePair <string, string> > ?? new KeyValuePair <string, string> [0])) { string key = input.Key?.Trim() ?? string.Empty; if (!string.IsNullOrEmpty(key)) { inputs[key] = input.Value?.Trim() ?? string.Empty; } } // Expand the inputs. Trace.Verbose("Expanding inputs."); runtimeVariables.ExpandValues(target: inputs); VarUtil.ExpandEnvironmentVariables(HostContext, target: inputs); // Translate the server file path inputs to local paths. foreach (var input in definition.Data?.Inputs ?? new TaskInputDefinition[0]) { if (string.Equals(input.InputType, TaskInputType.FilePath, StringComparison.OrdinalIgnoreCase)) { Trace.Verbose($"Translating file path input '{input.Name}': '{inputs[input.Name]}'"); inputs[input.Name] = stepHost.ResolvePathForStepHost(TranslateFilePathInput(inputs[input.Name] ?? string.Empty)); Trace.Verbose($"Translated file path input '{input.Name}': '{inputs[input.Name]}'"); } } // Load the task environment. Trace.Verbose("Loading task environment."); var environment = new Dictionary <string, string>(VarUtil.EnvironmentVariableKeyComparer); foreach (var env in (Task.Environment ?? new Dictionary <string, string>(0))) { string key = env.Key?.Trim() ?? string.Empty; if (!string.IsNullOrEmpty(key)) { environment[key] = env.Value?.Trim() ?? string.Empty; } } // Expand the inputs. Trace.Verbose("Expanding task environment."); runtimeVariables.ExpandValues(target: environment); VarUtil.ExpandEnvironmentVariables(HostContext, target: environment); // Expand the handler inputs. Trace.Verbose("Expanding handler inputs."); VarUtil.ExpandValues(HostContext, source: inputs, target: handlerData.Inputs); runtimeVariables.ExpandValues(target: handlerData.Inputs); // Get each endpoint ID referenced by the task. var endpointIds = new List <Guid>(); foreach (var input in definition.Data?.Inputs ?? new TaskInputDefinition[0]) { if ((input.InputType ?? string.Empty).StartsWith("connectedService:", StringComparison.OrdinalIgnoreCase)) { string inputKey = input?.Name?.Trim() ?? string.Empty; string inputValue; if (!string.IsNullOrEmpty(inputKey) && inputs.TryGetValue(inputKey, out inputValue) && !string.IsNullOrEmpty(inputValue)) { foreach (string rawId in inputValue.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { Guid parsedId; if (Guid.TryParse(rawId.Trim(), out parsedId) && parsedId != Guid.Empty) { endpointIds.Add(parsedId); } } } } } if (endpointIds.Count > 0 && (runtimeVariables.GetBoolean(WellKnownDistributedTaskVariables.RestrictSecrets) ?? false) && (runtimeVariables.GetBoolean(Microsoft.TeamFoundation.Build.WebApi.BuildVariables.IsFork) ?? false)) { ExecutionContext.Result = TaskResult.Skipped; ExecutionContext.ResultCode = $"References service endpoint. PRs from repository forks are not allowed to access secrets in the pipeline. For more information see https://go.microsoft.com/fwlink/?linkid=862029 "; return; } // Get the endpoints referenced by the task. var endpoints = (ExecutionContext.Endpoints ?? new List <ServiceEndpoint>(0)) .Join(inner: endpointIds, outerKeySelector: (ServiceEndpoint endpoint) => endpoint.Id, innerKeySelector: (Guid endpointId) => endpointId, resultSelector: (ServiceEndpoint endpoint, Guid endpointId) => endpoint) .ToList(); // Add the system endpoint. foreach (ServiceEndpoint endpoint in (ExecutionContext.Endpoints ?? new List <ServiceEndpoint>(0))) { if (string.Equals(endpoint.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase)) { endpoints.Add(endpoint); break; } } // Get each secure file ID referenced by the task. var secureFileIds = new List <Guid>(); foreach (var input in definition.Data?.Inputs ?? new TaskInputDefinition[0]) { if (string.Equals(input.InputType ?? string.Empty, "secureFile", StringComparison.OrdinalIgnoreCase)) { string inputKey = input?.Name?.Trim() ?? string.Empty; string inputValue; if (!string.IsNullOrEmpty(inputKey) && inputs.TryGetValue(inputKey, out inputValue) && !string.IsNullOrEmpty(inputValue)) { foreach (string rawId in inputValue.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { Guid parsedId; if (Guid.TryParse(rawId.Trim(), out parsedId) && parsedId != Guid.Empty) { secureFileIds.Add(parsedId); } } } } } if (secureFileIds.Count > 0 && (runtimeVariables.GetBoolean(WellKnownDistributedTaskVariables.RestrictSecrets) ?? false) && (runtimeVariables.GetBoolean(Microsoft.TeamFoundation.Build.WebApi.BuildVariables.IsFork) ?? false)) { ExecutionContext.Result = TaskResult.Skipped; ExecutionContext.ResultCode = $"References secure file. PRs from repository forks are not allowed to access secrets in the pipeline. For more information see https://go.microsoft.com/fwlink/?linkid=862029"; return; } // Get the endpoints referenced by the task. var secureFiles = (ExecutionContext.SecureFiles ?? new List <SecureFile>(0)) .Join(inner: secureFileIds, outerKeySelector: (SecureFile secureFile) => secureFile.Id, innerKeySelector: (Guid secureFileId) => secureFileId, resultSelector: (SecureFile secureFile, Guid secureFileId) => secureFile) .ToList(); // Set output variables. foreach (var outputVar in definition.Data?.OutputVariables ?? new OutputVariable[0]) { if (outputVar != null && !string.IsNullOrEmpty(outputVar.Name)) { ExecutionContext.OutputVariables.Add(outputVar.Name); } } // translate inputs inputs = inputs.ToDictionary(kvp => kvp.Key, kvp => ExecutionContext.TranslatePathForStepTarget(kvp.Value)); // Create the handler. IHandler handler = handlerFactory.Create( ExecutionContext, Task.Reference, stepHost, endpoints, secureFiles, handlerData, inputs, environment, runtimeVariables, taskDirectory: definition.Directory); // Run the task. await handler.RunAsync(); } }
public async Task RunAsync() { // Validate args. Trace.Entering(); ArgUtil.NotNull(ExecutionContext, nameof(ExecutionContext)); ArgUtil.NotNull(ExecutionContext.Variables, nameof(ExecutionContext.Variables)); ArgUtil.NotNull(Task, nameof(Task)); var taskManager = HostContext.GetService <ITaskManager>(); var handlerFactory = HostContext.GetService <IHandlerFactory>(); // Set the task id and display name variable. using (var scope = ExecutionContext.Variables.CreateScope()) { scope.Set(Constants.Variables.Task.DisplayName, DisplayName); scope.Set(WellKnownDistributedTaskVariables.TaskInstanceId, Task.Id.ToString("D")); scope.Set(WellKnownDistributedTaskVariables.TaskDisplayName, DisplayName); scope.Set(WellKnownDistributedTaskVariables.TaskInstanceName, Task.Name); // Load the task definition and choose the handler. // TODO: Add a try catch here to give a better error message. Definition definition = taskManager.Load(Task); ArgUtil.NotNull(definition, nameof(definition)); // Verify Signatures and Re-Extract Tasks if neccessary await VerifyTask(taskManager, definition); // Print out task metadata PrintTaskMetaData(definition); ExecutionData currentExecution = null; switch (Stage) { case JobRunStage.PreJob: currentExecution = definition.Data?.PreJobExecution; break; case JobRunStage.Main: currentExecution = definition.Data?.Execution; break; case JobRunStage.PostJob: currentExecution = definition.Data?.PostJobExecution; break; } ; HandlerData handlerData = GetHandlerData(ExecutionContext, currentExecution, PlatformUtil.HostOS); if (handlerData == null) { if (PlatformUtil.RunningOnWindows) { throw new InvalidOperationException(StringUtil.Loc("SupportedTaskHandlerNotFoundWindows", $"{PlatformUtil.HostOS}({PlatformUtil.HostArchitecture})")); } throw new InvalidOperationException(StringUtil.Loc("SupportedTaskHandlerNotFoundLinux")); } Trace.Info($"Handler data is of type {handlerData}"); Variables runtimeVariables = ExecutionContext.Variables; IStepHost stepHost = HostContext.CreateService <IDefaultStepHost>(); var stepTarget = ExecutionContext.StepTarget(); // Setup container stephost and the right runtime variables for running job inside container. if (stepTarget is ContainerInfo containerTarget) { if (Stage == JobRunStage.PostJob && AgentKnobs.SkipPostExeceutionIfTargetContainerStopped.GetValue(ExecutionContext).AsBoolean()) { try { // Check that the target container is still running, if not Skip task execution IDockerCommandManager dockerManager = HostContext.GetService <IDockerCommandManager>(); bool isContainerRunning = await dockerManager.IsContainerRunning(ExecutionContext, containerTarget.ContainerId); if (!isContainerRunning) { ExecutionContext.Result = TaskResult.Skipped; ExecutionContext.ResultCode = $"Target container - {containerTarget.ContainerName} has been stopped, task post-execution will be skipped"; return; } } catch (Exception ex) { ExecutionContext.Write(WellKnownTags.Warning, $"Failed to check container state for task post-execution. Exception: {ex}"); } } if (handlerData is AgentPluginHandlerData) { // plugin handler always runs on the Host, the runtime variables needs to the variable works on the Host, ex: file path variable System.DefaultWorkingDirectory Dictionary <string, VariableValue> variableCopy = new Dictionary <string, VariableValue>(StringComparer.OrdinalIgnoreCase); foreach (var publicVar in ExecutionContext.Variables.Public) { variableCopy[publicVar.Key] = new VariableValue(stepTarget.TranslateToHostPath(publicVar.Value)); } foreach (var secretVar in ExecutionContext.Variables.Private) { variableCopy[secretVar.Key] = new VariableValue(stepTarget.TranslateToHostPath(secretVar.Value), true); } List <string> expansionWarnings; runtimeVariables = new Variables(HostContext, variableCopy, out expansionWarnings); expansionWarnings?.ForEach(x => ExecutionContext.Warning(x)); } else if (handlerData is BaseNodeHandlerData || handlerData is PowerShell3HandlerData) { // Only the node, node10, and powershell3 handlers support running inside container. // Make sure required container is already created. ArgUtil.NotNullOrEmpty(containerTarget.ContainerId, nameof(containerTarget.ContainerId)); var containerStepHost = HostContext.CreateService <IContainerStepHost>(); containerStepHost.Container = containerTarget; stepHost = containerStepHost; } else { throw new NotSupportedException(String.Format("Task '{0}' is using legacy execution handler '{1}' which is not supported in container execution flow.", definition.Data.FriendlyName, handlerData.GetType().ToString())); } } // Load the default input values from the definition. Trace.Verbose("Loading default inputs."); var inputs = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); foreach (var input in (definition.Data?.Inputs ?? new TaskInputDefinition[0])) { string key = input?.Name?.Trim() ?? string.Empty; if (!string.IsNullOrEmpty(key)) { if (AgentKnobs.DisableInputTrimming.GetValue(ExecutionContext).AsBoolean()) { inputs[key] = input.DefaultValue ?? string.Empty; } else { inputs[key] = input.DefaultValue?.Trim() ?? string.Empty; } } } // Merge the instance inputs. Trace.Verbose("Loading instance inputs."); foreach (var input in (Task.Inputs as IEnumerable <KeyValuePair <string, string> > ?? new KeyValuePair <string, string> [0])) { string key = input.Key?.Trim() ?? string.Empty; if (!string.IsNullOrEmpty(key)) { if (AgentKnobs.DisableInputTrimming.GetValue(ExecutionContext).AsBoolean()) { inputs[key] = input.Value ?? string.Empty; } else { inputs[key] = input.Value?.Trim() ?? string.Empty; } } } // Expand the inputs. Trace.Verbose("Expanding inputs."); runtimeVariables.ExpandValues(target: inputs); // We need to verify inputs of the tasks that were injected by decorators, to check if they contain secrets, // for security reasons execution of tasks in this case should be skipped. // Target task inputs could be injected into the decorator's tasks if the decorator has post-task-tasks or pre-task-tasks targets, // such tasks will have names that start with __system_pretargettask_ or __system_posttargettask_. var taskDecoratorManager = HostContext.GetService <ITaskDecoratorManager>(); if (taskDecoratorManager.IsInjectedTaskForTarget(Task.Name) && taskDecoratorManager.IsInjectedInputsContainsSecrets(inputs, out var inputsWithSecrets)) { var inputsForReport = taskDecoratorManager.GenerateTaskResultMessage(inputsWithSecrets); ExecutionContext.Result = TaskResult.Skipped; ExecutionContext.ResultCode = StringUtil.Loc("SecretsAreNotAllowedInInjectedTaskInputs", inputsForReport); return; } VarUtil.ExpandEnvironmentVariables(HostContext, target: inputs); // Translate the server file path inputs to local paths. foreach (var input in definition.Data?.Inputs ?? new TaskInputDefinition[0]) { if (string.Equals(input.InputType, TaskInputType.FilePath, StringComparison.OrdinalIgnoreCase)) { Trace.Verbose($"Translating file path input '{input.Name}': '{inputs[input.Name]}'"); inputs[input.Name] = stepHost.ResolvePathForStepHost(TranslateFilePathInput(inputs[input.Name] ?? string.Empty)); Trace.Verbose($"Translated file path input '{input.Name}': '{inputs[input.Name]}'"); } } // Load the task environment. Trace.Verbose("Loading task environment."); var environment = new Dictionary <string, string>(VarUtil.EnvironmentVariableKeyComparer); foreach (var env in (Task.Environment ?? new Dictionary <string, string>(0))) { string key = env.Key?.Trim() ?? string.Empty; if (!string.IsNullOrEmpty(key)) { environment[key] = env.Value?.Trim() ?? string.Empty; } } // Expand the inputs. Trace.Verbose("Expanding task environment."); runtimeVariables.ExpandValues(target: environment); VarUtil.ExpandEnvironmentVariables(HostContext, target: environment); // Expand the handler inputs. Trace.Verbose("Expanding handler inputs."); VarUtil.ExpandValues(HostContext, source: inputs, target: handlerData.Inputs); runtimeVariables.ExpandValues(target: handlerData.Inputs); // Get each endpoint ID referenced by the task. var endpointIds = new List <Guid>(); foreach (var input in definition.Data?.Inputs ?? new TaskInputDefinition[0]) { if ((input.InputType ?? string.Empty).StartsWith("connectedService:", StringComparison.OrdinalIgnoreCase)) { string inputKey = input?.Name?.Trim() ?? string.Empty; string inputValue; if (!string.IsNullOrEmpty(inputKey) && inputs.TryGetValue(inputKey, out inputValue) && !string.IsNullOrEmpty(inputValue)) { foreach (string rawId in inputValue.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { Guid parsedId; if (Guid.TryParse(rawId.Trim(), out parsedId) && parsedId != Guid.Empty) { endpointIds.Add(parsedId); } } } } } if (endpointIds.Count > 0 && (runtimeVariables.GetBoolean(WellKnownDistributedTaskVariables.RestrictSecrets) ?? false) && (runtimeVariables.GetBoolean(Microsoft.TeamFoundation.Build.WebApi.BuildVariables.IsFork) ?? false)) { ExecutionContext.Result = TaskResult.Skipped; ExecutionContext.ResultCode = $"References service endpoint. PRs from repository forks are not allowed to access secrets in the pipeline. For more information see https://go.microsoft.com/fwlink/?linkid=862029 "; return; } // Get the endpoints referenced by the task. var endpoints = (ExecutionContext.Endpoints ?? new List <ServiceEndpoint>(0)) .Join(inner: endpointIds, outerKeySelector: (ServiceEndpoint endpoint) => endpoint.Id, innerKeySelector: (Guid endpointId) => endpointId, resultSelector: (ServiceEndpoint endpoint, Guid endpointId) => endpoint) .ToList(); // Add the system endpoint. foreach (ServiceEndpoint endpoint in (ExecutionContext.Endpoints ?? new List <ServiceEndpoint>(0))) { if (string.Equals(endpoint.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase)) { endpoints.Add(endpoint); break; } } // Get each secure file ID referenced by the task. var secureFileIds = new List <Guid>(); foreach (var input in definition.Data?.Inputs ?? new TaskInputDefinition[0]) { if (string.Equals(input.InputType ?? string.Empty, "secureFile", StringComparison.OrdinalIgnoreCase)) { string inputKey = input?.Name?.Trim() ?? string.Empty; string inputValue; if (!string.IsNullOrEmpty(inputKey) && inputs.TryGetValue(inputKey, out inputValue) && !string.IsNullOrEmpty(inputValue)) { foreach (string rawId in inputValue.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { Guid parsedId; if (Guid.TryParse(rawId.Trim(), out parsedId) && parsedId != Guid.Empty) { secureFileIds.Add(parsedId); } } } } } if (secureFileIds.Count > 0 && (runtimeVariables.GetBoolean(WellKnownDistributedTaskVariables.RestrictSecrets) ?? false) && (runtimeVariables.GetBoolean(Microsoft.TeamFoundation.Build.WebApi.BuildVariables.IsFork) ?? false)) { ExecutionContext.Result = TaskResult.Skipped; ExecutionContext.ResultCode = $"References secure file. PRs from repository forks are not allowed to access secrets in the pipeline. For more information see https://go.microsoft.com/fwlink/?linkid=862029"; return; } // Get the endpoints referenced by the task. var secureFiles = (ExecutionContext.SecureFiles ?? new List <SecureFile>(0)) .Join(inner: secureFileIds, outerKeySelector: (SecureFile secureFile) => secureFile.Id, innerKeySelector: (Guid secureFileId) => secureFileId, resultSelector: (SecureFile secureFile, Guid secureFileId) => secureFile) .ToList(); // Set output variables. foreach (var outputVar in definition.Data?.OutputVariables ?? new OutputVariable[0]) { if (outputVar != null && !string.IsNullOrEmpty(outputVar.Name)) { ExecutionContext.OutputVariables.Add(outputVar.Name); } } // translate inputs inputs = inputs.ToDictionary(kvp => kvp.Key, kvp => ExecutionContext.TranslatePathForStepTarget(kvp.Value)); // Create the handler. IHandler handler = handlerFactory.Create( ExecutionContext, Task.Reference, stepHost, endpoints, secureFiles, handlerData, inputs, environment, runtimeVariables, taskDirectory: definition.Directory); // Run the task. int retryCount = this.Task.RetryCountOnTaskFailure; if (retryCount > 0) { if (retryCount > RetryCountOnTaskFailureLimit) { ExecutionContext.Warning(StringUtil.Loc("RetryCountLimitExceeded", RetryCountOnTaskFailureLimit, retryCount)); retryCount = RetryCountOnTaskFailureLimit; } RetryHelper rh = new RetryHelper(ExecutionContext, retryCount); await rh.RetryStep(async() => await handler.RunAsync(), RetryHelper.ExponentialDelay); } else { await handler.RunAsync(); } } }