Support backgrounding pipelines with ampersand (#3360)

Implements support for backgrounding pipelines with &. Putting & at the end of a pipeline will cause the pipeline
to be run as a PowerShell job. When a pipeline is backgrounded a job object is returned. Once the pipeline is
running as a job, all of the normal job cmdlets can be used to manage the job. Variables (ignoring process-specific
variables) used in the pipeline are automatically copied to the job so
    copy $foo $bar &
just works. The job is also run in the current directory instead of the user's home directory as is the case with Start-Job.Implement
This commit is contained in:
Bruce Payette
2017-05-18 19:15:19 -07:00
committed by Jason Shirk
parent f0b03bc57d
commit dfb3866c5d
7 changed files with 310 additions and 116 deletions
@@ -318,6 +318,8 @@ namespace System.Management.Automation.Language
typeof(PipelineOps).GetMethod(nameof(PipelineOps.FlushPipe), staticFlags);
internal static readonly MethodInfo PipelineOps_InvokePipeline =
typeof(PipelineOps).GetMethod(nameof(PipelineOps.InvokePipeline), staticFlags);
internal static readonly MethodInfo PipelineOps_InvokePipelineInBackground =
typeof(PipelineOps).GetMethod(nameof(PipelineOps.InvokePipelineInBackground), staticFlags);
internal static readonly MethodInfo PipelineOps_Nop =
typeof(PipelineOps).GetMethod(nameof(PipelineOps.Nop), staticFlags);
internal static readonly MethodInfo PipelineOps_PipelineResult =
@@ -1942,7 +1944,8 @@ namespace System.Management.Automation.Language
}
var pipelineAst = stmt as PipelineAst;
if (pipelineAst != null)
// If it's a pipeline that isn't being backgrounded, try to optimize expression
if (pipelineAst != null && ! pipelineAst.Background)
{
var expr = pipelineAst.GetPureExpression();
if (expr != null) { return Compile(expr); }
@@ -2983,104 +2986,115 @@ namespace System.Management.Automation.Language
exprs.Add(UpdatePosition(pipelineAst));
}
var pipeElements = pipelineAst.PipelineElements;
var firstCommandExpr = (pipeElements[0] as CommandExpressionAst);
if (firstCommandExpr != null && pipeElements.Count == 1)
if (pipelineAst.Background)
{
if (firstCommandExpr.Redirections.Count > 0)
{
exprs.Add(GetRedirectedExpression(firstCommandExpr, captureForInput: false));
}
else
{
exprs.Add(Compile(firstCommandExpr));
}
Expression invokeBackgroundPipe = Expression.Call(
CachedReflectionInfo.PipelineOps_InvokePipelineInBackground,
Expression.Constant(pipelineAst),
_functionContext);
exprs.Add(invokeBackgroundPipe);
}
else
{
Expression input;
int i, commandsInPipe;
if (firstCommandExpr != null)
var pipeElements = pipelineAst.PipelineElements;
var firstCommandExpr = (pipeElements[0] as CommandExpressionAst);
if (firstCommandExpr != null && pipeElements.Count == 1)
{
if (firstCommandExpr.Redirections.Count > 0)
{
input = GetRedirectedExpression(firstCommandExpr, captureForInput: true);
exprs.Add(GetRedirectedExpression(firstCommandExpr, captureForInput: false));
}
else
{
input = GetRangeEnumerator(firstCommandExpr.Expression) ??
Compile(firstCommandExpr.Expression);
exprs.Add(Compile(firstCommandExpr));
}
i = 1;
commandsInPipe = pipeElements.Count - 1;
}
else
{
// Compiled code normally never sees AutomationNull. We use that value
// here so that we can tell the difference b/w $null and no input when
// starting the pipeline, in other words, PipelineOps.InvokePipe will
// not pass this value to the pipe.
input = ExpressionCache.AutomationNullConstant;
i = 0;
commandsInPipe = pipeElements.Count;
Expression input;
int i, commandsInPipe;
if (firstCommandExpr != null)
{
if (firstCommandExpr.Redirections.Count > 0)
{
input = GetRedirectedExpression(firstCommandExpr, captureForInput: true);
}
else
{
input = GetRangeEnumerator(firstCommandExpr.Expression) ??
Compile(firstCommandExpr.Expression);
}
i = 1;
commandsInPipe = pipeElements.Count - 1;
}
else
{
// Compiled code normally never sees AutomationNull. We use that value
// here so that we can tell the difference b/w $null and no input when
// starting the pipeline, in other words, PipelineOps.InvokePipe will
// not pass this value to the pipe.
input = ExpressionCache.AutomationNullConstant;
i = 0;
commandsInPipe = pipeElements.Count;
}
Expression[] pipelineExprs = new Expression[commandsInPipe];
CommandBaseAst[] pipeElementAsts = new CommandBaseAst[commandsInPipe];
var commandRedirections = new object[commandsInPipe];
for (int j = 0; i < pipeElements.Count; ++i, ++j)
{
var pipeElement = pipeElements[i];
pipelineExprs[j] = Compile(pipeElement);
commandRedirections[j] = GetCommandRedirections(pipeElement);
pipeElementAsts[j] = pipeElement;
}
// The redirections are passed as a CommandRedirection[][] - one dimension for each command in the pipe,
// one dimension because each command may have multiple redirections. Here we create the array for
// each command in the pipe, either a compile time constant or created at runtime if necessary.
Expression redirectionExpr;
if (commandRedirections.Any(r => r is Expression))
{
// If any command redirections are non-constant, commandRedirections will have a Linq.Expression in it,
// in which case we must create the array at runtime
redirectionExpr =
Expression.NewArrayInit(typeof(CommandRedirection[]),
commandRedirections.Select(r => (r as Expression) ?? Expression.Constant(r, typeof(CommandRedirection[]))));
}
else if (commandRedirections.Any(r => r != null))
{
// There were redirections, but all were compile time constant, so build the array at compile time.
redirectionExpr =
Expression.Constant(commandRedirections.Map(r => r as CommandRedirection[]));
}
else
{
// No redirections.
redirectionExpr = ExpressionCache.NullCommandRedirections;
}
if (firstCommandExpr != null)
{
var inputTemp = Expression.Variable(input.Type);
temps.Add(inputTemp);
exprs.Add(Expression.Assign(inputTemp, input));
input = inputTemp;
}
Expression invokePipe = Expression.Call(
CachedReflectionInfo.PipelineOps_InvokePipeline,
input.Cast(typeof(object)),
firstCommandExpr != null ? ExpressionCache.FalseConstant : ExpressionCache.TrueConstant,
Expression.NewArrayInit(typeof(CommandParameterInternal[]), pipelineExprs),
Expression.Constant(pipeElementAsts),
redirectionExpr,
_functionContext);
exprs.Add(invokePipe);
}
Expression[] pipelineExprs = new Expression[commandsInPipe];
CommandBaseAst[] pipeElementAsts = new CommandBaseAst[commandsInPipe];
var commandRedirections = new object[commandsInPipe];
for (int j = 0; i < pipeElements.Count; ++i, ++j)
{
var pipeElement = pipeElements[i];
pipelineExprs[j] = Compile(pipeElement);
commandRedirections[j] = GetCommandRedirections(pipeElement);
pipeElementAsts[j] = pipeElement;
}
// The redirections are passed as a CommandRedirection[][] - one dimension for each command in the pipe,
// one dimension because each command may have multiple redirections. Here we create the array for
// each command in the pipe, either a compile time constant or created at runtime if necessary.
Expression redirectionExpr;
if (commandRedirections.Any(r => r is Expression))
{
// If any command redirections are non-constant, commandRedirections will have a Linq.Expression in it,
// in which case we must create the array at runtime
redirectionExpr =
Expression.NewArrayInit(typeof(CommandRedirection[]),
commandRedirections.Select(r => (r as Expression) ?? Expression.Constant(r, typeof(CommandRedirection[]))));
}
else if (commandRedirections.Any(r => r != null))
{
// There were redirections, but all were compile time constant, so build the array at compile time.
redirectionExpr =
Expression.Constant(commandRedirections.Map(r => r as CommandRedirection[]));
}
else
{
// No redirections.
redirectionExpr = ExpressionCache.NullCommandRedirections;
}
if (firstCommandExpr != null)
{
var inputTemp = Expression.Variable(input.Type);
temps.Add(inputTemp);
exprs.Add(Expression.Assign(inputTemp, input));
input = inputTemp;
}
Expression invokePipe = Expression.Call(
CachedReflectionInfo.PipelineOps_InvokePipeline,
input.Cast(typeof(object)),
firstCommandExpr != null ? ExpressionCache.FalseConstant : ExpressionCache.TrueConstant,
Expression.NewArrayInit(typeof(CommandParameterInternal[]), pipelineExprs),
Expression.Constant(pipeElementAsts),
redirectionExpr,
_functionContext);
exprs.Add(invokePipe);
}
return Expression.Block(temps, exprs);
@@ -2451,7 +2451,7 @@ namespace System.Management.Automation.Language
{
endErrorStatement = fileNameExpr.Extent;
condition = new PipelineAst(fileNameExpr.Extent,
new CommandExpressionAst(fileNameExpr.Extent, fileNameExpr, null));
new CommandExpressionAst(fileNameExpr.Extent, fileNameExpr, null), background: false);
if (!specifiedFlags.ContainsKey("file"))
{
@@ -5182,6 +5182,7 @@ namespace System.Management.Automation.Language
Token pipeToken = null;
bool scanning = true;
bool background = false;
while (scanning)
{
CommandBaseAst commandAst;
@@ -5293,6 +5294,11 @@ namespace System.Management.Automation.Language
case TokenKind.EndOfInput:
scanning = false;
continue;
case TokenKind.Ampersand:
SkipToken();
scanning = false;
background = true;
break;
case TokenKind.Pipe:
SkipToken();
SkipNewlines();
@@ -5328,7 +5334,7 @@ namespace System.Management.Automation.Language
return null;
}
return new PipelineAst(ExtentOf(startExtent, pipelineElements[pipelineElements.Count - 1]), pipelineElements);
return new PipelineAst(ExtentOf(startExtent, pipelineElements[pipelineElements.Count - 1]), pipelineElements, background);
}
private RedirectionAst RedirectionRule(RedirectionToken redirectionToken, RedirectionAst[] redirections, ref IScriptExtent extent)
@@ -5672,16 +5678,11 @@ namespace System.Management.Automation.Language
case TokenKind.Semi:
case TokenKind.AndAnd:
case TokenKind.OrOr:
case TokenKind.Ampersand:
UngetToken(token);
scanning = false;
continue;
case TokenKind.Ampersand:
// ErrorRecovery: just ignore the token.
endExtent = token.Extent;
ReportError(token.Extent, () => ParserStrings.AmpersandNotAllowed);
break;
case TokenKind.MinusMinus:
endExtent = token.Extent;
// Add the first -- as a parameter, which is then ignored when constructing the command processor unless it's a native
@@ -5472,13 +5472,14 @@ namespace System.Management.Automation.Language
/// </summary>
/// <param name="extent">The extent of the pipeline.</param>
/// <param name="pipelineElements">The collection of commands representing the pipeline.</param>
/// <param name="background">Indicates that this pipeline should be run in the background</param>
/// <exception cref="PSArgumentNullException">
/// If <paramref name="extent"/> is null.
/// </exception>
/// <exception cref="PSArgumentException">
/// If <paramref name="pipelineElements"/> is null or is an empty collection.
/// </exception>
public PipelineAst(IScriptExtent extent, IEnumerable<CommandBaseAst> pipelineElements)
public PipelineAst(IScriptExtent extent, IEnumerable<CommandBaseAst> pipelineElements, bool background)
: base(extent)
{
if (pipelineElements == null || !pipelineElements.Any())
@@ -5486,10 +5487,49 @@ namespace System.Management.Automation.Language
throw PSTraceSource.NewArgumentException("pipelineElements");
}
this.Background = background;
this.PipelineElements = new ReadOnlyCollection<CommandBaseAst>(pipelineElements.ToArray());
SetParents(PipelineElements);
}
/// <summary>
/// Construct a pipeline from a collection of commands.
/// </summary>
/// <param name="extent">The extent of the pipeline.</param>
/// <param name="pipelineElements">The collection of commands representing the pipeline.</param>
/// <exception cref="PSArgumentNullException">
/// If <paramref name="extent"/> is null.
/// </exception>
/// <exception cref="PSArgumentException">
/// If <paramref name="pipelineElements"/> is null or is an empty collection.
/// </exception>
public PipelineAst(IScriptExtent extent, IEnumerable<CommandBaseAst> pipelineElements) :this (extent, pipelineElements, background: false)
{
}
/// <summary>
/// Construct a pipeline from a single command.
/// </summary>
/// <param name="extent">The extent of the pipeline (which should be the extent of the command).</param>
/// <param name="commandAst">The command for the pipeline.</param>
/// <param name="background">Indicates that this pipeline should be run in the background</param>
/// <exception cref="PSArgumentNullException">
/// If <paramref name="extent"/> or <paramref name="commandAst"/> is null.
/// </exception>
public PipelineAst(IScriptExtent extent, CommandBaseAst commandAst, bool background)
: base(extent)
{
if (commandAst == null)
{
throw PSTraceSource.NewArgumentNullException("commandAst");
}
this.Background = background;
this.PipelineElements = new ReadOnlyCollection<CommandBaseAst>(new CommandBaseAst[] { commandAst });
SetParent(commandAst);
}
/// <summary>
/// Construct a pipeline from a single command.
/// </summary>
@@ -5498,16 +5538,9 @@ namespace System.Management.Automation.Language
/// <exception cref="PSArgumentNullException">
/// If <paramref name="extent"/> or <paramref name="commandAst"/> is null.
/// </exception>
public PipelineAst(IScriptExtent extent, CommandBaseAst commandAst)
: base(extent)
public PipelineAst(IScriptExtent extent, CommandBaseAst commandAst) :this (extent, commandAst, background: false)
{
if (commandAst == null)
{
throw PSTraceSource.NewArgumentNullException("commandAst");
}
this.PipelineElements = new ReadOnlyCollection<CommandBaseAst>(new CommandBaseAst[] { commandAst });
SetParent(commandAst);
}
/// <summary>
@@ -5515,6 +5548,11 @@ namespace System.Management.Automation.Language
/// </summary>
public ReadOnlyCollection<CommandBaseAst> PipelineElements { get; private set; }
/// <summary>
/// Indicates that this pipeline should be run in the background.
/// </summary>
public bool Background { get; private set; }
/// <summary>
/// If the pipeline represents a pure expression, the expression is returned, otherwise null is returned.
/// </summary>
@@ -5540,7 +5578,7 @@ namespace System.Management.Automation.Language
public override Ast Copy()
{
var newPipelineElements = CopyElements(this.PipelineElements);
return new PipelineAst(this.Extent, newPipelineElements);
return new PipelineAst(this.Extent, newPipelineElements, this.Background);
}
internal override IEnumerable<PSTypeName> GetInferredType(CompletionContext context)
@@ -6314,9 +6352,10 @@ namespace System.Management.Automation.Language
throw PSTraceSource.NewArgumentException("operator");
}
// If the assignment is just an expression - remove the pipeline wrapping the expression.
// If the assignment is just an expression and the expression is not backgrounded then
// remove the pipeline wrapping the expression.
var pipelineAst = right as PipelineAst;
if (pipelineAst != null)
if (pipelineAst != null && ! pipelineAst.Background)
{
if (pipelineAst.PipelineElements.Count == 1)
{
@@ -6662,7 +6701,7 @@ namespace System.Management.Automation.Language
var cmdAst = new CommandAst(this.Extent, cea, TokenKind.Unknown, null);
var pipeLineAst = new PipelineAst(this.Extent, cmdAst);
var pipeLineAst = new PipelineAst(this.Extent, cmdAst, background: false);
var funcStatements = ConfigurationExtraParameterStatements.Select(statement => (StatementAst)statement.Copy()).ToList();
funcStatements.Add(pipeLineAst);
var statmentBlockAst = new StatementBlockAst(this.Extent, funcStatements, null);
@@ -6702,7 +6741,7 @@ namespace System.Management.Automation.Language
var setItemCmdlet = new CommandAst(this.Extent, setItemCmdElements, TokenKind.Unknown, null);
#endregion
var returnPipelineAst = new PipelineAst(this.Extent, setItemCmdlet);
var returnPipelineAst = new PipelineAst(this.Extent, setItemCmdlet, background: false);
SetParent(returnPipelineAst);
@@ -7247,7 +7286,7 @@ namespace System.Management.Automation.Language
//
var cmdAst = new CommandAst(FunctionName.Extent, cea, TokenKind.Unknown, null);
cmdAst.DefiningKeyword = Keyword;
_commandCallPipelineAst = new PipelineAst(FunctionName.Extent, cmdAst);
_commandCallPipelineAst = new PipelineAst(FunctionName.Extent, cmdAst, background: false);
return _commandCallPipelineAst;
}
@@ -403,13 +403,14 @@ namespace System.Management.Automation
CommandProcessorBase commandProcessor = null;
CommandRedirection[] commandRedirection = null;
for (int i = 0; i < pipeElements.Length; i++)
{
commandRedirection = commandRedirections != null ? commandRedirections[i] : null;
commandProcessor = AddCommand(pipelineProcessor, pipeElements[i], pipeElementAsts[i],
commandRedirection, context);
}
var cmdletInfo = commandProcessor?.CommandInfo as CmdletInfo;
if (cmdletInfo?.ImplementingType == typeof(OutNullCommand))
{
@@ -419,7 +420,7 @@ namespace System.Management.Automation
// Out-Null is the only command, bail without running anything
return;
}
// Out-Null is the last command, rewrite command before Out-Null to a null pipe, but
// only if it didn't redirect anything, e.g. `Get-Stuff > o.txt | Out-Null`
var nextToLastCommand = pipelineProcessor.Commands[commandsCount - 2];
@@ -465,6 +466,83 @@ namespace System.Management.Automation
}
}
internal static void InvokePipelineInBackground(
PipelineAst pipelineAst,
FunctionContext funcContext)
{
PipelineProcessor pipelineProcessor = new PipelineProcessor();
ExecutionContext context = funcContext._executionContext;
Pipe outputPipe = funcContext._outputPipe;
try
{
if (context.Events != null)
{
context.Events.ProcessPendingActions();
}
CommandProcessorBase commandProcessor = null;
// For background jobs rewrite the pipeline as a Start-Job command
var scriptblockBodyString = pipelineAst.Extent.Text;
var pipelineOffset = pipelineAst.Extent.StartOffset;
var variables = pipelineAst.FindAll(x => x is VariableExpressionAst, true);
// Used to make sure that the job runs in the current directory
const string cmdPrefix = @"Microsoft.PowerShell.Management\Set-Location -LiteralPath $using:pwd ; ";
// Minimize allocations by initializing the stringbuilder to the size of the source string + prefix + space for ${using:} * 2
System.Text.StringBuilder updatedScriptblock = new System.Text.StringBuilder(cmdPrefix.Length + scriptblockBodyString.Length + 18);
updatedScriptblock.Append(cmdPrefix);
int position = 0;
// Prefix variables in the scriptblock with $using:
foreach (var v in variables)
{
var vName = ((VariableExpressionAst) v).VariablePath.UserPath;
// Skip variables that don't exist
if (funcContext._executionContext.EngineSessionState.GetVariable(vName) == null)
continue;
// Skip PowerShell magic variables
if (Regex.Match(vName,
"^(global:){0,1}(PID|PSVersionTable|PSEdition|PSHOME|HOST|TRUE|FALSE|NULL)$",
RegexOptions.IgnoreCase|RegexOptions.CultureInvariant).Success == false
)
{
updatedScriptblock.Append(scriptblockBodyString.Substring(position, v.Extent.StartOffset - pipelineOffset - position));
updatedScriptblock.Append("${using:");
updatedScriptblock.Append(CodeGeneration.EscapeVariableName(vName));
updatedScriptblock.Append('}');
position = v.Extent.EndOffset - pipelineOffset;
}
}
updatedScriptblock.Append(scriptblockBodyString.Substring(position));
var sb = ScriptBlock.Create(updatedScriptblock.ToString());
var commandInfo = new CmdletInfo("Start-Job", typeof(StartJobCommand));
commandProcessor = context.CommandDiscovery.LookupCommandProcessor(
commandInfo, CommandOrigin.Internal, false, context.EngineSessionState);
var parameter = CommandParameterInternal.CreateParameterWithArgument(
pipelineAst.Extent, "ScriptBlock", null,
pipelineAst.Extent, sb,
false);
commandProcessor.AddParameter(parameter);
pipelineProcessor.Add(commandProcessor);
pipelineProcessor.LinkPipelineSuccessOutput(outputPipe ?? new Pipe(new List<object>()));
context.PushPipelineProcessor(pipelineProcessor);
try
{
pipelineProcessor.SynchronousExecuteEnumerate(AutomationNull.Value);
}
finally
{
context.PopPipelineProcessor(false);
}
}
finally
{
context.QuestionMarkVariableValue = !pipelineProcessor.ExecutionFailed;
pipelineProcessor.Dispose();
}
}
private static void AddNoopCommandProcessor(PipelineProcessor pipelineProcessor, ExecutionContext context)
{
var commandInfo = new CmdletInfo("Out-Null", typeof(OutNullCommand));
@@ -716,7 +794,7 @@ namespace System.Management.Automation
internal static void Nop() { }
}
#region Redirections
#region Redirections
internal abstract class CommandRedirection
{
@@ -1105,7 +1183,7 @@ namespace System.Management.Automation
}
}
#endregion Redirections
#endregion Redirections
internal static class FunctionOps
{
@@ -446,9 +446,6 @@ Possible matches are</value>
<data name="InvalidEndOfLine" xml:space="preserve">
<value>The token '{0}' is not a valid statement separator in this version.</value>
</data>
<data name="AmpersandNotAllowed" xml:space="preserve">
<value>The ampersand (&amp;) character is not allowed. The &amp; operator is reserved for future use; wrap an ampersand in double quotation marks ("&amp;") to pass it as part of a string.</value>
</data>
<data name="FromKeywordNotAllowed" xml:space="preserve">
<value>The 'from' keyword is not supported in this version of the language.</value>
</data>
@@ -1447,4 +1444,4 @@ ModuleVersion : Version of module to import. If used, ModuleName must represent
<data name="PsDscRunAsCredentialMergeErrorForCompositeResources" xml:space="preserve">
<value>Conflict in using PsDscRunAsCredential for Resource {0} because it already specifies PsDscRunAsCredential value. We can only use one PsDscRunAsCredential for the composite resource. </value>
</data>
</root>
</root>
@@ -277,7 +277,6 @@ Describe 'Pipes parsing' -Tags "CI" {
ShouldBeParseError 'gps|' EmptyPipeElement 4
ShouldBeParseError '1|1' ExpressionsMustBeFirstInPipeline 2
ShouldBeParseError '$a=' ExpectedValueExpression 3
ShouldBeParseError '1 &' UnexpectedToken,MissingExpression 2,2
}
Describe 'commands parsing' -Tags "CI" {
@@ -165,3 +165,69 @@ Describe "Debug-job test" -tag "Feature" {
$result.Command | Should be "<ScriptBlock>"
}
}
Describe "Ampersand background test" -tag "CI","Slow" {
Context "Simple background job" {
AfterEach {
Get-Job | Remove-Job -Force
}
It "Background with & produces a job object" {
$j = Write-Output Hi &
$j | Should BeOfType System.Management.Automation.Job
}
}
Context "Variable tests" {
AfterEach {
Get-Job | Remove-Job -Force
}
It "doesn't cause error when variable is missing" {
Remove-Item variable:name -ErrorAction Ignore
$j = write-output "Hi $name" &
Receive-Job $j -Wait | Should BeExactly "Hi "
}
It "Copies variables to the child process" {
$n1 = "Bob"
$n2 = "Mary"
${n 3} = "Bill"
$j = Write-Output "Hi $n1! Hi ${n2}! Hi ${n 3}!" &
Receive-Job $j -Wait | Should BeExactly "Hi Bob! Hi Mary! Hi Bill!"
}
It 'Make sure that $PID from the parent process does not overwrite $PID in the child process' {
$j = Write-Output $pid &
$cpid = Receive-Job $j -Wait
$pid | Should Not BeExactly $cpid
}
It 'Make sure that $global:PID from the parent process does not overwrite $global:PID in the child process' {
$j = Write-Output $global:pid &
$cpid = Receive-Job -Wait $j
$pid | Should Not BeExactly $cpid
}
It "starts in the current directory" {
$j = Get-Location | Foreach-Object -MemberName Path &
Receive-Job -Wait $j | Should Be ($pwd.Path)
}
It "Test that output redirection is done in the background job" {
$j = Write-Output hello > $TESTDRIVE/hello.txt &
Receive-Job -Wait $j | Should Be $null
Get-Content $TESTDRIVE/hello.txt | Should BeExactly "hello"
}
It "Test that error redirection is done in the background job" {
$j = Write-Error MyError 2> $TESTDRIVE/myerror.txt &
Receive-Job -Wait $j | Should Be $null
Get-Content -Raw $TESTDRIVE/myerror.txt | Should Match "MyError"
}
}
Context "Backgrounding expressions" {
AfterEach {
Get-Job | Remove-Job -Force
}
It "handles backgrounding expressions" {
$j = 2+3 &
Receive-Job $j -Wait | Should Be 5
}
It "handles backgrounding mixed expressions" {
$j = 1..10 | ForEach-Object -Begin {$s=0} -Process {$s += $_} -End {$s} &
Receive-Job -Wait $j | Should Be 55
}
}
}