Skip to content

Commit 31d4034

Browse files
rumyaxkirill-ivlev
andauthored
Fallback from Node 20 to Node 16 (#4987)
Co-authored-by: Kirill Ivlev <102740624+kirill-ivlev@users.noreply.github.com>
1 parent 6b8de4d commit 31d4034

File tree

6 files changed

+137
-73
lines changed

6 files changed

+137
-73
lines changed

src/Agent.Sdk/ContainerInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public ContainerInfo(Pipelines.ContainerResource container, Boolean isJobContain
8383
public string ContainerName { get; set; }
8484
public string ContainerCommand { get; set; }
8585
public string CustomNodePath { get; set; }
86+
public string ResultNodePath { get; set; }
8687
public Guid ContainerRegistryEndpoint { get; private set; }
8788
public string ContainerCreateOptions { get; set; }
8889
public bool SkipContainerImagePull { get; private set; }

src/Agent.Sdk/ExecutionTargetInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public interface ExecutionTargetInfo
88
{
99
PlatformUtil.OS ExecutionOS { get; }
1010
string CustomNodePath { get; set; }
11+
string ResultNodePath { get; set; }
1112

1213
string TranslateContainerPathForImageOS(PlatformUtil.OS runningOs, string path);
1314
string TranslateToContainerPath(string path);
@@ -18,6 +19,7 @@ public class HostInfo : ExecutionTargetInfo
1819
{
1920
public PlatformUtil.OS ExecutionOS => PlatformUtil.HostOS;
2021
public string CustomNodePath { get; set; }
22+
public string ResultNodePath { get; set; }
2123

2224
public string TranslateToContainerPath(string path)
2325
{

src/Agent.Worker/Container/DockerCommandManager.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public interface IDockerCommandManager : IAgentService
2626
Task<string> DockerCreate(IExecutionContext context, ContainerInfo container);
2727
Task<int> DockerStart(IExecutionContext context, string containerId);
2828
Task<int> DockerLogs(IExecutionContext context, string containerId);
29+
Task<List<string>> GetDockerLogs(IExecutionContext context, string containerId);
2930
Task<List<string>> DockerPS(IExecutionContext context, string options);
3031
Task<int> DockerRemove(IExecutionContext context, string containerId);
3132
Task<int> DockerNetworkCreate(IExecutionContext context, string network);
@@ -229,6 +230,14 @@ public async Task<int> DockerLogs(IExecutionContext context, string containerId)
229230
return await ExecuteDockerCommandAsync(context, "logs", $"--details {containerId}", context.CancellationToken);
230231
}
231232

233+
public async Task<List<string>> GetDockerLogs(IExecutionContext context, string containerId)
234+
{
235+
ArgUtil.NotNull(context, nameof(context));
236+
ArgUtil.NotNull(containerId, nameof(containerId));
237+
238+
return await ExecuteDockerCommandAsync(context, "logs", $"--details {containerId}");
239+
}
240+
232241
public async Task<List<string>> DockerPS(IExecutionContext context, string options)
233242
{
234243
ArgUtil.NotNull(context, nameof(context));

src/Agent.Worker/ContainerOperationProvider.cs

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -525,37 +525,58 @@ private async Task StartContainerAsync(IExecutionContext executionContext, Conta
525525
container.MountVolumes.Add(new MountVolume(taskKeyFile, container.TranslateToContainerPath(taskKeyFile)));
526526
}
527527

528+
bool useNode20ToStartContainer = AgentKnobs.UseNode20ToStartContainer.GetValue(executionContext).AsBoolean();
529+
bool useAgentNode = false;
530+
531+
string labelContainerStartupUsingNode20 = "container-startup-using-node-20";
532+
string labelContainerStartupUsingNode16 = "container-startup-using-node-16";
533+
string labelContainerStartupFailed = "container-startup-failed";
534+
535+
string containerNodePath(string nodeFolder)
536+
{
537+
return container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeFolder, "bin", $"node{IOUtil.ExeExtension}"));
538+
}
539+
540+
string nodeContainerPath = containerNodePath(NodeHandler.NodeFolder);
541+
string node16ContainerPath = containerNodePath(NodeHandler.Node16Folder);
542+
string node20ContainerPath = containerNodePath(NodeHandler.Node20_1Folder);
543+
528544
if (container.IsJobContainer)
529545
{
530546
// See if this container brings its own Node.js
531547
container.CustomNodePath = await _dockerManger.DockerInspect(context: executionContext,
532548
dockerObject: container.ContainerImage,
533549
options: $"--format=\"{{{{index .Config.Labels \\\"{_nodeJsPathLabel}\\\"}}}}\"");
534550

535-
string node;
551+
string nodeSetInterval(string node)
552+
{
553+
return $"'{node}' -e 'setInterval(function(){{}}, 24 * 60 * 60 * 1000);'";
554+
}
555+
556+
string useDoubleQuotes(string value)
557+
{
558+
return value.Replace('\'', '"');
559+
}
560+
536561
if (!string.IsNullOrEmpty(container.CustomNodePath))
537562
{
538-
node = container.CustomNodePath;
563+
container.ContainerCommand = useDoubleQuotes(nodeSetInterval(container.CustomNodePath));
564+
container.ResultNodePath = container.CustomNodePath;
565+
}
566+
else if (PlatformUtil.RunningOnMacOS || (PlatformUtil.RunningOnWindows && container.ImageOS == PlatformUtil.OS.Linux))
567+
{
568+
// require container to have node if running on macOS, or if running on Windows and attempting to run Linux container
569+
container.CustomNodePath = "node";
570+
container.ContainerCommand = useDoubleQuotes(nodeSetInterval(container.CustomNodePath));
571+
container.ResultNodePath = container.CustomNodePath;
539572
}
540573
else
541574
{
542-
node = container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), AgentKnobs.UseNode20ToStartContainer.GetValue(executionContext).AsBoolean() ? NodeHandler.Node20_1Folder : NodeHandler.NodeFolder, "bin", $"node{IOUtil.ExeExtension}"));
543-
544-
// if on Mac OS X, require container to have node
545-
if (PlatformUtil.RunningOnMacOS)
546-
{
547-
container.CustomNodePath = "node";
548-
node = container.CustomNodePath;
549-
}
550-
// if running on Windows, and attempting to run linux container, require container to have node
551-
else if (PlatformUtil.RunningOnWindows && container.ImageOS == PlatformUtil.OS.Linux)
552-
{
553-
container.CustomNodePath = "node";
554-
node = container.CustomNodePath;
555-
}
575+
useAgentNode = true;
576+
string sleepCommand = useNode20ToStartContainer ? $"'{node20ContainerPath}' --version && echo '{labelContainerStartupUsingNode20}' && {nodeSetInterval(node20ContainerPath)} || '{node16ContainerPath}' --version && echo '{labelContainerStartupUsingNode16}' && {nodeSetInterval(node16ContainerPath)} || echo '{labelContainerStartupFailed}'" : nodeSetInterval(nodeContainerPath);
577+
container.ContainerCommand = PlatformUtil.RunningOnWindows ? $"cmd.exe /c call {useDoubleQuotes(sleepCommand)}" : $"bash -c \"{sleepCommand}\"";
578+
container.ResultNodePath = nodeContainerPath;
556579
}
557-
string sleepCommand = $"\"{node}\" -e \"setInterval(function(){{}}, 24 * 60 * 60 * 1000);\"";
558-
container.ContainerCommand = sleepCommand;
559580
}
560581

