-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Support backgrounding pipelines with ampersand #3360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Please rebase on master so commits that aren't yours go away. |
81b84fb to
564cc3f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The api is public so this is a breaking change. We need to introduce a new constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we maybe inherit another class from PipelineAst instead? Something like BackgroundPipelineAst.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a new Ast is too much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another public api - so this is breaking as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add the proposed test (which does pass).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Utils.EmptyArray<Expression>() instead of new Expression[0].
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've verified that it only executes once in the background task.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It takes > 10 s so nightly is probably right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm changing it to report the pipeline extent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is 'cd' here? Alias? If so we should use full name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This title is difficult to understand. Maybe better "Background job don't overwrite the parent process 'Pid'"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworded for clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same about title
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworded for clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use "Should Not BeExactly".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use "Should Not BeExactly".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
30642fc to
80e4547
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please replace alias with full name. Below the alias is used too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please replace alias foreach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please recheck - I dont see the fixes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still open.
|
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that as simple as subtracting pipeElementAsts[0].Parent.Extent.StartOffset ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can remove the resource string - this is the only reference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
& 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment has not been addressed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be fixed now.
8905083 to
80041b2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use C# 7.0 pattern here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We aren't targeting C# 7.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe "BeExactly" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe "BeExactly" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use Receive-Job $j -Wait as above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unnecessary churn.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still open.
…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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored the code.
| Expression.NewArrayInit(typeof(CommandParameterInternal[]), | ||
| !pipelineAst.BackgroundProcess ? pipelineExprs : Utils.EmptyArray<Expression>()), | ||
| Expression.Constant(pipeElementAsts), | ||
| redirectionExpr, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
backgroundProcess: false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| /// <exception cref="PSArgumentNullException"> | ||
| /// If <paramref name="extent"/> or <paramref name="commandAst"/> is null. | ||
| /// </exception> | ||
| public PipelineAst(IScriptExtent extent, CommandBaseAst commandAst) :this (extent, commandAst, false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
backgroundProcess: false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
|
|
||
| var pipeLineAst = new PipelineAst(this.Extent, cmdAst); | ||
| var pipeLineAst = new PipelineAst(this.Extent, cmdAst, false); | ||
| var funcStatements = ConfigurationExtraParameterStatements.Select(statement => (StatementAst)statement.Copy()).ToList(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
backgroundProcess: false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
|
|
||
| var returnPipelineAst = new PipelineAst(this.Extent, setItemCmdlet); | ||
| var returnPipelineAst = new PipelineAst(this.Extent, setItemCmdlet, false); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
backgroundProcess: false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
backgroundProcess: false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Annoying corner case - you need to escape } in vName. CodeGeneration.EscapeVariableName will handle that for you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
|
@lzybkr it looks like all the feedback has been addressed? |
| redirectionExpr, | ||
| _functionContext); | ||
| Expression invokePipe; | ||
| if (pipelineAst.BackgroundProcess) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can revert this change now - it can't be a background process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| int i, commandsInPipe; | ||
|
|
||
| if (firstCommandExpr != null && !pipelineAst.BackgroundProcess) | ||
| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can revert this change now - it can't be a background process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| /// <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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
false [](start = 83, length = 5)
backgroundProcess: false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
false [](start = 119, length = 5)
backgroundProcess: false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
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