diff --git a/docs/checks/nodejs.md b/docs/checks/nodejs.md index 62b2958cd95..fb842168c27 100644 --- a/docs/checks/nodejs.md +++ b/docs/checks/nodejs.md @@ -8,6 +8,8 @@ The runner carries its own copies of node.js executables under `/ex All javascript base Actions will get executed by the built-in `node` at either `/externals/node20/` or `/externals/node24/` depending on the version specified in the action's metadata. +The runner's own internal operations (for example `hashFiles()` and this check) use `/externals/node24/`. Set `ACTIONS_RUNNER_FORCED_INTERNAL_NODE_VERSION=node20` to use `node20` instead. + > Not the `node` from `$PATH` ## What is checked? diff --git a/images/Dockerfile b/images/Dockerfile index 0d063f6e474..2ff6cfb14fd 100644 --- a/images/Dockerfile +++ b/images/Dockerfile @@ -5,8 +5,8 @@ ARG TARGETOS ARG TARGETARCH ARG RUNNER_VERSION ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0 -ARG DOCKER_VERSION=29.8.1 -ARG BUILDX_VERSION=0.37.1 +ARG DOCKER_VERSION=29.8.2 +ARG BUILDX_VERSION=0.37.2 RUN apt update -y && apt install curl unzip -y diff --git a/patches/last_processed_commit.txt b/patches/last_processed_commit.txt index efc64e34d0c..e85c64ac0c7 100644 --- a/patches/last_processed_commit.txt +++ b/patches/last_processed_commit.txt @@ -1 +1 @@ -a634d7339463392169ca6eea270e36b66c68e9a3 +539695a10a5b0fe3af98990525044236bfa67a75 diff --git a/patches/runner-main-sdk8-arm.patch b/patches/runner-main-sdk8-arm.patch index 62abd8d2af1..bccf7324f15 100644 --- a/patches/runner-main-sdk8-arm.patch +++ b/patches/runner-main-sdk8-arm.patch @@ -44,7 +44,7 @@ index 4ed18b2b..e0c88fbc 100644 win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64;osx-arm64;win-arm64 true diff --git a/src/Runner.Common/Util/NodeUtil.cs b/src/Runner.Common/Util/NodeUtil.cs -index d87224f9..07d822fb 100644 +index 7b8055b9..b4683559 100644 --- a/src/Runner.Common/Util/NodeUtil.cs +++ b/src/Runner.Common/Util/NodeUtil.cs @@ -144,7 +144,7 @@ namespace GitHub.Runner.Common.Util @@ -157,7 +157,7 @@ index 03290778..efd2b8cd 100644 win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64;osx-arm64;win-arm64 true diff --git a/src/Sdk/Sdk.csproj b/src/Sdk/Sdk.csproj -index 5db8c10e..49bc0143 100644 +index e6ba57af..0b5b1d62 100644 --- a/src/Sdk/Sdk.csproj +++ b/src/Sdk/Sdk.csproj @@ -1,7 +1,7 @@ @@ -279,4 +279,4 @@ index 5821cb36..70cfe5c2 100644 + "version": "9.0.318" } } -# From upstream commit: a634d7339463392169ca6eea270e36b66c68e9a3 +# From upstream commit: 539695a10a5b0fe3af98990525044236bfa67a75 diff --git a/patches/runner-main-sdk8-ppc64le.patch b/patches/runner-main-sdk8-ppc64le.patch index 3f07222472e..50117a4f188 100644 --- a/patches/runner-main-sdk8-ppc64le.patch +++ b/patches/runner-main-sdk8-ppc64le.patch @@ -127,7 +127,7 @@ index 97273a1a..78f78701 100644 throw new NotSupportedException(); // Should never reach here. } diff --git a/src/Sdk/Sdk.csproj b/src/Sdk/Sdk.csproj -index 5db8c10e..7c4ba7ac 100644 +index e6ba57af..fc8b3d34 100644 --- a/src/Sdk/Sdk.csproj +++ b/src/Sdk/Sdk.csproj @@ -4,7 +4,7 @@ @@ -284,4 +284,4 @@ index 056a312e..3f9a3679 100644 -# From upstream commit: a634d7339463392169ca6eea270e36b66c68e9a3 +# From upstream commit: 539695a10a5b0fe3af98990525044236bfa67a75 diff --git a/patches/runner-main-sdk8-s390x.patch b/patches/runner-main-sdk8-s390x.patch index 3f07222472e..50117a4f188 100644 --- a/patches/runner-main-sdk8-s390x.patch +++ b/patches/runner-main-sdk8-s390x.patch @@ -127,7 +127,7 @@ index 97273a1a..78f78701 100644 throw new NotSupportedException(); // Should never reach here. } diff --git a/src/Sdk/Sdk.csproj b/src/Sdk/Sdk.csproj -index 5db8c10e..7c4ba7ac 100644 +index e6ba57af..fc8b3d34 100644 --- a/src/Sdk/Sdk.csproj +++ b/src/Sdk/Sdk.csproj @@ -4,7 +4,7 @@ @@ -284,4 +284,4 @@ index 056a312e..3f9a3679 100644 -# From upstream commit: a634d7339463392169ca6eea270e36b66c68e9a3 +# From upstream commit: 539695a10a5b0fe3af98990525044236bfa67a75 diff --git a/src/Misc/expressionFunc/hashFiles/package-lock.json b/src/Misc/expressionFunc/hashFiles/package-lock.json index 66d123e48a4..6b07d6a38e2 100644 --- a/src/Misc/expressionFunc/hashFiles/package-lock.json +++ b/src/Misc/expressionFunc/hashFiles/package-lock.json @@ -72,9 +72,10 @@ } }, "node_modules/@actions/glob/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -1116,10 +1117,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -1471,10 +1473,11 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4973,9 +4976,9 @@ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==" }, "brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "requires": { "balanced-match": "^4.0.2" } @@ -5590,9 +5593,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "requires": { "balanced-match": "^4.0.2" @@ -5829,9 +5832,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "requires": { "balanced-match": "^1.0.0", diff --git a/src/Misc/layoutbin/runsvc.sh b/src/Misc/layoutbin/runsvc.sh index 63d1b62e1a6..35e44227cb8 100755 --- a/src/Misc/layoutbin/runsvc.sh +++ b/src/Misc/layoutbin/runsvc.sh @@ -10,7 +10,7 @@ if [ -f ".path" ]; then echo ".path=${PATH}" fi -nodever="node20" +nodever="node24" # insert anything to setup env when running as a service # run the host process which keep the listener alive diff --git a/src/Runner.Common/BrokerServer.cs b/src/Runner.Common/BrokerServer.cs index 87747f985f2..8c5e96fc7c5 100644 --- a/src/Runner.Common/BrokerServer.cs +++ b/src/Runner.Common/BrokerServer.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Net.WebSockets; +using System.Text; using System.Threading; using System.Threading.Tasks; using GitHub.Actions.RunService.WebApi; @@ -28,6 +31,9 @@ public interface IBrokerServer : IRunnerService Task UpdateConnectionIfNeeded(Uri serverUri, VssCredentials credentials); Task ForceRefreshConnection(VssCredentials credentials); + + // Temporary probe: holds/reconnects a websocket to the broker listener until cancelled. + Task RunLongPollWebSocketProbeAsync(CancellationToken cancellationToken); } public sealed class BrokerServer : RunnerService, IBrokerServer @@ -37,6 +43,9 @@ public sealed class BrokerServer : RunnerService, IBrokerServer private RawConnection _connection; private BrokerHttpClient _brokerHttpClient; + private static readonly TimeSpan MinDelayForWebSocketReconnect = TimeSpan.FromSeconds(10); + private static readonly TimeSpan MaxDelayForWebSocketReconnect = TimeSpan.FromSeconds(300); + public async Task ConnectAsync(Uri serverUri, VssCredentials credentials) { Trace.Entering(); @@ -80,6 +89,111 @@ public async Task AcknowledgeRunnerRequestAsync(string runnerRequestId, Guid? se await _brokerHttpClient.AcknowledgeRunnerRequestAsync(runnerRequestId, sessionId, version, status, os, architecture, cancellationToken); } + public async Task RunLongPollWebSocketProbeAsync(CancellationToken cancellationToken) + { + CheckConnection(); + + var result = new BrokerWebSocketProbeResult(); + var stopwatch = Stopwatch.StartNew(); + + while (!cancellationToken.IsCancellationRequested) + { + var socket = await ConnectWebSocketAsync(cancellationToken); + if (socket == null) + { + result.ConnectFailures++; + result.LastCloseReason = "connect_failed"; + + var delay = BackoffTimerHelper.GetRandomBackoff(MinDelayForWebSocketReconnect, MaxDelayForWebSocketReconnect); + try + { + await Task.Delay(delay, cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + + continue; + } + + result.Connected = true; + result.ConnectCount++; + + using (socket) + { + var buffer = new byte[4096]; + try + { + while (socket.State == WebSocketState.Open) + { + var receiveResult = await socket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + if (receiveResult.MessageType == WebSocketMessageType.Close) + { + result.LastCloseReason = $"server_closed:{receiveResult.CloseStatus}:{receiveResult.CloseStatusDescription}"; + Trace.Info($"Runner websocket closed by server. CloseStatus: {receiveResult.CloseStatus}, Description: {receiveResult.CloseStatusDescription}"); + await socket.CloseOutputAsync(receiveResult.CloseStatus.Value, "Closing websocket", cancellationToken); + break; + } + + result.PingsReceived++; + Trace.Info($"Runner websocket received a ping: " + $"{Encoding.UTF8.GetString(buffer, 0, receiveResult.Count)}"); + + if (receiveResult.MessageType == WebSocketMessageType.Text) + { + await socket.SendAsync( + new ArraySegment(buffer, 0, receiveResult.Count), + receiveResult.MessageType, + receiveResult.EndOfMessage, + cancellationToken); + Trace.Info($"Runner replied via websocket with: " + $"{Encoding.UTF8.GetString(buffer, 0, receiveResult.Count)}"); + } + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + result.LastCloseReason = "job_completed"; + } + catch (Exception ex) + { + Trace.Info("Exception caught while holding runner websocket, will reconnect."); + Trace.Error(ex); + if (!result.Errors.Contains(ex.Message)) + { + result.Errors.Add(ex.Message); + } + result.LastCloseReason = "error"; + } + + if (socket.State == WebSocketState.Open) + { + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing websocket", CancellationToken.None); + } + } + } + + stopwatch.Stop(); + result.TotalDurationMs = stopwatch.ElapsedMilliseconds; + return result; + } + + private async Task ConnectWebSocketAsync(CancellationToken cancellationToken) + { + try + { + Trace.Info("Attempting to start runner websocket client."); + var socket = await _brokerHttpClient.ConnectRunnerLongPollWebSocketAsync(cancellationToken); + Trace.Info("Successfully started runner websocket client."); + return socket; + } + catch (Exception ex) + { + Trace.Info("Exception caught during runner websocket connect, will retry."); + Trace.Error(ex); + return null; + } + } + public async Task DeleteSessionAsync(Guid sessionId, CancellationToken cancellationToken) { CheckConnection(); diff --git a/src/Runner.Common/Util/NodeUtil.cs b/src/Runner.Common/Util/NodeUtil.cs index d87224f9820..7b8055b9c01 100644 --- a/src/Runner.Common/Util/NodeUtil.cs +++ b/src/Runner.Common/Util/NodeUtil.cs @@ -28,8 +28,8 @@ private class EnvironmentVariableInfo public bool FromSystem { get; set; } } - private const string _defaultNodeVersion = "node20"; - public static readonly ReadOnlyCollection BuiltInNodeVersions = new(new[] { "node20" }); + private const string _defaultNodeVersion = "node24"; + public static readonly ReadOnlyCollection BuiltInNodeVersions = new(new[] { "node20", "node24" }); public static string GetInternalNodeVersion() { var forcedInternalNodeVersion = Environment.GetEnvironmentVariable(Constants.Variables.Agent.ForcedInternalNodeVersion); diff --git a/src/Runner.Sdk/RunnerWebProxy.cs b/src/Runner.Sdk/RunnerWebProxy.cs index 4c3c92a5502..1829aaa7fec 100644 --- a/src/Runner.Sdk/RunnerWebProxy.cs +++ b/src/Runner.Sdk/RunnerWebProxy.cs @@ -185,7 +185,7 @@ public Uri GetProxy(Uri destination) return null; } - if (destination.Scheme == Uri.UriSchemeHttps) + if (destination.Scheme == Uri.UriSchemeHttps || destination.Scheme == Uri.UriSchemeWss) { return new Uri(_httpsProxyAddress); } @@ -197,12 +197,12 @@ public Uri GetProxy(Uri destination) public bool IsBypassed(Uri uri) { - if (uri.Scheme == Uri.UriSchemeHttps && string.IsNullOrEmpty(_httpsProxyAddress)) + if ((uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeWss) && string.IsNullOrEmpty(_httpsProxyAddress)) { return true; } - if (uri.Scheme == Uri.UriSchemeHttp && string.IsNullOrEmpty(_httpProxyAddress)) + if ((uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeWs) && string.IsNullOrEmpty(_httpProxyAddress)) { return true; } diff --git a/src/Runner.Sdk/Util/VssUtil.cs b/src/Runner.Sdk/Util/VssUtil.cs index 012d27f7345..b2d046d2417 100644 --- a/src/Runner.Sdk/Util/VssUtil.cs +++ b/src/Runner.Sdk/Util/VssUtil.cs @@ -1,14 +1,14 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Net; using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.InteropServices; using GitHub.DistributedTask.WebApi; using GitHub.Services.Common; -using GitHub.Services.WebApi; using GitHub.Services.OAuth; -using System.Net.Http.Headers; -using System.Runtime.InteropServices; -using System.Net; +using GitHub.Services.WebApi; using Sdk.WebApi.WebApi.RawClient; namespace GitHub.Runner.Sdk @@ -34,6 +34,7 @@ public static void InitializeVssClientSettings(List addi VssClientHttpRequestSettings.Default.UserAgent = headerValues; VssHttpMessageHandler.DefaultWebProxy = proxy; + RawHttpMessageHandler.DefaultWebProxy = proxy; if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY"))) { diff --git a/src/Runner.Worker/JobExtension.cs b/src/Runner.Worker/JobExtension.cs index 1705f749f2f..d3d5fb7b0e2 100644 --- a/src/Runner.Worker/JobExtension.cs +++ b/src/Runner.Worker/JobExtension.cs @@ -53,6 +53,8 @@ public sealed class JobExtension : RunnerService, IJobExtension private Task _diskSpaceCheckTask = null; private CancellationTokenSource _serviceConnectivityCheckToken = new(); private Task _serviceConnectivityCheckTask = null; + private CancellationTokenSource _brokerWebSocketProbeToken = new(); + private Task _brokerWebSocketProbeTask = null; private IDapDebugger _dapDebugger; // Download all required actions. @@ -639,6 +641,15 @@ public async Task> InitializeJob(IExecutionContext jobContext, Pipel Trace.Info($"Start checking service connectivity in background."); _serviceConnectivityCheckTask = CheckServiceConnectivityAsync(context, _serviceConnectivityCheckToken.Token); + // Temporary probe: gauge websocket compatibility with the broker listener, + // only when run-service sends down a probe URL via the job message. + var brokerWebSocketProbeUrl = context.Global.Variables?.Get(WellKnownDistributedTaskVariables.RunnerBrokerWebSocketProbeUrl); + if (!string.IsNullOrEmpty(brokerWebSocketProbeUrl)) + { + Trace.Info($"Start checking runner websocket connectivity in background."); + _brokerWebSocketProbeTask = CheckBrokerLongPollWebSocketAsync(brokerWebSocketProbeUrl, message, _brokerWebSocketProbeToken.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) @@ -991,6 +1002,28 @@ public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRe } } + // Collect runner websocket probe result + if (_brokerWebSocketProbeTask != null) + { + _brokerWebSocketProbeToken.Cancel(); + try + { + var probeResult = await _brokerWebSocketProbeTask; + if (probeResult != null) + { + var telemetryData = StringUtil.ConvertToJson(probeResult, Formatting.None); + Trace.Info($"Runner websocket probe result: {telemetryData}"); + context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.ConnectivityCheck, Message = $"broker_websocket_telemetry:{telemetryData}" }); + } + } + catch (Exception ex) + { + Trace.Error($"Fail to check runner websocket connectivity."); + Trace.Error(ex); + context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.ConnectivityCheck, Message = $"Fail to check runner websocket connectivity. {ex.Message}" }); + } + } + // Read dates from server variables with hardcoded fallbacks var node24DefaultDateRaw = context.Global.Variables?.Get(Constants.Runner.NodeMigration.Node24DefaultDateVariable); var node24DefaultDate = string.IsNullOrEmpty(node24DefaultDateRaw) ? Constants.Runner.NodeMigration.Node24DefaultDate : node24DefaultDateRaw; @@ -1260,6 +1293,35 @@ private async Task CheckServiceConnectivityAsync(IExecutionContext context, Canc context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.ConnectivityCheck, Message = telemetryData }); } + // Temporary probe: connects to the broker listener websocket using the job's + // SystemVssConnection credential and holds/reconnects until cancelled. Never throws. + private async Task CheckBrokerLongPollWebSocketAsync( + string probeUrl, + Pipelines.AgentJobRequestMessage message, + CancellationToken token) + { + try + { + ServiceEndpoint systemConnection = message.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase)); + VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection); + + var brokerServer = HostContext.CreateService(); + await brokerServer.ConnectAsync(new Uri(probeUrl), jobServerCredential); + + return await brokerServer.RunLongPollWebSocketProbeAsync(token); + } + catch (Exception ex) + { + Trace.Info("Exception caught while setting up runner websocket probe."); + Trace.Error(ex); + return new BrokerWebSocketProbeResult + { + Connected = false, + Errors = new List { ex.Message }, + }; + } + } + private Dictionary SnapshotProcesses() { Dictionary snapshot = new(); diff --git a/src/Sdk/DTWebApi/WebApi/BrokerWebSocketProbeResult.cs b/src/Sdk/DTWebApi/WebApi/BrokerWebSocketProbeResult.cs new file mode 100644 index 00000000000..5b2393e0349 --- /dev/null +++ b/src/Sdk/DTWebApi/WebApi/BrokerWebSocketProbeResult.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Newtonsoft.Json; + +namespace GitHub.DistributedTask.WebApi +{ + [DataContract] + public class BrokerWebSocketProbeResult + { + [JsonConstructor] + public BrokerWebSocketProbeResult() + { + Errors = new List(); + } + + [DataMember(Order = 1, EmitDefaultValue = true)] + public bool Connected { get; set; } + + [DataMember(Order = 2, EmitDefaultValue = false)] + public int ConnectCount { get; set; } + + [DataMember(Order = 3, EmitDefaultValue = false)] + public int ConnectFailures { get; set; } + + [DataMember(Order = 4, EmitDefaultValue = false)] + public int PingsReceived { get; set; } + + [DataMember(Order = 5, EmitDefaultValue = false)] + public string LastCloseReason { get; set; } + + [DataMember(Order = 6, EmitDefaultValue = false)] + public long TotalDurationMs { get; set; } + + [DataMember(Order = 7, EmitDefaultValue = false)] + public List Errors { get; set; } + } +} diff --git a/src/Sdk/DTWebApi/WebApi/WellKnownDistributedTaskVariables.cs b/src/Sdk/DTWebApi/WebApi/WellKnownDistributedTaskVariables.cs index a4ea950de1e..031316a078d 100644 --- a/src/Sdk/DTWebApi/WebApi/WellKnownDistributedTaskVariables.cs +++ b/src/Sdk/DTWebApi/WebApi/WellKnownDistributedTaskVariables.cs @@ -8,5 +8,6 @@ public static class WellKnownDistributedTaskVariables public static readonly String RunnerLowDiskspaceThreshold = "system.runner.lowdiskspacethreshold"; public static readonly String RunnerEnvironment = "system.runnerEnvironment"; public static readonly String RunnerServiceConnectivityTest = "system.runner.serviceconnectivitycheckinput"; + public static readonly String RunnerBrokerWebSocketProbeUrl = "system.runner.brokerwebsocketprobeurl"; } } diff --git a/src/Sdk/Sdk.csproj b/src/Sdk/Sdk.csproj index 5db8c10e457..e6ba57affae 100644 --- a/src/Sdk/Sdk.csproj +++ b/src/Sdk/Sdk.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/Sdk/WebApi/WebApi/BrokerHttpClient.cs b/src/Sdk/WebApi/WebApi/BrokerHttpClient.cs index 355a0503929..b99383a2e06 100644 --- a/src/Sdk/WebApi/WebApi/BrokerHttpClient.cs +++ b/src/Sdk/WebApi/WebApi/BrokerHttpClient.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; +using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using GitHub.DistributedTask.Pipelines; @@ -166,6 +167,27 @@ public async Task CreateSessionAsync( throw new Exception($"Failed to create broker session: {result.Error}"); } + // Temporary probe: validates websocket connectivity to the broker listener + // using the same authenticated pipeline (Client) as other broker calls. + // Client.BaseAddress is the full probe URL (including the .sock path) + // sent down by run-service. + public async Task ConnectRunnerLongPollWebSocketAsync( + CancellationToken cancellationToken = default) + { + var socket = new ClientWebSocket(); + try + { + await socket.ConnectAsync(Client.BaseAddress, Client, cancellationToken); + } + catch + { + socket.Dispose(); + throw; + } + + return socket; + } + public async Task DeleteSessionAsync( Guid sessionId, CancellationToken cancellationToken = default) diff --git a/src/Test/L0/Util/NodeUtilL0.cs b/src/Test/L0/Util/NodeUtilL0.cs index 599d235145e..0a493c739d4 100644 --- a/src/Test/L0/Util/NodeUtilL0.cs +++ b/src/Test/L0/Util/NodeUtilL0.cs @@ -8,6 +8,27 @@ namespace GitHub.Runner.Common.Tests.Util { public class NodeUtilL0 { + [Theory] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + [InlineData(null, "node24")] + [InlineData("node24", "node24")] + [InlineData("node20", "node20")] + [InlineData("node16", "node24")] // Unsupported versions are ignored + public void TestInternalNodeVersion(string forcedVersion, string expectedVersion) + { + try + { + Environment.SetEnvironmentVariable(Constants.Variables.Agent.ForcedInternalNodeVersion, forcedVersion); + + Assert.Equal(expectedVersion, NodeUtil.GetInternalNodeVersion()); + } + finally + { + Environment.SetEnvironmentVariable(Constants.Variables.Agent.ForcedInternalNodeVersion, null); + } + } + // We're testing the logic with feature flags [Theory] [InlineData(false, false, false, false, "node20", false)] // Phase 1: No env vars