561582
container.ContainerId = await _dockerManger.DockerCreate(executionContext, container);
@@ -588,6 +609,56 @@ private async Task StartContainerAsync(IExecutionContext executionContext, Conta
588609

589610
executionContext.Warning($"Docker container {container.ContainerId} is not in running state.");
590611
}
612+
else if (useAgentNode && useNode20ToStartContainer)
613+
{
614+
bool containerStartupCompleted = false;
615+
int containerStartupTimeoutInMilliseconds = 10000;
616+
int delayInMilliseconds = 100;
617+
int checksCount = 0;
618+
619+
while (true)
620+
{
621+
List<string> containerLogs = await _dockerManger.GetDockerLogs(executionContext, container.ContainerId);
622+
623+
foreach (string logLine in containerLogs)
624+
{
625+
if (logLine.Contains(labelContainerStartupUsingNode20))
626+
{
627+
executionContext.Debug("Using Node 20 for container startup.");
628+
containerStartupCompleted = true;
629+
container.ResultNodePath = node20ContainerPath;
630+
break;
631+
}
632+
else if (logLine.Contains(labelContainerStartupUsingNode16))
633+
{
634+
executionContext.Warning("Can not run Node 20 in container. Falling back to Node 16 for container startup.");
635+
containerStartupCompleted = true;
636+
container.ResultNodePath = node16ContainerPath;
637+
break;
638+
}
639+
else if (logLine.Contains(labelContainerStartupFailed))
640+
{
641+
executionContext.Error("Can not run both Node 20 and Node 16 in container. Container startup failed.");
642+
containerStartupCompleted = true;
643+
break;
644+
}
645+
}
646+
647+
if (containerStartupCompleted)
648+
{
649+
break;
650+
}
651+
652+
checksCount++;
653+
if (checksCount * delayInMilliseconds > containerStartupTimeoutInMilliseconds)
654+
{
655+
executionContext.Warning("Can not get startup status from container.");
656+
break;
657+
}
658+
659+
await Task.Delay(delayInMilliseconds);
660+
}
661+
}
591662
}
592663
catch (Exception ex)
593664
{

src/Agent.Worker/Handlers/StepHost.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,16 +182,6 @@ public async Task<int> ExecuteAsync(string workingDirectory,
182182
HostContext.GetTrace(nameof(ContainerStepHost)).Info($"Copying containerHandlerInvoker.js to {tempDir}");
183183
File.Copy(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), "containerHandlerInvoker.js.template"), targetEntryScript, true);
184184

