Skip to content

Conversation

@BrucePay
Copy link
Collaborator

@BrucePay BrucePay commented Mar 17, 2017

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

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.

Addresses #716

@lzybkr
Copy link
Contributor

lzybkr commented Mar 17, 2017

Please rebase on master so commits that aren't yours go away.

@SteveL-MSFT SteveL-MSFT changed the title Brucepay ampersand Support backgrounding pipelines with ampersand Mar 17, 2017
@BrucePay BrucePay force-pushed the brucepay-ampersand branch from 81b84fb to 564cc3f Compare March 17, 2017 23:07
Copy link
Contributor

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.

Copy link
Collaborator

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Contributor

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Contributor

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 | % { $_ } &

Copy link
Collaborator Author

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).

Copy link
Contributor

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].

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Contributor

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Contributor

@lzybkr lzybkr left a 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.

@lzybkr
Copy link
Contributor

lzybkr commented Mar 21, 2017

@BrucePay - please fix the build break in appveyor - it looks like it catches a breaking change in the api.

@BrucePay BrucePay force-pushed the brucepay-ampersand branch from b4d2822 to 30642fc Compare March 21, 2017 23:17
Copy link
Collaborator

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Collaborator

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'"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded for clarity.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same about title

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded for clarity.

Copy link
Collaborator

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".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Collaborator

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".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra line.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Collaborator

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace alias foreach.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed.

Copy link
Collaborator

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed.

Copy link
Collaborator

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open.

@lzybkr
Copy link
Contributor

lzybkr commented Mar 26, 2017

Just a general comment about the design - do we have a good feel for how:

  1. people use & today?
  2. how we think they'll use it in the future?

There was the hypothesis that & is mostly used to launch windowed applications. If this is true, launching PowerShell to launch the application will feel slow because launching PowerShell is not fast. If we could avoid launching PowerShell first, that would be ideal, though that can only work if the pipeline is only running external commands.

On the other hand, if the primary scenario really is background jobs, then this PR will be a great syntactic shortcut.

@BrucePay
Copy link
Collaborator Author

The primary scenario really is to background pipelines. The need to launch blocking apps was simply a forcing function.

Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Contributor

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 ?

Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Collaborator

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

Copy link
Collaborator Author

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".

@BrucePay BrucePay force-pushed the brucepay-ampersand branch 2 times, most recently from 379aff2 to 06ae76d Compare April 18, 2017 22:51
Copy link
Contributor

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.

Copy link
Collaborator Author

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.

@BrucePay BrucePay force-pushed the brucepay-ampersand branch from 8905083 to 80041b2 Compare April 23, 2017 23:53
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe "BeExactly" ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe "BeExactly" ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Collaborator

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary churn.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open.

@SteveL-MSFT SteveL-MSFT added this to the 6.0.0-beta1 milestone May 1, 2017
@SteveL-MSFT SteveL-MSFT modified the milestones: 6.0.0-beta, 6.0.0-beta1 May 10, 2017
…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
@BrucePay BrucePay force-pushed the brucepay-ampersand branch from 80041b2 to 52b8460 Compare May 15, 2017 18:47
firstCommandExpr != null ? ExpressionCache.FalseConstant : ExpressionCache.TrueConstant,
Expression.NewArrayInit(typeof(CommandParameterInternal[]), pipelineExprs),
firstCommandExpr != null && !pipelineAst.BackgroundProcess ? ExpressionCache.FalseConstant : ExpressionCache.TrueConstant,
Expression.NewArrayInit(typeof(CommandParameterInternal[]),
Copy link
Contributor

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.

Copy link
Collaborator Author

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,
Copy link
Contributor

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.

Copy link
Collaborator Author

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backgroundProcess: false

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Collaborator Author

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backgroundProcess: false

Copy link
Collaborator Author

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backgroundProcess: false

Copy link
Collaborator Author

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);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backgroundProcess: false

Copy link
Collaborator Author

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backgroundProcess: false

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Contributor

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;
Copy link
Contributor

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.

Copy link
Collaborator Author

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);
Copy link
Contributor

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@SteveL-MSFT
Copy link
Member

@lzybkr it looks like all the feedback has been addressed?

redirectionExpr,
_functionContext);
Expression invokePipe;
if (pipelineAst.BackgroundProcess)
Copy link
Contributor

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,
Copy link
Contributor

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)
{
Copy link
Contributor

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.

Copy link
Collaborator Author

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)
{
Copy link
Contributor

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.

Copy link
Collaborator Author

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; }
Copy link
Contributor

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.

Copy link
Collaborator Author

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;
Copy link
Contributor

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

Copy link
Collaborator Author

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);
Copy link
Contributor

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review - Needed The PR is being reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants