In a pipeline like
GenerateLotsOfText | external-program | ConsumeExternalProgramOutput
when external-program exits, the pipeline just keeps on running until GenerateLotsOfText completes. Suppose external-program generates only one line of output then
GenerateLotsOfText | external-program | Select-Object -First 1 | ConsumeExternalProgramOutput
will stop the whole pipeline from the moment external-program generates output. This is the behavior I'm looking for, but the caveat is that when external-program generates no output but exits prematurely (because of ctrl-c for instance) the pipe still keeps on running. So I'm now looking for a nice way to detect when that happens and have the pipe terminate when it does.
It seems it's possible to write a cmdlet that uses System.Diagnostics.Process
and then use Register-ObjectEvent
to listen for the 'Exit' event, but that's quite involved with the handling of I/O streams, etc so I'd rather find another way.
I figured pretty much all other shells have this 'produce output when program exits' builtin via ||
and &&
and indeed this works:
GenerateLotsOfText | cmd /c '(external-program && echo FOO) || echo FOO' | Select-Object -First 1 | FilterOutFooString | ConsumeExternalProgramOutput
so no matter what external-program does, a line FOO will always be produced when it exits so the pipe will stop immediately (and then the FilterOutFooString takes care of only producing actual output). This isn't particularly 'nice' and has extra overhead because everything needs piping through cmd (or any other shell would work as well I assume). I was hoping pipeline chain operators would allow this natively but they don't seem to: trying the same syntax results in Expressions are only allowed as the first element of a pipeline.
The chaining does work as expected, just not in the pipeline e.g. this yields the aforementioned ParserError:
GenerateLotsOfText | ((external-program && echo FOO) || echo FOO) | Select-Object -First 1
Is there another way to accomplish this?
Update other possible approaches:
- use a separate runspace that polls
$LASTEXITCODE
in the runspace which runs the external command. Didn't find a way to do that in a thread-safe way though (e.g.$otherRunspace.SessionStateProxy.GetVariable('LASTEXITCODE')
cannot be called from another thread when the pipeline is running) - same idea but for something else to poll on: in the
NativeCommandProcessor
can be seen that it will set theExecutionFailed
failed flag on the parent pipeline once the external process exits, but I didn't find a way to access that flag, let alone in a thread-safe way - I might be onto something using a SteppablePipeline. If I get it correctly it gets an object which allows manually executing pipeline iterations. Which would allow checking
$LASTEXITCODE
in between iterations.
looking into this after having implemented the last idea which is really straightforward, none of the above are an option: the process exit code basically only gets determined once the end
block of the pipeline runs, i.e. after the upstream pipeline element produced all its output