185-
string node;
186-
if (!string.IsNullOrEmpty(Container.CustomNodePath))
187-
{
188-
node = Container.CustomNodePath;
189-
}
190-
else
191-
{
192-
node = Container.TranslateToContainerPath(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), "node", "bin", $"node{IOUtil.ExeExtension}"));
193-
}
194-
195185
string entryScript = Container.TranslateContainerPathForImageOS(PlatformUtil.HostOS, Container.TranslateToContainerPath(targetEntryScript));
196186

197187
string userArgs = "";
@@ -209,7 +199,7 @@ public async Task<int> ExecuteAsync(string workingDirectory,
209199
}
210200
}
211201

212-
string containerExecutionArgs = $"exec -i {userArgs} {workingDirectoryParam} {Container.ContainerId} {node} {entryScript}";
202+
string containerExecutionArgs = $"exec -i {userArgs} {workingDirectoryParam} {Container.ContainerId} {Container.ResultNodePath} {entryScript}";
213203

214204
using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
215205
{

src/Misc/layoutbin/containerHandlerInvoker.js.template

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,53 @@
11
const { spawn } = require('child_process');
2-
var stdinString = "";
3-
process.stdin.on('data', function (chunk) {
4-
stdinString += chunk;
5-
});
62

7-
process.stdin.on('end', function () {
8-
var stdinData = JSON.parse(stdinString);
9-
var handler = stdinData.handler;
10-
var handlerArg = stdinData.args;
11-
var handlerWorkDir = stdinData.workDir;
12-
var prependPath = stdinData.prependPath;
3+
const debug = log => console.log(`##vso[task.debug]${log}`);
134

14-
console.log("##vso[task.debug]Handler: " + handler);
15-
console.log("##vso[task.debug]HandlerArg: " + handlerArg);
16-
console.log("##vso[task.debug]HandlerWorkDir: " + handlerWorkDir);
17-
Object.keys(stdinData.environment).forEach(function (key) {
18-
console.log("##vso[task.debug]Set env: " + key + "=" + stdinData.environment[key].toString().replace(/\r/g, '%0D').replace(/\n/g, '%0A'));
19-
process.env[key] = stdinData.environment[key];
20-
});
5+
let stdinString = '';
6+
process.stdin.on('data', chunk => stdinString += chunk);
7+
8+
process.stdin.on('end', () => {
9+
const { handler, args: handlerArg, workDir: handlerWorkDir, prependPath, environment } = JSON.parse(stdinString);
2110

22-
var currentPath = process.env['PATH'];
23-
var options = {
11+
debug(`Handler: ${handler}`);
12+
debug(`HandlerArg: ${handlerArg}`);
13+
debug(`HandlerWorkDir: ${handlerWorkDir}`);
14+
15+
for (const key in environment) {
16+
const value = environment[key].toString().replace(/\r/g, '%0D').replace(/\n/g, '%0A');
17+
debug(`Set env: ${key}=${value}`);
18+
process.env[key] = environment[key];
19+
}
20+
21+
const options = {
2422
stdio: 'inherit',
2523
cwd: handlerWorkDir
2624
};
27-
if (process.platform == 'win32') {
25+
26+
const isWindows = process.platform == 'win32';
27+
28+
if (isWindows) {
2829
options.argv0 = `"${handler}"`;
2930
options.windowsVerbatimArguments = true;
30-
31-
if (prependPath && prependPath.length > 0) {
32-
if (currentPath && currentPath.length > 0) {
33-
process.env['PATH'] = prependPath + ';' + currentPath;
34-
}
35-
else {
36-
process.env['PATH'] = prependPath;
37-
}
38-
}
39-
}
40-
else {
41-
if (prependPath && prependPath.length > 0) {
42-
if (currentPath && currentPath.length > 0) {
43-
process.env['PATH'] = prependPath + ':' + currentPath;
44-
}
45-
else {
46-
process.env['PATH'] = prependPath;
47-
}
48-
}
4931
}
5032

5133
if (prependPath && prependPath.length > 0) {
52-
console.log("##vso[task.debug]Prepend Path: " + process.env['PATH']);
34+
const currentPath = process.env['PATH'];
35+
process.env['PATH'] = prependPath;
36+
37+
if (currentPath && currentPath.length > 0) {
38+
process.env['PATH'] += `${isWindows ? ';' : ':'}${currentPath}`;
39+
}
40+
41+
debug(`Prepend Path: ${process.env['PATH']}`);
5342
}
5443

5544
process.env['TF_BUILD'] = 'True';
56-
console.log("##vso[task.debug]Handler Setup Complete");
57-
var launch = spawn(handler, [handlerArg], options);
58-
launch.on('exit', function (code) {
59-
console.log("##vso[task.debug]Handler exit code: " + code);
45+
debug(`Handler Setup Complete`);
46+
const launch = spawn(handler, [handlerArg], options);
47+
48+
launch.on('exit', code => {
49+
debug(`Handler exit code: ${code}`);
50+
6051
if (code != 0) {
6152
process.exit(code);
6253
}

0 commit comments

Comments
 (0)