L0
ProcessInvokerL0.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using GitHub.Runner.Common.Util;
using System.Threading.Channels;
using GitHub.Runner.Sdk;
using System.Linq;
namespace GitHub.Runner.Common.Tests
{
public sealed clast ProcessInvokerL0
{
#if OS_WINDOWS
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public async Task DefaultsToCurrentSystemOemEncoding()
{
// This test verifies that the additional code pages encoding provider is registered.
// By default, only Unicode encodings, ASCII, and code page 28591 are supported. An
// additional provider must be registered to support the full set of encodings that
// were included in Full .NET prior to 4.6.
//
// For example, on an en-US box, this is required for loading the encoding for the
// default console output code page '437'. Without loading the correct encoding for
// code page IBM437, some characters cannot be translated correctly, e.g. write 'ç'
// from powershell.exe.
using (TestHostContext hc = new TestHostContext(this))
{
Tracing trace = hc.GetTrace();
var processInvoker = new ProcessInvokerWrapper();
processInvoker.Initialize(hc);
var stdout = new List();
var stderr = new List();
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs e) =>
{
stdout.Add(e.Data);
};
processInvoker.ErrorDataReceived += (object sender, ProcessDataReceivedEventArgs e) =>
{
stderr.Add(e.Data);
};
await processInvoker.ExecuteAsync(
workingDirectory: "",
fileName: "powershell.exe",
arguments: $@"-NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command ""Write-Host 'From STDOUT ''ç''' ; Write-Error 'From STDERR ''ç'''""",
environment: null,
requireExitCodeZero: false,
cancellationToken: CancellationToken.None);
astert.Equal(1, stdout.Count);
astert.Equal("From STDOUT 'ç'", stdout[0]);
astert.True(stderr.Count > 0);
astert.Contains("From STDERR 'ç'", stderr[0]);
}
}
#endif
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public async Task SuccessExitsWithCodeZero()
{
using (TestHostContext hc = new TestHostContext(this))
{
Tracing trace = hc.GetTrace();
Int32 exitCode = -1;
var processInvoker = new ProcessInvokerWrapper();
processInvoker.Initialize(hc);
#if OS_WINDOWS
exitCode = await processInvoker.ExecuteAsync("", "cmd.exe", "/c \"dir >nul\"", null, CancellationToken.None);
#else
exitCode = await processInvoker.ExecuteAsync("", "bash", "-c echo .", null, CancellationToken.None);
#endif
trace.Info("Exit Code: {0}", exitCode);
astert.Equal(0, exitCode);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public async Task SetCIEnv()
{
using (TestHostContext hc = new TestHostContext(this))
{
var existingCI = Environment.GetEnvironmentVariable("CI");
try
{
// Clear out CI and make sure process invoker sets it.
Environment.SetEnvironmentVariable("CI", null);
Tracing trace = hc.GetTrace();
Int32 exitCode = -1;
var processInvoker = new ProcessInvokerWrapper();
processInvoker.Initialize(hc);
var stdout = new List();
var stderr = new List();
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs e) =>
{
trace.Info(e.Data);
stdout.Add(e.Data);
};
processInvoker.ErrorDataReceived += (object sender, ProcessDataReceivedEventArgs e) =>
{
trace.Info(e.Data);
stderr.Add(e.Data);
};
#if OS_WINDOWS
exitCode = await processInvoker.ExecuteAsync("", "cmd.exe", "/c \"echo %CI%\"", null, CancellationToken.None);
#else
exitCode = await processInvoker.ExecuteAsync("", "bash", "-c \"echo $CI\"", null, CancellationToken.None);
#endif
trace.Info("Exit Code: {0}", exitCode);
astert.Equal(0, exitCode);
astert.Equal("true", stdout.First(x => !string.IsNullOrWhiteSpace(x)));
}
finally
{
Environment.SetEnvironmentVariable("CI", existingCI);
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public async Task KeepExistingCIEnv()
{
using (TestHostContext hc = new TestHostContext(this))
{
var existingCI = Environment.GetEnvironmentVariable("CI");
try
{
// Clear out CI and make sure process invoker sets it.
Environment.SetEnvironmentVariable("CI", null);
Tracing trace = hc.GetTrace();
Int32 exitCode = -1;
var processInvoker = new ProcessInvokerWrapper();
processInvoker.Initialize(hc);
var stdout = new List();
var stderr = new List();
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs e) =>
{
trace.Info(e.Data);
stdout.Add(e.Data);
};
processInvoker.ErrorDataReceived += (object sender, ProcessDataReceivedEventArgs e) =>
{
trace.Info(e.Data);
stderr.Add(e.Data);
};
#if OS_WINDOWS
exitCode = await processInvoker.ExecuteAsync("", "cmd.exe", "/c \"echo %CI%\"", new Dictionary() { { "CI", "false" } }, CancellationToken.None);
#else
exitCode = await processInvoker.ExecuteAsync("", "bash", "-c \"echo $CI\"", new Dictionary() { { "CI", "false" } }, CancellationToken.None);
#endif
trace.Info("Exit Code: {0}", exitCode);
astert.Equal(0, exitCode);
astert.Equal("false", stdout.First(x => !string.IsNullOrWhiteSpace(x)));
}
finally
{
Environment.SetEnvironmentVariable("CI", existingCI);
}
}
}
#if !OS_WINDOWS
//Run a process that normally takes 20sec to finish and cancel it.
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public async Task TestCancel()
{
const int SecondsToRun = 20;
using (TestHostContext hc = new TestHostContext(this))
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = hc.GetTrace();
var processInvoker = new ProcessInvokerWrapper();
processInvoker.Initialize(hc);
Stopwatch watch = Stopwatch.StartNew();
Task execTask;
#if OS_WINDOWS
execTask = processInvoker.ExecuteAsync("", "cmd.exe", $"/c \"choice /T {SecondsToRun} /D y\"", null, tokenSource.Token);
#else
execTask = processInvoker.ExecuteAsync("", "bash", $"-c \"sleep {SecondsToRun}s\"", null, tokenSource.Token);
#endif
await Task.Delay(500);
tokenSource.Cancel();
try
{
await execTask;
}
catch (OperationCanceledException)
{
trace.Info("Get expected OperationCanceledException.");
}
astert.True(execTask.IsCompleted);
astert.True(!execTask.IsFaulted);
astert.True(execTask.IsCanceled);
watch.Stop();
long elapsedSeconds = watch.ElapsedMilliseconds / 1000;
#if ARM
// if cancellation fails, then execution time is more than 15 seconds
// longer time to compensate for a slower ARM environment (e.g. Raspberry Pi)
long expectedSeconds = (SecondsToRun * 3) / 4;
#else
// if cancellation fails, then execution time is more than 10 seconds
long expectedSeconds = SecondsToRun / 2;
#endif
astert.True(elapsedSeconds
{
stdout.Add(e.Data);
};
processInvoker.Initialize(hc);
#if OS_WINDOWS
var proc = processInvoker.ExecuteAsync("", "cmd.exe", "/c more", null, false, null, false, redirectSTDIN, false, false, cancellationTokenSource.Token);
#else
var proc = processInvoker.ExecuteAsync("", "bash", "-c \"read input; echo $input; read input; echo $input; read input; echo $input;\"", null, false, null, false, redirectSTDIN, false, false, cancellationTokenSource.Token);
#endif
redirectSTDIN.Writer.TryWrite("More line of STDIN");
redirectSTDIN.Writer.TryWrite("More line of STDIN");
await Task.Delay(100);
redirectSTDIN.Writer.TryWrite("More line of STDIN");
redirectSTDIN.Writer.TryWrite("More line of STDIN");
await Task.Delay(100);
redirectSTDIN.Writer.TryWrite("More line of STDIN");
cancellationTokenSource.CancelAfter(100);
try
{
exitCode = await proc;
trace.Info("Exit Code: {0}", exitCode);
}
catch (Exception ex)
{
trace.Error(ex);
}
trace.Info("STDOUT: {0}", string.Join(Environment.NewLine, stdout));
astert.False(stdout.Contains("More line of STDIN"), "STDIN should be closed after first input line.");
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public async Task RedirectSTreplacedeepStreamOpen()
{
using (TestHostContext hc = new TestHostContext(this))
{
Tracing trace = hc.GetTrace();
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
Int32 exitCode = -1;
Channel redirectSTDIN = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleReader = true, SingleWriter = true });
List stdout = new List();
redirectSTDIN.Writer.TryWrite("Single line of STDIN");
var processInvoker = new ProcessInvokerWrapper();
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs e) =>
{
stdout.Add(e.Data);
};
processInvoker.Initialize(hc);
#if OS_WINDOWS
var proc = processInvoker.ExecuteAsync("", "cmd.exe", "/c more", null, false, null, false, redirectSTDIN, false, true, cancellationTokenSource.Token);
#else
var proc = processInvoker.ExecuteAsync("", "bash", "-c \"read input; echo $input; read input; echo $input; read input; echo $input;\"", null, false, null, false, redirectSTDIN, false, true, cancellationTokenSource.Token);
#endif
redirectSTDIN.Writer.TryWrite("More line of STDIN");
redirectSTDIN.Writer.TryWrite("More line of STDIN");
await Task.Delay(100);
redirectSTDIN.Writer.TryWrite("More line of STDIN");
redirectSTDIN.Writer.TryWrite("More line of STDIN");
await Task.Delay(100);
redirectSTDIN.Writer.TryWrite("More line of STDIN");
cancellationTokenSource.CancelAfter(100);
try
{
exitCode = await proc;
trace.Info("Exit Code: {0}", exitCode);
}
catch (Exception ex)
{
trace.Error(ex);
}
trace.Info("STDOUT: {0}", string.Join(Environment.NewLine, stdout));
astert.True(stdout.Contains("More line of STDIN"), "STDIN should keep open and accept more inputs after first input line.");
}
}
#if OS_LINUX
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public async Task OomScoreAdjIsWriten_Default()
{
// We are on a system that supports oom_score_adj in procfs as astumed by ProcessInvoker
string testProcPath = $"/proc/{Process.GetCurrentProcess().Id}/oom_score_adj";
if (File.Exists(testProcPath))
{
using (TestHostContext hc = new TestHostContext(this))
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = hc.GetTrace();
var processInvoker = new ProcessInvokerWrapper();
processInvoker.Initialize(hc);
int oomScoreAdj = -9999;
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs e) =>
{
oomScoreAdj = int.Parse(e.Data);
tokenSource.Cancel();
};
try
{
var proc = await processInvoker.ExecuteAsync("", "bash", "-c \"cat /proc/$$/oom_score_adj\"", null, false, null, false, null, false, false,
highPriorityProcess: false,
cancellationToken: tokenSource.Token);
astert.Equal(500, oomScoreAdj);
}
catch (OperationCanceledException)
{
trace.Info("Caught expected OperationCanceledException");
}
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public async Task OomScoreAdjIsWriten_FromEnv()
{
// We are on a system that supports oom_score_adj in procfs as astumed by ProcessInvoker
string testProcPath = $"/proc/{Process.GetCurrentProcess().Id}/oom_score_adj";
if (File.Exists(testProcPath))
{
using (TestHostContext hc = new TestHostContext(this))
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = hc.GetTrace();
var processInvoker = new ProcessInvokerWrapper();
processInvoker.Initialize(hc);
int oomScoreAdj = -9999;
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs e) =>
{
oomScoreAdj = int.Parse(e.Data);
tokenSource.Cancel();
};
try
{
var proc = await processInvoker.ExecuteAsync("", "bash", "-c \"cat /proc/$$/oom_score_adj\"",
new Dictionary { {"PIPELINE_JOB_OOMSCOREADJ", "1234"} },
false, null, false, null, false, false,
highPriorityProcess: false,
cancellationToken: tokenSource.Token);
astert.Equal(1234, oomScoreAdj);
}
catch (OperationCanceledException)
{
trace.Info("Caught expected OperationCanceledException");
}
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public async Task OomScoreAdjIsInherited()
{
// We are on a system that supports oom_score_adj in procfs as astumed by ProcessInvoker
string testProcPath = $"/proc/{Process.GetCurrentProcess().Id}/oom_score_adj";
if (File.Exists(testProcPath))
{
int testProcOomScoreAdj = 123;
File.WriteAllText(testProcPath, testProcOomScoreAdj.ToString());
using (TestHostContext hc = new TestHostContext(this))
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = hc.GetTrace();
var processInvoker = new ProcessInvokerWrapper();
processInvoker.Initialize(hc);
int oomScoreAdj = -9999;
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs e) =>
{
oomScoreAdj = int.Parse(e.Data);
tokenSource.Cancel();
};
try
{
var proc = await processInvoker.ExecuteAsync("", "bash", "-c \"cat /proc/$$/oom_score_adj\"", null, false, null, false, null, false, false,
highPriorityProcess: true,
cancellationToken: tokenSource.Token);
astert.Equal(123, oomScoreAdj);
}
catch (OperationCanceledException)
{
trace.Info("Caught expected OperationCanceledException");
}
}
}
}
#endif
}
}