Support backgrounding pipelines with ampersand#3360
Support backgrounding pipelines with ampersand#3360lzybkr merged 5 commits intoPowerShell:masterfrom
Conversation
|
Please rebase on master so commits that aren't yours go away. |
81b84fb to
564cc3f
Compare
There was a problem hiding this comment.
The api is public so this is a breaking change. We need to introduce a new constructor.
There was a problem hiding this comment.
Should we maybe inherit another class from PipelineAst instead? Something like BackgroundPipelineAst.
There was a problem hiding this comment.
I think a new Ast is too much.
There was a problem hiding this comment.
Another public api - so this is breaking as well.
There was a problem hiding this comment.
I see no tests to cover this case - are you sure it's correct?
A test would look like:
1..10 | % { $_ } &There was a problem hiding this comment.
I'll add the proposed test (which does pass).
There was a problem hiding this comment.
Use Utils.EmptyArray<Expression>() instead of new Expression[0].
There was a problem hiding this comment.
Are you sure the expression is evaluated only once and in the background.
From my quick review, I'm concerned it's evaluated twice, once in the foreground (and thrown away), and then again in the background.
Something like $(Sleep -Seconds 10; 2+3) & would make it obvious I think.
There was a problem hiding this comment.
I've verified that it only executes once in the background task.
There was a problem hiding this comment.
We should monitor how long this test takes, maybe move it to Nightly before merging if it's too slow from creating too many background jobs.
There was a problem hiding this comment.
It takes > 10 s so nightly is probably right.
There was a problem hiding this comment.
You generally don't want to use Extent.Text except for error messages or other ui purposes, it might include more than you think, e.g. ${pid} - it includes the braces.
Instead, you should use the VariablePath property.
There was a problem hiding this comment.
This regex won't work if a scope was used, like global:pid. You probably also want to skip constants like $null, $true, and $false.
There was a problem hiding this comment.
This is pretty inefficient in that it creates a bunch of strings.
It would be better to start with a copy of the script block body in a StringBuilder and insert using as appropriate.
There was a problem hiding this comment.
If something fails in starting the job, you won't report the actual position of the background invocation.
I think you probably also want to ensure we call PowerShell's Start-Job, not any random command called Start-Job.
There was a problem hiding this comment.
I'm changing it to report the pipeline extent.
There was a problem hiding this comment.
I think this would be cleaner to just build the CommandProcessor directly instead of creating an Ast to build the CommandProcessor.
I think of the Ast as mapping directly to source as much as possible, and this use of the Ast doesn't seem necessary.
lzybkr
left a comment
There was a problem hiding this comment.
We definitely need to fix the breaking change and have more tests.
It feels like we already have similar code rewriting the script block, but I can't recall where. If you do, it would be nice to unify that code if possible.
|
@BrucePay - please fix the build break in appveyor - it looks like it catches a breaking change in the api. |
b4d2822 to
30642fc
Compare
There was a problem hiding this comment.
What is 'cd' here? Alias? If so we should use full name.
There was a problem hiding this comment.
This title is difficult to understand. Maybe better "Background job don't overwrite the parent process 'Pid'"
There was a problem hiding this comment.
Reworded for clarity.
There was a problem hiding this comment.
Reworded for clarity.
There was a problem hiding this comment.
Please use "Should Not BeExactly".
There was a problem hiding this comment.
Please use "Should Not BeExactly".
30642fc to
80e4547
Compare
There was a problem hiding this comment.
Please replace alias with full name. Below the alias is used too.
There was a problem hiding this comment.
Please replace alias foreach.
There was a problem hiding this comment.
Please replace alias and add parameter names.
Also please correct register Should Be here and above -Wait, Write-Output, titles and so on.
There was a problem hiding this comment.
Please recheck - I dont see the fixes.
|
Just a general comment about the design - do we have a good feel for how:
There was the hypothesis that On the other hand, if the primary scenario really is background jobs, then this PR will be a great syntactic shortcut. |
|
The primary scenario really is to background pipelines. The need to launch blocking apps was simply a forcing function. |
There was a problem hiding this comment.
I don't see why you couldn't use pipelineElementAsts[0].Parent here instead of parsing the pipeline again.
There was a problem hiding this comment.
I need the actual offsets in the string to do the variable substitution. The parent AST will have the offsets in the parent script not the scriptblock.
There was a problem hiding this comment.
Isn't that as simple as subtracting pipeElementAsts[0].Parent.Extent.StartOffset ?
There was a problem hiding this comment.
You might as well use scriptblockBodyString.Length + length of the set-location text as the initial capacity to minimize allocations.
There was a problem hiding this comment.
Since I'm inserting $using: for each variable, scriptblockBody.Length is guaranteed to be too short forcing an allocation.
There was a problem hiding this comment.
Yes, but StringBuilder starts with 16 characters and adds chunks by the larger of how much you are appending and how big the buffer is already.
With many small appends, you'll probably end up with multiple chunks, whereas starting with the minimum needed will result in probably 1 extra allocation, or you could just double the minimum needed if it's "small" (say less than 40kb or so) to avoid a single allocation going into the LOH.
There was a problem hiding this comment.
I don't have a good feeling about not copying any variables starting with PS. I understand the intent, but I confidently predict it will trip up some folks and their scenario will seem perfectly reasonable.
There was a problem hiding this comment.
You can remove the resource string - this is the only reference.
There was a problem hiding this comment.
TokenKind [](start = 29, length = 9)
what do the following emit?
$A = "bar" & "foo"
or
$A = $("bar" & echo "foo")
and we should probably have tests for them
There was a problem hiding this comment.
& is a statement separator like in ksh so in your examples, you'd get an array of two objects: the job object and "foo".
379aff2 to
06ae76d
Compare
There was a problem hiding this comment.
This should be fixed now.
8905083 to
80041b2
Compare
There was a problem hiding this comment.
We can use C# 7.0 pattern here.
There was a problem hiding this comment.
We aren't targeting C# 7.
There was a problem hiding this comment.
We are now, but you didn't change this code, just indented it, so don't bother changing it now.
There was a problem hiding this comment.
Please use Receive-Job $j -Wait as above.
There was a problem hiding this comment.
This is unnecessary churn.
…he 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
Marked background jobs as SLOW
Removed call to Parser.Parse in processing the background job string
80041b2 to
52b8460
Compare
| firstCommandExpr != null ? ExpressionCache.FalseConstant : ExpressionCache.TrueConstant, | ||
| Expression.NewArrayInit(typeof(CommandParameterInternal[]), pipelineExprs), | ||
| firstCommandExpr != null && !pipelineAst.BackgroundProcess ? ExpressionCache.FalseConstant : ExpressionCache.TrueConstant, | ||
| Expression.NewArrayInit(typeof(CommandParameterInternal[]), |
There was a problem hiding this comment.
After looking more closely, I wonder if it makes sense to avoid basically this whole method (Compiler.VisitPipeline) and generate code to call PipelineOps.InvokePipeline directly.
Of particular concern - the pipelineExprs are ignored - so they shouldn't be generated in the first place. You never know what sort of side effects generating those expressions might have, it's better to avoid that entirely.
There was a problem hiding this comment.
Refactored the code.
| Expression.NewArrayInit(typeof(CommandParameterInternal[]), | ||
| !pipelineAst.BackgroundProcess ? pipelineExprs : Utils.EmptyArray<Expression>()), | ||
| Expression.Constant(pipeElementAsts), | ||
| redirectionExpr, |
There was a problem hiding this comment.
I don't see any tests for redirections. I assume the redirections are also done in the background, and hence should not be generated in the foreground.
There was a problem hiding this comment.
Added tests for redirection.
| /// <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, false) |
| /// <exception cref="PSArgumentNullException"> | ||
| /// If <paramref name="extent"/> or <paramref name="commandAst"/> is null. | ||
| /// </exception> | ||
| public PipelineAst(IScriptExtent extent, CommandBaseAst commandAst) :this (extent, commandAst, false) |
|
|
||
| var pipeLineAst = new PipelineAst(this.Extent, cmdAst); | ||
| var pipeLineAst = new PipelineAst(this.Extent, cmdAst, false); | ||
| var funcStatements = ConfigurationExtraParameterStatements.Select(statement => (StatementAst)statement.Copy()).ToList(); |
|
|
||
| var returnPipelineAst = new PipelineAst(this.Extent, setItemCmdlet); | ||
| var returnPipelineAst = new PipelineAst(this.Extent, setItemCmdlet, false); | ||
|
|
| cmdAst.DefiningKeyword = Keyword; | ||
| _commandCallPipelineAst = new PipelineAst(FunctionName.Extent, cmdAst); | ||
| _commandCallPipelineAst = new PipelineAst(FunctionName.Extent, cmdAst, false); | ||
| return _commandCallPipelineAst; |
There was a problem hiding this comment.
We are now, but you didn't change this code, just indented it, so don't bother changing it now.
|
|
||
| var cmdletInfo = commandProcessor?.CommandInfo as CmdletInfo; | ||
| if (cmdletInfo?.ImplementingType == typeof(OutNullCommand)) | ||
| var pipelineAst = (PipelineAst)pipeElementAsts[0].Parent; |
There was a problem hiding this comment.
After thinking more about the code in Compiler.VisitPipeline and looking at this code, I think it makes sense to add a new method InvokePipelineInBackground that accepts the PipelineAst and avoid touching this method completely. They really are very different operations.
There was a problem hiding this comment.
I've refactored the code as suggested.
| { | ||
| updatedScriptblock.Append(scriptblockBodyString.Substring(position, v.Extent.StartOffset - pipelineOffset - position)); | ||
| updatedScriptblock.Append("${using:"); | ||
| updatedScriptblock.Append(vName); |
There was a problem hiding this comment.
Annoying corner case - you need to escape } in vName. CodeGeneration.EscapeVariableName will handle that for you.
|
@lzybkr it looks like all the feedback has been addressed? |
| redirectionExpr, | ||
| _functionContext); | ||
| Expression invokePipe; | ||
| if (pipelineAst.BackgroundProcess) |
There was a problem hiding this comment.
I was thinking this would go at the top of this method to avoid needlessly compiling any of the body. That would also eliminate the need for some of the tests that were added above. Is that possible?
| } | ||
|
|
||
| internal static void InvokePipelineInBackground( | ||
| CommandBaseAst[] pipeElementAsts, |
There was a problem hiding this comment.
You can pass the PipelineAst instead of the elements because that's all you use here.
| var firstCommandExpr = (pipeElements[0] as CommandExpressionAst); | ||
| if (firstCommandExpr != null && pipeElements.Count == 1 && ! pipelineAst.BackgroundProcess) | ||
| { |
There was a problem hiding this comment.
You can revert this change now - it can't be a background process.
| int i, commandsInPipe; | ||
|
|
||
| if (firstCommandExpr != null && !pipelineAst.BackgroundProcess) | ||
| { |
There was a problem hiding this comment.
You can revert this change now - it can't be a background process.
| /// <summary> | ||
| /// Indicates that this pipeline should be run in the background. | ||
| /// </summary> | ||
| public bool BackgroundProcess { get; private set; } |
There was a problem hiding this comment.
BackgroundProcess [](start = 20, length = 17)
Maybe the property should not include Process, e.g. if in the future, we support in-process jobs, then it's just in the background, not necessarily in another process.
There was a problem hiding this comment.
Change BackgroundProcess to just be Background.
| cmdAst.DefiningKeyword = Keyword; | ||
| _commandCallPipelineAst = new PipelineAst(FunctionName.Extent, cmdAst); | ||
| _commandCallPipelineAst = new PipelineAst(FunctionName.Extent, cmdAst, false); | ||
| return _commandCallPipelineAst; |
There was a problem hiding this comment.
false [](start = 83, length = 5)
backgroundProcess: false
| endErrorStatement = fileNameExpr.Extent; | ||
| condition = new PipelineAst(fileNameExpr.Extent, | ||
| new CommandExpressionAst(fileNameExpr.Extent, fileNameExpr, null)); | ||
| new CommandExpressionAst(fileNameExpr.Extent, fileNameExpr, null), false); |
There was a problem hiding this comment.
false [](start = 119, length = 5)
backgroundProcess: false
This 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
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.Addresses #716