Repository navigation
Robocopy Broken #18944
Copy link
Copy link
Closed
Labels
Resolution-By DesignThe reported behavior is by design.The reported behavior is by design.WG-Enginecore PowerShell engine, interpreter, and runtimecore PowerShell engine, interpreter, and runtime
Description
Prerequisites
- Write a descriptive title.
- Make sure you are able to repro it on the latest released version
- Search the existing issues.
- Refer to the FAQ.
- Refer to Differences between Windows PowerShell 5.1 and PowerShell.
Steps to reproduce
PowerShell 7.4.0 Preview 1
Can no longer use robocopy.exe from powershell. As robocopy.exe returns status code other than 0 for successful copies.
Repro:
- robocopy .\PowerShell-7.4.0-preview.1-win-x64\ .\temp\
Expect: Runs without creating an error object.
Actual: Creates Error Object
NativeCommandExitException: Program "Robocopy.exe" ended with non-zero exit code: 1.
Issue: This will break all of our existing automation. We use robocopy everywhere. And our scripts all use ErrorAction = Stop
Expected behavior
robocopy .\PowerShell-7.4.0-preview.1-win-x64\ .\temp\
# Files are copied. No error is raised when all files are copied.Actual behavior
robocopy .\PowerShell-7.4.0-preview.1-win-x64\ .\temp\
# An error is raised even though robocopy detected no errors.
# 1 indicates all files copied.Error details
PS > Get-Error
Exception :
Type : System.Management.Automation.NativeCommandExitException
Path : C:\Windows\system32\Robocopy.exe
ExitCode : 1
ProcessId : 17460
ErrorRecord :
Exception :
Type : System.Management.Automation.ParentContainsErrorRecordException
Message : Program "Robocopy.exe" ended with non-zero exit code: 1.
HResult : -2146233087
CategoryInfo : NotSpecified: (:) [], ParentContainsErrorRecordException
FullyQualifiedErrorId : ProgramExitedWithNonZeroCode
Message : Program "Robocopy.exe" ended with non-zero exit code: 1.
HResult : -2146233087
TargetObject : C:\Windows\system32\Robocopy.exe
CategoryInfo : NotSpecified: (C:\Windows\system32\Robocopy.exe:String) [], NativeCommandExitException
FullyQualifiedErrorId : ProgramExitedWithNonZeroCodeEnvironment data
$PSVersionTable
Name Value
---- -----
PSVersion 7.4.0-preview.1
PSEdition Core
GitCommitId 7.4.0-preview.1
OS Microsoft Windows 10.0.22621
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0Visuals
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Resolution-By DesignThe reported behavior is by design.The reported behavior is by design.WG-Enginecore PowerShell engine, interpreter, and runtimecore PowerShell engine, interpreter, and runtime
Activity
iSazonov commentedon Jan 14, 2023
What is in robocopy log?
mklement0 commentedon Jan 14, 2023
I know you're aware of at least some of this, but let me summarize:
By convention, CLIs use nonzero exit codes to signal error conditions, and that is what the - experimental, at this point -
PSNativeCommandUseErrorActionPreferencefeature and its$PSNativeCommandUseErrorActionPreferencepreference variable rely on.Robocopy does not follow this convention and uses nonzero codes as success error codes too (which communicate additional information) - see https://ss64.com/nt/robocopy-exit.html
Robocopy is a prominent example of such an "unconvential" CLI, but there's no telling how many there are, so it's impossible to anticipate all possible CLIs that require special treatment (let alone maintain tables of what nonzero codes, specifically, indicate success), so I don't think maintaining a list of exceptions is practical.
Pragmatically speaking:
I'm assuming - perhaps Steve Lee (@SteveL-MSFT) can confirm - that setting
$PSNativeCommandUseErrorActionPreferenceto$truewill be opt-in if and when the experimentalPSNativeCommandUseErrorActionPreferencefeature will be made official. Somewhat curiously, in the 7.4 preview versions it is opt-out. If the stable versions didn't make this opt-in, it would amount to a serious breaking change.Thus, in stable versions, only users who explicitly set
$PSNativeCommandUseErrorActionPreference = $truewill be affected (for the preview versions, you can simple disable the experimental feature or run$PSNativeCommandUseErrorActionPreference = $false)If the feature is deliberately enabled, a - somewhat cumbersome ad-hoc workaround is to call via a child scope:
2>$nullwould silence PowerShell's error, it would also silence the CLI's standard error output, which is undesirable. More importantly,2>$nullwouldn't prevent a script-terminating error if$ErrorActionPreference = 'Stop'were in effect.(Tangentially related: Native error handling:
$PSNativeCommandUseErrorActionPreference = $trueunexpectedly causes non-terminating, not statement-terminating errors #18368)RichardMeadowsTC commentedon Jan 14, 2023
Michael Klement (@mklement0) Thank you for the explanation. A another way to prevent the failure would be to shell out to cmd.exe and redirect stderr to stdout. I had to do that 5 to 10 years ago. When scripting in TFS. Ironically there was a tfs exe which wrote things it thought were very important to stderr.
To do that with robocopy will be very complicated if we need to detect real errors.
mklement0 commentedon Jan 14, 2023
Richard Meadows (@RichardMeadowsTC), shelling out to
cmd /cwill only help you if place an explicit& exit 0at the end - by contrast, it is fine not to redirect stderr to stdout, as it is only the exit code that matters.It is not uncommon for CLIs to write anything that's not data to stderr, which may or may not be error messages.
However, If you shell out to
cmd /c, you'll lose Robocopy's exit code, which you need if you want to robustly detect failure .Note that the
& { $PSNativeCommandUseErrorActionPreference = $false; ... }workaround does preserve the exit code.RichardMeadowsTC commentedon Jan 14, 2023
I would need to run some prototypes. I am not so sure about the cmd.exe not returning the exit code of the process which was ran. I do know powershell.exe tends to return 0 only. Anyway it is all overly complicated and would require a special wrapper for EXEs.
mklement0 commentedon Jan 14, 2023
It does - that's precisely why you need
& exit 0to prevent the nonzero Robocopy exit code from being passed through to PowerShell and again triggering the PowerShell-side error message (but, as noted,& exit 0comes at the expense of losing the specific Robocopy exit code).A simple wrapper function that avoids the PowerShell error while preserving the exit code:
RichardMeadowsTC commentedon Jan 15, 2023
There is no need for Exit 0 in cmd script. Only ever needed that when running ps1 script from cmd scripts. Not the other way around. How well last exit code is sent from cmd.exe I need to check.
mklement0 commentedon Jan 15, 2023
I've explained why
& exit 0(a) is necessary if you were to usecmd /cin order to avoid the problem at hand and (b) that doing so would be suboptimal, because Robocopy's specific exit code would be lost in the process - and, by implication, (c) why this attempt at a workaround is best avoided. Finally, (d) I've provided a workaround that is generally viable.If you have specific feedback about these points, please provide it.
RichardMeadowsTC commentedon Jan 15, 2023
I don't think you understand. I working on my tuck today. Will get back to this in the morning. But there a thing we can fix right now. The exit code just set it to what robocopy sets. But I think that is not even necessary. As I have never seen that needed in cmd scripts. Only seen that needed in powershell scripts.
All that said, I think what you do have for properly working solution is very good.
Andrew74L commentedon Jan 15, 2023
What about a built-in variable to complement $PSNativeCommandUseErrorActionPreference, such as:
$ZEROEXITCODE
The variable resets to zero after a native command is executed.
Powershell calculates $LASTEXITCODE just prior to processing any errors.
Ex:
This would maintain the benefit of $PSNativeCommandUseErrorActionPreference = $true
RichardMeadowsTC commentedon Jan 15, 2023
All we have to do is set $PSNativeCommandUseErrorActionPreference = $false
Thankfully, it isn't raising error when the exe simply writes to stderr. Like TFS Workflows were doing a few years ago.
I made an EXE to verify all this. And sample scripts. https://github.com/RichardMeadowsTC/PowerShellCmdInvoke
This is a side note. The cmd.exe will return current error level. This is different than powershell which has always tried to hide script result from app which invokes powershell.exe or pwsh.exe. I can guess they aren't changing it to prevent existing code from breaking. But here clearly lots of code is going to be broken. Much more than was broken with Pwsh.exe version 7.3.
Andrew74L commentedon Jan 15, 2023
Then set it back to $true after running the command. If not, why have the feature at all?
I think that's fine as a workaround solution, but it's not ideal.
RichardMeadowsTC commentedon Jan 15, 2023
The reason for not setting it back is that it never should have been enabled. Old scripts that we have are well tested and check $LASTEXITCODE where necessary.
mklement0 commentedon Jan 17, 2023
Please see the following proposal for a possible solution:
2 remaining items
Andrew74L commentedon Jan 28, 2023
For tools that don't follow the non-zero exit is error convention, there is the option to set (by default or otherwise)
$PSNativeCommandUseErrorActionPreference = $falseand continue to use $LASTEXITCODE for error handling. The other option is to employ some method for 'tweaking' exit codes so as to prevent 'false positives' from triggering Powershell's error detection, thereby maintaining consistency of error handling.I would suggest that a good reason for favoring the second option is that eventually, all CLI executables that still have at least moderate usage, are going to be 'consumed' by Crescendo-based modules, and the authors of those modules can handle whatever 'tweaks' are required to produce the desired response. So I say make
$PSNativeCommandUseErrorActionPreference = $truethe default setting, and provide a mechanism for altering non-zero, non-error exit codes, that most Powershell users will rarely if ever need to use.mklement0 commentedon Jan 28, 2023
Steve Lee (@SteveL-MSFT), please confirm that
$PSNativeCommandUseErrorActionPreferencewill default to$falsein release versions - not doing so would be a massively breaking change, not least because an effective$ErrorActionPreference = 'Stop'setting would then start breaking scripts with external-program calls that happen to return non-zero exit codes (which are currently ignored).Proceeding on that assumption:
Setting
$PSNativeCommandUseErrorActionPreference = $trueis therefore a deliberate act and the user should be assumed to understand the consequences: any non-zero exit code reported reports a non-terminating error; as such, it can be controlled via$ErrorActionPreference, which, however, is just as cumbersome as temporarily setting$PSNativeCommandUseErrorActionPreference = $true$PSNativeCommandUseErrorActionPreference = $trueunexpectedly causes non-terminating, not statement-terminating errors #18368try/catchsolution - see below.$PSNativeCommandUseErrorActionPreference = $truecould continue to happily ignore exit codes or, preferably, always test$LASTEXITCODEafterwards, CLI-appropriately.Building exception list for specific CLIs along with the specific non-zero exit codes that constitute success would mean playing an everlasting game of catch-up, as new non-conformant CLIs are discovered; it would lead to the same mess as with
$PSNativeCommandArgumentPassing'sWindowsmode - see Parameter invalid when it contains a colon. Works prior to 7.3.0. #18554 (comment)Not everyone will or should be expected to be using Crescendo modules (if available at all), so the best, exception-free solution is to make per-command overrides of
$PSNativeCommandUseErrorActionPreferenceas simple as possible:A
try/catchapproach is reasonable concise, but requires fixing Native error handling:$PSNativeCommandUseErrorActionPreference = $trueunexpectedly causes non-terminating, not statement-terminating errors #18368The proposed
Invoke-NativeCommand- see Introduce a new cmdlet for calls to native (external) programs #18991 - could help too; assumingincas its built-in alias:Arguably, analogous to
bash'sset -e, no error should be triggered for external-program calls in the non-final link of a pipeline chain, i.e with&&and||- this is currently not the case. If this worked, a concise - albeit slightly obscure - solution would be:Andrew74L commentedon Jan 30, 2023
The following assumes
$PSNativeCommandUseErrorActionPreference = $trueis the default.Assume "$PSHOME\ZeroErrorCodes.json" store the minimum magnitude non-error exit codes (with sign), for CLI executables that do not follow the non-zero exit code implies error convention. Example content:
At Powershell startup:
When user inputs a native command, Powershell does this processing:
Consider these scenarios.
User runs Robocopy which copies zero files without error:
User runs Robocopy which copies files without error:
User runs Robocopy which returns error:
CLI whose json value is zero:
An exe not in json file:
Same but user sets ZeroErrorCode immediately prior to execution:
SteveL-MSFT commentedon Jan 30, 2023
Discussion in the team we agree that this feature is working as expected and we don't want to special case
robocopy. Instead, I've opened a doc bug to ensure we have a good example for cases likerobocopyto turn it off and on.mklement0 commentedon Jan 30, 2023
To offer an alternative, let me add solutions that already work / will work with
$PSNativeCommandUseErrorActionPreferenceset to$false.In other words: for general per-native-program-call error handling.
Note the unfortunate need to enclose even
throwandexitin$(...), otherwise they'll be treated as (usually non-existent command names) - see #10967 for background.jazzdelightsme commentedon Jan 30, 2023
Hang on... I didn't see a response to mklement's request for confirmation that we would still have existing behavior in release versions. Your statement, Steve Lee (@SteveL-MSFT) almost sounds like "no, we are turning this on, and doc'ing it better". If that is the case, please allow me to respectfully scream "bloody murder". I've already gotten bug reports from users of my modules for robocopy, and git, and I can't even remember what else. I assumed this was so clearly such a massively breaking change that it surely wouldn't stand...
SteveL-MSFT commentedon Jan 30, 2023
Dan Thompson (@jazzdelightsme) I believe it was mentioned in a separate issue that
$PSNativeCommandUseErrorActionPreferenceis default to$falseand only set to$truein 7.4 previews to get feedback.mklement0 commentedon Jan 30, 2023
Glad to hear it, Steve Lee (@SteveL-MSFT), but it's somewhat disheartening that it took three separate pleas to get you to address this aspect.
sba923 commentedon Jul 3, 2023
Steve Lee (@SteveL-MSFT) Is there an explanation for the fact that the
NativeCommandExitExceptiondisappears if the native command's output is discarded using2>&1 | Out-Null? (I'm testing with 7.4.0-preview.4)mklement0 commentedon Aug 2, 2023
Stéphane BARIZIEN (@sba923), the current behavior is a bit counter-intuitive:
A
2>redirection also swallows theNativeCommandExitException, because the latter is implemented as a non-terminating error and is therefore treated the same as the external program's stderr output.$PSNativeCommandUseErrorActionPreference = $trueunexpectedly causes non-terminating, not statement-terminating errors #18368, and if it were converted to a statement-terminating one, it would no longer be swallowed.However, if you have
$ErrorActionPreference = 'Stop'in effect, the script aborts (as intended) and theNativeCommandExceptionsurfaces (but not the native program's stderr output, as intended).sba923 commentedon Aug 2, 2023
This is indeed far from intuitive...