mirror of
https://github.com/actions/runner.git
synced 2026-07-07 05:18:42 +08:00
Compare commits
5 Commits
dapsetup
...
fix/normal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed0e5b75ee | ||
|
|
fffded93ac | ||
|
|
8307b8fe33 | ||
|
|
7585eb30aa | ||
|
|
0cdaa36d07 |
@@ -64,6 +64,7 @@ namespace GitHub.Runner.Common
|
|||||||
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();
|
||||||
|
private string _canonicalRootDirectory;
|
||||||
private Tracing _trace;
|
private Tracing _trace;
|
||||||
private Tracing _actionsHttpTrace;
|
private Tracing _actionsHttpTrace;
|
||||||
private Tracing _netcoreHttpTrace;
|
private Tracing _netcoreHttpTrace;
|
||||||
@@ -391,7 +392,12 @@ namespace GitHub.Runner.Common
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case WellKnownDirectory.Root:
|
case WellKnownDirectory.Root:
|
||||||
path = new DirectoryInfo(GetDirectory(WellKnownDirectory.Bin)).Parent.FullName;
|
if (_canonicalRootDirectory == null)
|
||||||
|
{
|
||||||
|
_canonicalRootDirectory = PathUtil.GetCanonicalPath(
|
||||||
|
new DirectoryInfo(GetDirectory(WellKnownDirectory.Bin)).Parent.FullName);
|
||||||
|
}
|
||||||
|
path = _canonicalRootDirectory;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WellKnownDirectory.Temp:
|
case WellKnownDirectory.Temp:
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
|
||||||
namespace GitHub.Runner.Sdk
|
namespace GitHub.Runner.Sdk
|
||||||
{
|
{
|
||||||
@@ -6,8 +9,98 @@ namespace GitHub.Runner.Sdk
|
|||||||
{
|
{
|
||||||
#if OS_WINDOWS
|
#if OS_WINDOWS
|
||||||
public static readonly string PathVariable = "Path";
|
public static readonly string PathVariable = "Path";
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
private static extern SafeFileHandle CreateFile(
|
||||||
|
string lpFileName,
|
||||||
|
uint dwDesiredAccess,
|
||||||
|
uint dwShareMode,
|
||||||
|
System.IntPtr lpSecurityAttributes,
|
||||||
|
uint dwCreationDisposition,
|
||||||
|
uint dwFlagsAndAttributes,
|
||||||
|
System.IntPtr hTemplateFile);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
private static extern uint GetFinalPathNameByHandle(
|
||||||
|
SafeFileHandle hFile,
|
||||||
|
[Out] StringBuilder lpszFilePath,
|
||||||
|
uint cchFilePath,
|
||||||
|
uint dwFlags);
|
||||||
|
|
||||||
|
private const uint FILE_READ_ATTRIBUTES = 0x80;
|
||||||
|
private const uint FILE_SHARE_READ = 0x1;
|
||||||
|
private const uint FILE_SHARE_WRITE = 0x2;
|
||||||
|
private const uint FILE_SHARE_DELETE = 0x4;
|
||||||
|
private const uint OPEN_EXISTING = 3;
|
||||||
|
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
|
||||||
|
private const uint VOLUME_NAME_DOS = 0x0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the NTFS canonical path for a directory, resolving drive letter
|
||||||
|
/// and folder name casing to match what is stored on disk.
|
||||||
|
/// On non-Windows platforms, returns the path unchanged.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetCanonicalPath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path) || !Directory.Exists(path))
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var handle = CreateFile(
|
||||||
|
path,
|
||||||
|
FILE_READ_ATTRIBUTES,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||||
|
System.IntPtr.Zero,
|
||||||
|
OPEN_EXISTING,
|
||||||
|
FILE_FLAG_BACKUP_SEMANTICS,
|
||||||
|
System.IntPtr.Zero);
|
||||||
|
|
||||||
|
if (handle.IsInvalid)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new StringBuilder(1024);
|
||||||
|
var result = GetFinalPathNameByHandle(handle, buffer, (uint)buffer.Capacity, VOLUME_NAME_DOS);
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry with a larger buffer if the path was longer than expected
|
||||||
|
if (result >= buffer.Capacity)
|
||||||
|
{
|
||||||
|
buffer = new StringBuilder((int)result + 1);
|
||||||
|
result = GetFinalPathNameByHandle(handle, buffer, (uint)buffer.Capacity, VOLUME_NAME_DOS);
|
||||||
|
if (result == 0 || result >= buffer.Capacity)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var canonicalPath = buffer.ToString();
|
||||||
|
|
||||||
|
// Strip the \\?\UNC\ prefix and convert to standard UNC path
|
||||||
|
if (canonicalPath.StartsWith(@"\\?\UNC\", System.StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
canonicalPath = @"\\" + canonicalPath.Substring(8);
|
||||||
|
}
|
||||||
|
// Strip the \\?\ prefix for local paths
|
||||||
|
else if (canonicalPath.StartsWith(@"\\?\", System.StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
canonicalPath = canonicalPath.Substring(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return canonicalPath;
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
public static readonly string PathVariable = "PATH";
|
public static readonly string PathVariable = "PATH";
|
||||||
|
|
||||||
|
public static string GetCanonicalPath(string path)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public static string PrependPath(string path, string currentPath)
|
public static string PrependPath(string path, string currentPath)
|
||||||
|
|||||||
@@ -243,6 +243,26 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
{
|
{
|
||||||
if (_state != DapSessionState.NotStarted)
|
if (_state != DapSessionState.NotStarted)
|
||||||
{
|
{
|
||||||
|
// Pause so the user can inspect final job state before we tear down,
|
||||||
|
// but only if the user was stepping through (not if they hit continue).
|
||||||
|
if (IsActive && _pauseOnNextStep)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_jobContext != null)
|
||||||
|
{
|
||||||
|
Trace.Info("Job completed — pausing for inspection");
|
||||||
|
SendStoppedEvent("completed", "Job completed — inspect variables before the session ends.");
|
||||||
|
|
||||||
|
await WaitForCommandAsync(_jobContext.CancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Warning($"DAP job-completed pause error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
OnJobCompleted();
|
OnJobCompleted();
|
||||||
@@ -252,8 +272,6 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
Trace.Warning($"DAP OnJobCompleted error: {ex.Message}");
|
Trace.Warning($"DAP OnJobCompleted error: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await StopAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopAsync()
|
public async Task StopAsync()
|
||||||
@@ -1302,6 +1320,13 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
_commandTcs = new TaskCompletionSource<DapCommand>(TaskCreationOptions.RunContinuationsAsynchronously);
|
_commandTcs = new TaskCompletionSource<DapCommand>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If cancellation already fired before we created the new TCS,
|
||||||
|
// the registration callback targeted the old one. Unblock now.
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_commandTcs.TrySetResult(DapCommand.Disconnect);
|
||||||
|
}
|
||||||
|
|
||||||
Trace.Info("Waiting for debugger command...");
|
Trace.Info("Waiting for debugger command...");
|
||||||
|
|
||||||
var command = await _commandTcs.Task;
|
var command = await _commandTcs.Task;
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
Task OnStepStartingAsync(IStep step);
|
Task OnStepStartingAsync(IStep step);
|
||||||
void OnStepCompleted(IStep step);
|
void OnStepCompleted(IStep step);
|
||||||
Task OnJobCompletedAsync();
|
Task OnJobCompletedAsync();
|
||||||
|
Task StopAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ using GitHub.DistributedTask.WebApi;
|
|||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Runner.Worker.Dap;
|
||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
@@ -50,6 +51,7 @@ namespace GitHub.Runner.Worker
|
|||||||
private Task _diskSpaceCheckTask = null;
|
private Task _diskSpaceCheckTask = null;
|
||||||
private CancellationTokenSource _serviceConnectivityCheckToken = new();
|
private CancellationTokenSource _serviceConnectivityCheckToken = new();
|
||||||
private Task _serviceConnectivityCheckTask = null;
|
private Task _serviceConnectivityCheckTask = null;
|
||||||
|
private IDapDebugger _dapDebugger;
|
||||||
|
|
||||||
// Download all required actions.
|
// Download all required actions.
|
||||||
// Make sure all condition inputs are valid.
|
// Make sure all condition inputs are valid.
|
||||||
@@ -67,6 +69,7 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
List<IStep> preJobSteps = new();
|
List<IStep> preJobSteps = new();
|
||||||
List<IStep> jobSteps = new();
|
List<IStep> jobSteps = new();
|
||||||
|
var initSucceeded = false;
|
||||||
using (var register = jobContext.CancellationToken.Register(() => { context.CancelToken(); }))
|
using (var register = jobContext.CancellationToken.Register(() => { context.CancelToken(); }))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -481,6 +484,41 @@ namespace GitHub.Runner.Worker
|
|||||||
Trace.Info($"Start checking service connectivity in background.");
|
Trace.Info($"Start checking service connectivity in background.");
|
||||||
_serviceConnectivityCheckTask = CheckServiceConnectivityAsync(context, _serviceConnectivityCheckToken.Token);
|
_serviceConnectivityCheckTask = CheckServiceConnectivityAsync(context, _serviceConnectivityCheckToken.Token);
|
||||||
|
|
||||||
|
// Start the DAP debugger and wait for a client connection inside
|
||||||
|
// "Set up job" so the step stays in-progress while we wait.
|
||||||
|
if (jobContext.Global.Debugger?.Enabled == true)
|
||||||
|
{
|
||||||
|
Trace.Info("Debugger enabled — starting inside Set up job");
|
||||||
|
context.Output("Starting debugger…");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_dapDebugger = HostContext.GetService<IDapDebugger>();
|
||||||
|
await _dapDebugger.StartAsync(jobContext);
|
||||||
|
|
||||||
|
context.Output("Waiting for debugger client to connect…");
|
||||||
|
|
||||||
|
await _dapDebugger.WaitUntilReadyAsync();
|
||||||
|
context.Output("Debugger connected.");
|
||||||
|
AddDebuggerConnectionTelemetry(jobContext, "Connected");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (jobContext.CancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Trace.Info("Job was cancelled before debugger client connected.");
|
||||||
|
AddDebuggerConnectionTelemetry(jobContext, "Canceled");
|
||||||
|
context.Error("Job was cancelled before debugger client connected.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error($"DAP debugger failed: {ex.Message}");
|
||||||
|
AddDebuggerConnectionTelemetry(jobContext, $"Failed: {ex.GetType().Name}");
|
||||||
|
context.Error("The debugger failed to start or no debugger client connected in time.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initSucceeded = true;
|
||||||
return steps;
|
return steps;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex) when (jobContext.CancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException ex) when (jobContext.CancellationToken.IsCancellationRequested)
|
||||||
@@ -501,12 +539,36 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
// If InitializeJob failed after the debugger was started,
|
||||||
|
// tear down the transport here since FinalizeJob won't run.
|
||||||
|
if (!initSucceeded && _dapDebugger != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dapDebugger.StopAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Warning($"DAP debugger cleanup during failed init: {ex.Message}");
|
||||||
|
}
|
||||||
|
_dapDebugger = null;
|
||||||
|
}
|
||||||
|
|
||||||
context.Debug("Finishing: Set up job");
|
context.Debug("Finishing: Set up job");
|
||||||
context.Complete();
|
context.Complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddDebuggerConnectionTelemetry(IExecutionContext jobContext, string result)
|
||||||
|
{
|
||||||
|
jobContext.Global.JobTelemetry.Add(new JobTelemetry
|
||||||
|
{
|
||||||
|
Type = JobTelemetryType.General,
|
||||||
|
Message = $"DebuggerConnectionResult: {result}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private string GetWorkflowReference(IDictionary<string, VariableValue> variables)
|
private string GetWorkflowReference(IDictionary<string, VariableValue> variables)
|
||||||
{
|
{
|
||||||
var reference = "";
|
var reference = "";
|
||||||
@@ -782,6 +844,34 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
// Pause for debugger inspection, then tear down the DAP session.
|
||||||
|
// OnJobCompletedAsync pauses first, then sends terminated/exited
|
||||||
|
// events and stops the transport.
|
||||||
|
if (_dapDebugger != null)
|
||||||
|
{
|
||||||
|
context.Output("Job completed — pausing for debugger inspection. Press continue to finish.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dapDebugger.OnJobCompletedAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Warning($"DAP debugger completion error: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dapDebugger.StopAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Warning($"DAP debugger stop error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_dapDebugger = null;
|
||||||
|
}
|
||||||
|
|
||||||
context.Debug("Finishing: Complete job");
|
context.Debug("Finishing: Complete job");
|
||||||
context.Complete();
|
context.Complete();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ using GitHub.DistributedTask.WebApi;
|
|||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Runner.Worker.Dap;
|
|
||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
using GitHub.Services.WebApi;
|
using GitHub.Services.WebApi;
|
||||||
using Sdk.RSWebApi.Contracts;
|
using Sdk.RSWebApi.Contracts;
|
||||||
@@ -29,7 +28,6 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
public sealed class JobRunner : RunnerService, IJobRunner
|
public sealed class JobRunner : RunnerService, IJobRunner
|
||||||
{
|
{
|
||||||
private const string DebuggerConnectionTelemetryPrefix = "DebuggerConnectionResult";
|
|
||||||
private IJobServerQueue _jobServerQueue;
|
private IJobServerQueue _jobServerQueue;
|
||||||
private RunnerSettings _runnerSettings;
|
private RunnerSettings _runnerSettings;
|
||||||
private ITempDirectoryManager _tempDirectoryManager;
|
private ITempDirectoryManager _tempDirectoryManager;
|
||||||
@@ -114,7 +112,6 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
IExecutionContext jobContext = null;
|
IExecutionContext jobContext = null;
|
||||||
CancellationTokenRegistration? runnerShutdownRegistration = null;
|
CancellationTokenRegistration? runnerShutdownRegistration = null;
|
||||||
IDapDebugger dapDebugger = null;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Create the job execution context.
|
// Create the job execution context.
|
||||||
@@ -181,25 +178,6 @@ namespace GitHub.Runner.Worker
|
|||||||
_tempDirectoryManager = HostContext.GetService<ITempDirectoryManager>();
|
_tempDirectoryManager = HostContext.GetService<ITempDirectoryManager>();
|
||||||
_tempDirectoryManager.InitializeTempDirectory(jobContext);
|
_tempDirectoryManager.InitializeTempDirectory(jobContext);
|
||||||
|
|
||||||
// Setup the debugger
|
|
||||||
if (jobContext.Global.Debugger?.Enabled == true)
|
|
||||||
{
|
|
||||||
Trace.Info("Debugger enabled for this job run");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
dapDebugger = HostContext.GetService<IDapDebugger>();
|
|
||||||
await dapDebugger.StartAsync(jobContext);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Error($"Failed to start DAP debugger: {ex.Message}");
|
|
||||||
AddDebuggerConnectionTelemetry(jobContext, $"Failed: {ex.Message}");
|
|
||||||
jobContext.Error("Failed to start debugger.");
|
|
||||||
return await CompleteJobAsync(server, jobContext, message, TaskResult.Failed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Get the job extension.
|
// Get the job extension.
|
||||||
Trace.Info("Getting job extension.");
|
Trace.Info("Getting job extension.");
|
||||||
@@ -242,33 +220,6 @@ namespace GitHub.Runner.Worker
|
|||||||
await Task.WhenAny(_jobServerQueue.JobRecordUpdated.Task, Task.Delay(1000));
|
await Task.WhenAny(_jobServerQueue.JobRecordUpdated.Task, Task.Delay(1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for DAP debugger client connection and handshake after "Set up job"
|
|
||||||
// so the job page shows the setup step before we block on the debugger
|
|
||||||
if (dapDebugger != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await dapDebugger.WaitUntilReadyAsync();
|
|
||||||
AddDebuggerConnectionTelemetry(jobContext, "Connected");
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (jobRequestCancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
Trace.Info("Job was cancelled before debugger client connected.");
|
|
||||||
AddDebuggerConnectionTelemetry(jobContext, "Canceled");
|
|
||||||
jobContext.Error("Job was cancelled before debugger client connected.");
|
|
||||||
return await CompleteJobAsync(server, jobContext, message, TaskResult.Canceled);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Error($"DAP debugger failed to become ready: {ex.Message}");
|
|
||||||
AddDebuggerConnectionTelemetry(jobContext, $"Failed: {ex.Message}");
|
|
||||||
|
|
||||||
// If debugging was requested but the debugger is not available, fail the job
|
|
||||||
jobContext.Error("The debugger failed to start or no debugger client connected in time.");
|
|
||||||
return await CompleteJobAsync(server, jobContext, message, TaskResult.Failed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all job steps
|
// Run all job steps
|
||||||
Trace.Info("Run all job steps.");
|
Trace.Info("Run all job steps.");
|
||||||
var stepsRunner = HostContext.GetService<IStepsRunner>();
|
var stepsRunner = HostContext.GetService<IStepsRunner>();
|
||||||
@@ -309,11 +260,6 @@ namespace GitHub.Runner.Worker
|
|||||||
runnerShutdownRegistration = null;
|
runnerShutdownRegistration = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dapDebugger != null)
|
|
||||||
{
|
|
||||||
await dapDebugger.OnJobCompletedAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
await ShutdownQueue(throwOnFailure: false);
|
await ShutdownQueue(throwOnFailure: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,15 +441,6 @@ namespace GitHub.Runner.Worker
|
|||||||
throw new AggregateException(exceptions);
|
throw new AggregateException(exceptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddDebuggerConnectionTelemetry(IExecutionContext jobContext, string result)
|
|
||||||
{
|
|
||||||
jobContext.Global.JobTelemetry.Add(new JobTelemetry
|
|
||||||
{
|
|
||||||
Type = JobTelemetryType.General,
|
|
||||||
Message = $"{DebuggerConnectionTelemetryPrefix}: {result}"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MaskTelemetrySecrets(List<JobTelemetry> jobTelemetry)
|
private void MaskTelemetrySecrets(List<JobTelemetry> jobTelemetry)
|
||||||
{
|
{
|
||||||
foreach (var telemetryItem in jobTelemetry)
|
foreach (var telemetryItem in jobTelemetry)
|
||||||
|
|||||||
@@ -299,6 +299,52 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetDirectoryRootReturnsCachedValue()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
|
||||||
|
// Call GetDirectory(Root) twice — should return the same reference
|
||||||
|
var root1 = _hc.GetDirectory(WellKnownDirectory.Root);
|
||||||
|
var root2 = _hc.GetDirectory(WellKnownDirectory.Root);
|
||||||
|
|
||||||
|
Assert.NotNull(root1);
|
||||||
|
Assert.Equal(root1, root2);
|
||||||
|
Assert.True(Directory.Exists(root1));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetDirectoryDerivedPathsUseRootCasing()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Setup();
|
||||||
|
|
||||||
|
var root = _hc.GetDirectory(WellKnownDirectory.Root);
|
||||||
|
var diag = _hc.GetDirectory(WellKnownDirectory.Diag);
|
||||||
|
var externals = _hc.GetDirectory(WellKnownDirectory.Externals);
|
||||||
|
|
||||||
|
// Diag and Externals should start with the same Root prefix
|
||||||
|
Assert.StartsWith(root, diag);
|
||||||
|
Assert.StartsWith(root, externals);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Setup([CallerMemberName] string testName = "")
|
private void Setup([CallerMemberName] string testName = "")
|
||||||
{
|
{
|
||||||
_tokenSource = new CancellationTokenSource();
|
_tokenSource = new CancellationTokenSource();
|
||||||
|
|||||||
151
src/Test/L0/Util/PathUtilL0.cs
Normal file
151
src/Test/L0/Util/PathUtilL0.cs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Common.Tests.Util
|
||||||
|
{
|
||||||
|
public sealed class PathUtilL0
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_ReturnsPath_WhenDirectoryDoesNotExist()
|
||||||
|
{
|
||||||
|
var fakePath = Path.Combine(Path.GetTempPath(), "nonexistent_" + Path.GetRandomFileName());
|
||||||
|
var result = PathUtil.GetCanonicalPath(fakePath);
|
||||||
|
Assert.Equal(fakePath, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_ReturnsPath_WhenNull()
|
||||||
|
{
|
||||||
|
Assert.Null(PathUtil.GetCanonicalPath(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_ReturnsEmpty_WhenEmpty()
|
||||||
|
{
|
||||||
|
Assert.Equal(string.Empty, PathUtil.GetCanonicalPath(string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_ReturnsValidPath_ForExistingDirectory()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), "pathutil_test_" + Path.GetRandomFileName());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
var result = PathUtil.GetCanonicalPath(tempDir);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.True(Directory.Exists(result));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (Directory.Exists(tempDir))
|
||||||
|
{
|
||||||
|
Directory.Delete(tempDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if OS_WINDOWS
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_NormalizesDriveLetter_OnWindows()
|
||||||
|
{
|
||||||
|
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
// Skip if temp is a UNC path (no drive letter to normalize)
|
||||||
|
if (tempDir.StartsWith(@"\\"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force lowercase drive letter
|
||||||
|
var lowerCased = char.ToLower(tempDir[0]) + tempDir.Substring(1);
|
||||||
|
|
||||||
|
var result = PathUtil.GetCanonicalPath(lowerCased);
|
||||||
|
|
||||||
|
// The canonical path should have an uppercase drive letter
|
||||||
|
Assert.True(char.IsUpper(result[0]),
|
||||||
|
$"Expected uppercase drive letter but got: {result}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_NormalizesFolderCasing_OnWindows()
|
||||||
|
{
|
||||||
|
// Create a directory with known casing, then query with wrong casing
|
||||||
|
var basePath = Path.GetTempPath();
|
||||||
|
if (basePath.StartsWith(@"\\"))
|
||||||
|
{
|
||||||
|
return; // Skip UNC
|
||||||
|
}
|
||||||
|
|
||||||
|
var realName = "PathUtilTest_MiXeDcAsE_" + Path.GetRandomFileName();
|
||||||
|
var realDir = Path.Combine(basePath, realName);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(realDir);
|
||||||
|
|
||||||
|
// Query with all-lowercase version
|
||||||
|
var wrongCased = Path.Combine(basePath, realName.ToLowerInvariant());
|
||||||
|
|
||||||
|
var result = PathUtil.GetCanonicalPath(wrongCased);
|
||||||
|
|
||||||
|
// The canonical result should contain the original mixed-case name
|
||||||
|
Assert.Contains(realName, result);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (Directory.Exists(realDir))
|
||||||
|
{
|
||||||
|
Directory.Delete(realDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_IsIdempotent_OnWindows()
|
||||||
|
{
|
||||||
|
// Calling GetCanonicalPath twice should return the same result
|
||||||
|
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
|
||||||
|
var first = PathUtil.GetCanonicalPath(tempDir);
|
||||||
|
var second = PathUtil.GetCanonicalPath(first);
|
||||||
|
Assert.Equal(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void GetCanonicalPath_ReturnsSameResult_RegardlessOfInputCasing_OnWindows()
|
||||||
|
{
|
||||||
|
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
|
||||||
|
if (tempDir.StartsWith(@"\\"))
|
||||||
|
{
|
||||||
|
return; // Skip UNC
|
||||||
|
}
|
||||||
|
|
||||||
|
var upper = tempDir.ToUpperInvariant();
|
||||||
|
var lower = tempDir.ToLowerInvariant();
|
||||||
|
|
||||||
|
var resultUpper = PathUtil.GetCanonicalPath(upper);
|
||||||
|
var resultLower = PathUtil.GetCanonicalPath(lower);
|
||||||
|
|
||||||
|
// Both should resolve to the same canonical path
|
||||||
|
Assert.Equal(resultUpper, resultLower);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -744,14 +744,32 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||||
await waitTask;
|
await waitTask;
|
||||||
|
|
||||||
// Complete the job — events are sent via OnJobCompletedAsync
|
// Complete the job — OnJobCompletedAsync pauses when stepping,
|
||||||
await _debugger.OnJobCompletedAsync();
|
// so run it in the background and send continue to unblock.
|
||||||
|
var completedTask = _debugger.OnJobCompletedAsync();
|
||||||
|
|
||||||
var msg1 = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
// Read the stopped event from the pause
|
||||||
var msg2 = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
var stoppedMsg = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||||
|
Assert.Contains("\"event\":\"stopped\"", stoppedMsg);
|
||||||
|
|
||||||
// Both events should arrive (order may vary)
|
// Send continue to unblock the pause
|
||||||
var combined = msg1 + msg2;
|
await SendRequestAsync(stream, new Request
|
||||||
|
{
|
||||||
|
Seq = 2,
|
||||||
|
Type = "request",
|
||||||
|
Command = "continue"
|
||||||
|
});
|
||||||
|
|
||||||
|
await completedTask;
|
||||||
|
|
||||||
|
// Read remaining messages — continue response + continued event + terminated + exited
|
||||||
|
var allMessages = new System.Text.StringBuilder();
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
allMessages.Append(await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var combined = allMessages.ToString();
|
||||||
Assert.Contains("\"event\":\"terminated\"", combined);
|
Assert.Contains("\"event\":\"terminated\"", combined);
|
||||||
Assert.Contains("\"event\":\"exited\"", combined);
|
Assert.Contains("\"event\":\"exited\"", combined);
|
||||||
}
|
}
|
||||||
@@ -809,5 +827,45 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async Task WaitForCommandAsyncUnblocksOnCancellationDuringWait()
|
||||||
|
{
|
||||||
|
using (CreateTestContext())
|
||||||
|
{
|
||||||
|
var port = GetFreePort();
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||||
|
var jobContext = CreateJobContextWithTunnel(cts.Token, port);
|
||||||
|
await _debugger.StartAsync(jobContext.Object);
|
||||||
|
|
||||||
|
var waitTask = _debugger.WaitUntilReadyAsync();
|
||||||
|
using var client = await ConnectClientAsync(port);
|
||||||
|
var stream = client.GetStream();
|
||||||
|
await SendRequestAsync(stream, new Request
|
||||||
|
{
|
||||||
|
Seq = 1,
|
||||||
|
Type = "request",
|
||||||
|
Command = "configurationDone"
|
||||||
|
});
|
||||||
|
|
||||||
|
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||||
|
await waitTask;
|
||||||
|
|
||||||
|
// Start OnJobCompletedAsync — it will pause because _pauseOnNextStep is true
|
||||||
|
var completedTask = _debugger.OnJobCompletedAsync();
|
||||||
|
|
||||||
|
// Read the stopped event
|
||||||
|
var stoppedMsg = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||||
|
Assert.Contains("\"event\":\"stopped\"", stoppedMsg);
|
||||||
|
|
||||||
|
// Cancel the job while waiting — should unblock the pause
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
// OnJobCompletedAsync should complete without hanging
|
||||||
|
var finished = await Task.WhenAny(completedTask, Task.Delay(TimeSpan.FromSeconds(5)));
|
||||||
|
Assert.Equal(completedTask, finished);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -760,5 +760,171 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
Environment.SetEnvironmentVariable("RUNNER_ENVIRONMENT", null);
|
Environment.SetEnvironmentVariable("RUNNER_ENVIRONMENT", null);
|
||||||
Environment.SetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED", null);
|
Environment.SetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async Task DebuggerStartedInSetupJobWhenEnabled()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
var jobExtension = new JobExtension();
|
||||||
|
jobExtension.Initialize(hc);
|
||||||
|
|
||||||
|
// Enable debugger on the message
|
||||||
|
_message.EnableDebugger = true;
|
||||||
|
_message.DebuggerTunnel = new Pipelines.DebuggerTunnelInfo
|
||||||
|
{
|
||||||
|
TunnelId = "test-tunnel",
|
||||||
|
ClusterId = "test-cluster",
|
||||||
|
HostToken = "test-token",
|
||||||
|
Port = 9229
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-initialize the execution context so it picks up debugger config
|
||||||
|
_jobEc = new Runner.Worker.ExecutionContext();
|
||||||
|
_jobEc.Initialize(hc);
|
||||||
|
_jobEc.InitializeJob(_message, _tokenSource.Token);
|
||||||
|
|
||||||
|
// Set up mock debugger
|
||||||
|
var mockDebugger = new Mock<IDapDebugger>();
|
||||||
|
mockDebugger.Setup(x => x.StartAsync(It.IsAny<IExecutionContext>())).Returns(Task.CompletedTask);
|
||||||
|
mockDebugger.Setup(x => x.WaitUntilReadyAsync()).Returns(Task.CompletedTask);
|
||||||
|
hc.SetSingleton(mockDebugger.Object);
|
||||||
|
|
||||||
|
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
|
||||||
|
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>(), new Dictionary<Guid, IActionRunner>())));
|
||||||
|
|
||||||
|
List<IStep> result = await jobExtension.InitializeJob(_jobEc, _message);
|
||||||
|
|
||||||
|
// Verify DAP debugger was started and waited on
|
||||||
|
mockDebugger.Verify(x => x.StartAsync(It.IsAny<IExecutionContext>()), Times.Once);
|
||||||
|
mockDebugger.Verify(x => x.WaitUntilReadyAsync(), Times.Once);
|
||||||
|
|
||||||
|
// Verify steps are still returned correctly
|
||||||
|
Assert.Equal(5, result.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async Task DebuggerNotStartedInSetupJobWhenDisabled()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
var jobExtension = new JobExtension();
|
||||||
|
jobExtension.Initialize(hc);
|
||||||
|
|
||||||
|
// Debugger NOT enabled on the message — should not be started
|
||||||
|
|
||||||
|
// Set up mock debugger (should NOT be called)
|
||||||
|
var mockDebugger = new Mock<IDapDebugger>();
|
||||||
|
hc.SetSingleton(mockDebugger.Object);
|
||||||
|
|
||||||
|
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
|
||||||
|
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>(), new Dictionary<Guid, IActionRunner>())));
|
||||||
|
|
||||||
|
List<IStep> result = await jobExtension.InitializeJob(_jobEc, _message);
|
||||||
|
|
||||||
|
// Verify DAP debugger was NOT started during setup job
|
||||||
|
mockDebugger.Verify(x => x.StartAsync(It.IsAny<IExecutionContext>()), Times.Never);
|
||||||
|
mockDebugger.Verify(x => x.WaitUntilReadyAsync(), Times.Never);
|
||||||
|
|
||||||
|
Assert.Equal(5, result.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async Task DebuggerCleanedUpInFinalizeJob()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
var jobExtension = new JobExtension();
|
||||||
|
jobExtension.Initialize(hc);
|
||||||
|
|
||||||
|
// Enable debugger on the message
|
||||||
|
_message.EnableDebugger = true;
|
||||||
|
_message.DebuggerTunnel = new Pipelines.DebuggerTunnelInfo
|
||||||
|
{
|
||||||
|
TunnelId = "test-tunnel",
|
||||||
|
ClusterId = "test-cluster",
|
||||||
|
HostToken = "test-token",
|
||||||
|
Port = 9229
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-initialize the execution context so it picks up debugger config
|
||||||
|
_jobEc = new Runner.Worker.ExecutionContext();
|
||||||
|
_jobEc.Initialize(hc);
|
||||||
|
_jobEc.InitializeJob(_message, _tokenSource.Token);
|
||||||
|
|
||||||
|
// Set up mock debugger
|
||||||
|
var mockDebugger = new Mock<IDapDebugger>();
|
||||||
|
mockDebugger.Setup(x => x.StartAsync(It.IsAny<IExecutionContext>())).Returns(Task.CompletedTask);
|
||||||
|
mockDebugger.Setup(x => x.WaitUntilReadyAsync()).Returns(Task.CompletedTask);
|
||||||
|
mockDebugger.Setup(x => x.OnJobCompletedAsync()).Returns(Task.CompletedTask);
|
||||||
|
hc.SetSingleton(mockDebugger.Object);
|
||||||
|
|
||||||
|
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
|
||||||
|
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>(), new Dictionary<Guid, IActionRunner>())));
|
||||||
|
|
||||||
|
// Run InitializeJob to start the debugger
|
||||||
|
await jobExtension.InitializeJob(_jobEc, _message);
|
||||||
|
|
||||||
|
// Run FinalizeJob — should pause (inside OnJobCompletedAsync) then clean up
|
||||||
|
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||||
|
|
||||||
|
// Verify OnJobCompletedAsync was called (it handles pause + cleanup)
|
||||||
|
mockDebugger.Verify(x => x.OnJobCompletedAsync(), Times.Once);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async Task FinalizeJobHandlesDebuggerCleanupException()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
var jobExtension = new JobExtension();
|
||||||
|
jobExtension.Initialize(hc);
|
||||||
|
|
||||||
|
// Enable debugger on the message
|
||||||
|
_message.EnableDebugger = true;
|
||||||
|
_message.DebuggerTunnel = new Pipelines.DebuggerTunnelInfo
|
||||||
|
{
|
||||||
|
TunnelId = "test-tunnel",
|
||||||
|
ClusterId = "test-cluster",
|
||||||
|
HostToken = "test-token",
|
||||||
|
Port = 9229
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-initialize the execution context so it picks up debugger config
|
||||||
|
_jobEc = new Runner.Worker.ExecutionContext();
|
||||||
|
_jobEc.Initialize(hc);
|
||||||
|
_jobEc.InitializeJob(_message, _tokenSource.Token);
|
||||||
|
|
||||||
|
// Set up mock debugger — OnJobCompletedAsync throws
|
||||||
|
var mockDebugger = new Mock<IDapDebugger>();
|
||||||
|
mockDebugger.Setup(x => x.StartAsync(It.IsAny<IExecutionContext>())).Returns(Task.CompletedTask);
|
||||||
|
mockDebugger.Setup(x => x.WaitUntilReadyAsync()).Returns(Task.CompletedTask);
|
||||||
|
mockDebugger.Setup(x => x.OnJobCompletedAsync()).ThrowsAsync(new InvalidOperationException("tunnel disposed"));
|
||||||
|
mockDebugger.Setup(x => x.StopAsync()).Returns(Task.CompletedTask);
|
||||||
|
hc.SetSingleton(mockDebugger.Object);
|
||||||
|
|
||||||
|
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
|
||||||
|
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>(), new Dictionary<Guid, IActionRunner>())));
|
||||||
|
|
||||||
|
await jobExtension.InitializeJob(_jobEc, _message);
|
||||||
|
|
||||||
|
// FinalizeJob should not throw even when OnJobCompletedAsync throws
|
||||||
|
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||||
|
|
||||||
|
mockDebugger.Verify(x => x.OnJobCompletedAsync(), Times.Once);
|
||||||
|
mockDebugger.Verify(x => x.StopAsync(), Times.Once);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user