-
Notifications
You must be signed in to change notification settings - Fork 7.5k
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
Implement -Extension parameter in New-TemporaryFile cmdlet #4612
Conversation
…c extension to New-TemporaryFile using a new -Extension parameter.
return; | ||
} | ||
|
||
if (ShouldProcess(filePath)) |
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.
While this implicitly fixes the bug in the original code that referred to environment variable TEMP
, which can only be assumed to exist in Windows, note that you've changed the logic from reporting the directory path to the file path.
In other words: to remain consistent with how things worked before, it should remain if (ShouldProcess(tempPath))
I guess the logic is: New-TemporaryFile
operates on the temp. directory as its target, and creates a new file inside it.
What if: Performing the operation "New-TemporaryFile" on target "C:\Users\jdoe\AppData\Local\Temp".
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.
Ok. Makes sense.
string fileName = Path.GetRandomFileName(); | ||
fileName = Path.ChangeExtension(fileName, _extension); | ||
filePath = Path.Combine(tempPath, fileName); | ||
if(!File.Exists(filePath)) |
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.
Unfortunately, you can run into race conditions with this approach: Between the time you check for the existence of a file with a given name and a subsequent attempt to create it, someone else could have created such a file, at least hypothetically.
The more robust approach is to attempt to create the file and let that attempt fail if a file with that name happens to exist already (which is what file mode CreateNew
does), catch the resulting IOException
exception, and try 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 was aware of this when writing it but did not consider handling it specifically since the global catch would have still handled it and the probability of having a clash of a random file name in such a short time period is really low. However, I liked your idea and as your approach does not seem to have a downside, I implemented your suggestion.
… file could get created matching the random file name in between the point of checking its presence and its creation. Revert SupportsShouldProcess behaviour to report temp path. Those changes were requested in the code review of PR PowerShell#4612
} | ||
catch (Exception e) | ||
|
||
if (!creationOfFileSuccessful) | ||
{ | ||
WriteError( |
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.
Thanks for the quick turnaround.
The following, which predates your PR, may not be worth doing, because it is technically a breaking change, but it's worth pointing out:
Based on the official cmdlet error-reporting guidelines, this should be a (statement)-terminating error: "Cmdlets that can accept only 0-1 input objects and generate only 0-1 output objects should treat errors as terminating errors and generate terminating exceptions." - https://msdn.microsoft.com/en-us/library/ms714412%28v=vs.85%29.aspx
cc @SteveL-MSFT
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.
Thanks for the quick review as well. 👍 You have an interesting point. My thinking is: If one has got a script in which a temporary file gets created, then in order to use it, one would need to use the returned object because otherwise one does not know where the temporary file is. Basically, the cmdlet is only useful if one receives a returned object. I am not sure if this is a case of the users having to check that that their variable is not $null
or that the error should be terminating.
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 agree that with @mklement0 a terminating error should be thrown. But it would be a breaking change and so we should continue with the existing pattern.
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.
Understood, @PaulHigin.
@bergmeister: Just to clarify: by default, the script would continue to run even if a terminating error were reported with ThrowTerminatingError()
, because only the statement is terminated; the difference between reporting a non-terminating vs. a terminating error only matters with respect to -ErrorAction
use and try
/ catch
use.
Since the conceptual fog around error handling lifted for me personally only recently, perhaps you'll find this useful too: Our Error Handling, Ourselves - time to fully understand and properly document PowerShell's error handling
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 point is to follow the standard, make the breaking change and "fix" it in our interop module.
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.
@lzybkr Could you please comment - should we make the breaking change or exclude it?
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 terminating error is appropriate and an unlikely breaking change, but we can make that change in a different PR.
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 there already an issue for the terminating error change?
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.
@SteveL-MSFT Now there is: here
I had a look at the other suggestions about how to improve my tests. I can certainly make those but it would be more important if someone could make a decision or approval whether the currently used algorithm is OK or if not how it should be modified.
{ | ||
Remove-Item $tempFile -ErrorAction SilentlyContinue -Force | ||
} | ||
Remove-Item $tempFile -ErrorAction SilentlyContinue -Force |
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.
Suggestion: To make cleanup reliable, use 'try' around the Should statements and place 'Remove-Item' in the finally block.
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.
Ok. Will do.
@@ -12,34 +12,73 @@ namespace Microsoft.PowerShell.Commands | |||
[OutputType(typeof(System.IO.FileInfo))] | |||
public class NewTemporaryFileCommand : Cmdlet | |||
{ | |||
private string _extension = ".tmp"; |
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 remove the _extension field and just use the property:
[Parameter(Position = 0)]
[ValidateNotNullOrEmpty]
public string Extension
{
get;
set;
} = ".tmp";
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 is CoreFX default - we can exclude this and simply check that the parameter presents.
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 the same but rather followed the existing style. What about a more elegant one-liner?
public string Extension { get; set; } = ".tmp";
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 directly use Path.GetRandomFileName()
if the parameter absent.
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.
@iSazonov I am not sure if you meant GetTempFileName()
because Path.GetRandomFileName()
gives you a random extension and would therefore be a breaking change. If you really meant GetTempFileName
then I disagree because this function does not just get you a file name, it also creates it for you, which means we would end up with 2 different ways of creating a temp file with 2 different error handlings...
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, I meant that
if (Extension parameter absent)
{
return Path.GetFileName()
}
else
{
Here new code to create a temp file with explicit extension.
}
We should follow CoreFX if we can. Here we can. Extension
parameter will be used rarely so it is good two code paths.
int attempts = 0; | ||
bool creationOfFileSuccessful = false; | ||
|
||
// In case the random temporary file already exists, retry |
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 am not convinced that this retry loop is necessary. What are the odds that the random file name already exists?
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 approved pattern
internal static DirectoryInfo CreateTemporaryDirectory() |
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 odds are low but we could make them really low by using e.g. Guids for the file name instead of Path.GetRandomFileName()
because the latter is only 8 characters long...
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 we should follow CoreFX while we can and fix issues there.
} | ||
catch (Exception e) | ||
|
||
if (!creationOfFileSuccessful) | ||
{ | ||
WriteError( |
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 agree that with @mklement0 a terminating error should be thrown. But it would be a breaking change and so we should continue with the existing pattern.
"NewTemporaryFileWriteError", | ||
ErrorCategory.WriteError, | ||
tempPath)); | ||
new IOException("Could not find an available temporary file name in {tempPath}."), |
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 exception message string should go into a resx file so it can be localized. I think this would be the right file: Microsoft.PowerShell.Commands.Utility\resources\UtilityCommonStrings.resx
@@ -12,34 +12,73 @@ namespace Microsoft.PowerShell.Commands | |||
[OutputType(typeof(System.IO.FileInfo))] | |||
public class NewTemporaryFileCommand : Cmdlet |
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 add a comment section here that describes this public property.
/// </summary> | ||
[Parameter(Position = 0)] | ||
[ValidateNotNullOrEmpty] | ||
public string Extension |
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.
Consider validating the value in the setter for valid characters instead of rely only on an exception in your EndProcessing implementation. Something like...
if (!string.IsNullOrEmpty() && value.IndexOfAny(Path.GetInvalidFileNameChars() != -1)
{
throw...
}
…#4612: - Check for invalid characters in Extension parameter - Simplify Extension property and document it in the class documentation. - Use resource strings for error messages - Improved tests to improve the test cleanup using a try-finally approach as suggested in the code review of PR PowerShell#4612. What is left to be discussed is the error handling in the case of a file conflict in the temp folder (i.e. should we retry at all or use a different approach)
@@ -174,4 +174,10 @@ | |||
<data name="TypeNotSupported" xml:space="preserve"> | |||
<value>'{0}' is not supported in this system.</value> | |||
</data> | |||
<data name="InvalidCharacterInParameter" xml:space="preserve"> | |||
<value>Parameter '{0}' contains at least one invalid character. It's value is: '{1}'</value> |
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 change "it's" to "its".
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.
Oops. Sure.
I have addressed the main suggestions but would like to wait until we have reached a consensus on the preferred approaches with the issues of the possibility of not being able to create a temp file and how to deal with it
I think in general it is good that we move away from I find it great that many people share their thoughts but at the end I would like to have a final word from someone before I make a commit to avoid going back and forth. |
A GUID based filename would certainly solve the problem but after thinking about it some more I am Ok with the retry loop. It feels like a reasonable compromise to write an error and fail after 10 tries. Also I am fine with initializing the new property with ".tmp". Using GUIDs feels like overkill. But maybe we could add a switch to allow it as an option. Something like: New-TemporaryFile -GuidBasedName |
…xtension parameter in New-TemporaryFile as part of PR PowerShell#4612 In Pester v3 the 'Should Throw' assertion is quite limited, see here: pester/Pester#673 Therefore the example in the guide here was followed: https://github.com/PowerShell/PowerShell/blob/68bcd4b5284bb169c52a570afbfdd1bb70d0de10/docs/testing-guidelines/WritingPesterTests.md
@PaulHigin Just to recap: Before this PR, the cmdlet was simply calling
To conclude: Without the retry logic, we would have not changed the error behaviour of the cmdlet because it was always just throwing if there is a problem (the Unix implementation of
Therefore I also agree that keeping the retry mechanism improves the error behaviour without making breaking changes to its past behaviour and the |
Why do you think this problem exists? If so, right way is - we have to open Issue in CoreFX and fix the problem there without complicating PowerShell Core code. |
} | ||
finally | ||
{ | ||
Remove-Item $tempFile -ErrorAction SilentlyContinue -Force |
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 use this multiple times - it is good candidate for AfterEach
.
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.
Ok. But I will wait with updating the tests for the moment as it is more important to know the final decision first whether the implementation would get accepted or if it requires a fix in corefx first.
} | ||
catch | ||
{ | ||
$_.FullyQualifiedErrorId | Should Be "NewTemporaryInvalidArgument,Microsoft.PowerShell.Commands.NewTemporaryFileCommand" |
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.
Currently we use our pattern:
{ New-TemporaryFile -Extension $invalidFileNameChar -ErrorAction Stop } | ShouldBeErrorId "NewTemporaryInvalidArgument,Microsoft.PowerShell.Commands.NewTemporaryFileCommand"
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.
Ok, can do. I was following the example given in the PowerShell Pester guideline here. Should the guideline then be updated 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.
Yes. Could you please open new Issue for that?
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'd prefer if you open the issue because the person opening an issue or PR is implicitly responsible for arguing and leading the discussion whether the guideline is out of date and/or should be changed. Therefore you are definitely the better person for that.
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.
person opening an issue or PR is implicitly responsible for arguing and leading the discussion
No, we welcome any error report - you don't have to "leading the discussion". Every time you see a bug - feel free to open new Issue (and forget it 😄) MSFT experts will help to open a Issue if the issue is difficult to understand.
After further thinking, I believe that if the CoreFX algorithm is bad, it must be corrected in CoreFX, not here. try
{
var tempFile = GetTempFileName();
if (parameter Extension present)
{
replace extension in tempFile
}
WriteObject(tempFile);
}
catch
{
WriteError(...) or write terminating error;
} |
/// </summary> | ||
protected override void EndProcessing() | ||
{ | ||
// Check for invalid characters in extension | ||
if (!string.IsNullOrEmpty(Extension) && Extension.IndexOfAny(Path.GetInvalidFileNameChars()) != -1) |
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 guess it makes sense if we want to fix the problem somehow, but we just write a error message. So it's best to just follow CoreFX, where this check is done and just handle the exception. As a result, we will have only one block try-catch
- see my post
@iSazonov In the beginning I was thinking that a fix in corefx would be nicer as well since this is a common problem that also others have (see e.g. here). |
Sorry for being unclear - I meant that it is better to fix "the file name uniqueness limitation to 65,535 files" algorithm in CoreFX not extension - adding new overload for extension require .Net Standard change - it is very long process. Also it will be not approved without real business case - mentioned issue is unuseful because don't contains any arguments. |
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 this is merged, the documentation of New-TemporaryFile
will need to be updated as it describes in some detail the current algorithm.
/// Use a Guid as a file name to allow for more than 65,535 unique file names in the temporary folder. | ||
/// </summary> | ||
[Parameter()] | ||
public SwitchParameter GuidBasedName { get; 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.
I'm not a fan of this parameter name, maybe -UseGuid
?
filePath = Path.Combine(tempPath, fileName); | ||
using (new FileStream(filePath, FileMode.CreateNew)) { } | ||
creationOfFileSuccessful = true; | ||
WriteVerbose("Created temporary file {filePath}."); |
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.
Shouldn't this be
WriteVerbose($"Created temporary file {filePath}.");
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. Thanks
} | ||
catch (Exception e) | ||
|
||
if (!creationOfFileSuccessful) | ||
{ | ||
WriteError( |
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 terminating error is appropriate and an unlikely breaking change, but we can make that change in a different PR.
} | ||
fileName = Path.ChangeExtension(fileName, Extension); | ||
filePath = Path.Combine(tempPath, fileName); | ||
using (new FileStream(filePath, FileMode.CreateNew)) { } |
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.
A comment would be good here - explaining why we create the file and do not remove it, similar to Path.GetTempFileName
.
<data name="InvalidCharacterInParameter" xml:space="preserve"> | ||
<value>Parameter '{0}' contains at least one invalid character. Its value is: '{1}'</value> | ||
</data> | ||
<data name="CouldNotFindTemporaryFilename" xml:space="preserve"> |
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.
Find
doesn't seem quite right, I would use Create
, and change the message to Could not create a temporary file name in {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.
OK, can do. I wasn't 100% happy with the error message myself either.
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 we change the text we should rename CouldNotFindTemporaryFilename
to CouldNotCreateTemporaryFilename
.
/// </summary> | ||
protected override void EndProcessing() | ||
{ | ||
// Check for invalid characters in extension | ||
if (!string.IsNullOrEmpty(Extension) && Extension.IndexOfAny(Path.GetInvalidFileNameChars()) != -1) |
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.
Extension
can't be null or empty because of ValidatenotNorOrEmpty
, so this test is not necessary.
$tempFile = New-TemporaryFile 'csv' # check that one can also omit the period and parameter name | ||
try | ||
{ | ||
Test-Path $tempFile | Should be $true |
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.
Shouldn't this be $tempFile | Should Exist
?
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 following the pattern of an existing test but you are right that Should Exist
is better.
try | ||
{ | ||
|
||
Test-Path $tempFile | Should be $true |
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.
Shouldn't this be $tempFile | Should Exist
?
|
||
Test-Path $tempFile | Should be $true | ||
$tempFile | Should BeOfType System.IO.FileInfo | ||
It "creates a new temporary file with a specific extension using the -Extension parameter" { |
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 test has repeated code, you should use the -Testcases
feature of Pester instead.
A simple way of rewriting this PR in order to keep
A retry loop is then only necessary in the |
@lzybkr @mklement0 Thanks for the discussion! |
@iSazonov: 👍
Well, I'm a customer, and I have that need. |
@iSazonov But it sounds like the dotNet implementation of file creation is susceptible to race and file access permission errors where a) two processes end up with access to the same temp file and b) file access permission is incorrect and possibly detrimental to one of the processes owning the file causing a security hole. Is this correct? So the conclusion is that currently there is no way to safely create a new file until these dotNet layer bugs are fixed. You are suggesting that we create our own native layer file creation for both Windows and Linux platforms that do not have these weaknesses, and then try and get these changes into dotNet. Correct? Thanks! |
Formally it is Ok. Really it has race and security hole.
It is not CoreFX bugs. This is due to the misuse of these functions. With glibc we can get the same - that's why My suggestion for CoreFX is add something like: public static string GetTempFileName(string Suffix)
public static string GetTempFileName(string Prefix)
public static string GetTempFileName(string InTempDirectory)
public static string GetTempFileName(string Prefix, string Suffix)
public static string GetTempFileName(string Prefix, string Suffix, int VariableLength)
public static string GetTempFileName(string Prefix, string Suffix, int VariableLength, string InTempDirectory)
// And API to create temp directory Yes, my suggestion is implement the APIs temporary in PowerShell. |
I think it would be best to modify the existing algorithm using the proposal of @mklement0 , which sounds like an overal improvement and closes the 'hole' (assuming that the rename method does not change the file permissions). I am not sure if this 'hole' is a bit superficial because on Windows any other process can edit the generated temporary file and it would only affect security if a developer directly injected content of the temporary file into other methods in their program or script(please correct me if i'm wrong). Any decent developer should know that any input has the possibility of being a vulnerability for injection methods and therefore needs to assess the risk and potentially defend against it. We should probably have a chat with corefx (maybe we don't have enough understanding of the security model that underlines corefx) but trying to do all the low level stuff ourselves makes it look like to me that we are either overegging the pudding or that corefx is unsafe. |
@bergmeister Sorry for the delay.
|
…l method Path.GetTempFileName() method by default (because this method takes care of the file creation, which would've let to other issues) but keep the retry logic, which is mainly needed in case the -Extension parameter is used.
temporaryFilePath = temporaryFileWithCustomExtension; | ||
} | ||
creationOfFileSuccessful = true; | ||
WriteVerbose($"Created temporary file {temporaryFilePath}."); |
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 probably be a WriteDebug as that tempfile may be deleted
…sed for temporary work whereby the file probably gets deleted by the user at some point. This was suggested in the code review of PR 4612
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.
Leave a comment
/// <summary> | ||
/// Specify a different file extension other than the default one, which is '.tmp'. The period in this parameter is optional. | ||
/// </summary> | ||
[Parameter(Position = 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.
It doesn't make sense for single parameter and block future enhancements. I think we should remove this.
/// </summary> | ||
[Parameter(Position = 0)] | ||
[ValidateNotNullOrEmpty] | ||
public string Extension { get; set; } = defaultExtension; |
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 believe we should use simple code route so the initialization should be removed.
string filePath = null; | ||
string tempPath = System.Environment.GetEnvironmentVariable("TEMP"); | ||
// Check for invalid characters in extension | ||
if (Extension.IndexOfAny(Path.GetInvalidFileNameChars()) != -1) |
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 seems we should move the code in BeginProcessing()
.
try | ||
{ | ||
temporaryFilePath = Path.GetTempFileName(); // this already creates the temporary file | ||
if (Extension != defaultExtension) |
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.
That is why we must follow the @mklement0 proposal and implement two code routes. Here we do unneeded checks in loop. This loop only makes sense for a very loaded scenarios. If we want to guarantee anything in this cycle, the number of attempts should be greater than 10000 - see Unix implementations.
Also we should exclude creationOfFileSuccessful
- if we get file name we should return immediately without extra checks.
if ( !Extension.IsPresent() )
{
return Path.GetTempFileName();
}
else
{
return GetFileNameWithExtension(Extention);
}
} | ||
catch (IOException) // file already exists -> retry | ||
{ | ||
attempts++; |
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 remove - we do this in cycle header.
#> | ||
|
||
Describe "NewTemporaryFile" -Tags "CI" { | ||
|
||
It "creates a new temporary file" { | ||
$tempFile = New-TemporaryFile | ||
$defaultExtension = '.tmp' |
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 move the line to BeforeAll.
{ | ||
Remove-Item $tempFile -ErrorAction SilentlyContinue -Force | ||
{ New-TemporaryFile -Extension $invalidFileNameChar -ErrorAction Stop } | ShouldBeErrorId "NewTemporaryInvalidArgument,Microsoft.PowerShell.Commands.NewTemporaryFileCommand" |
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.
Doesn't make sense to test for all invalid chars - enough of one - we should check only the error id.
[System.IO.Path]::ChangeExtension($tempFile, $defaultExtension) | Should Not Exist | ||
$tempFile | Should BeOfType System.IO.FileInfo | ||
$tempFile.Extension | Should be ".csv" | ||
$tempFile.FullName.EndsWith($extension) | Should be $true |
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 remove the line - previous line already make the check.
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 last test is for test cases like .txt.csv
because the Extension
property returns only .csv
in this case
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.
Thanks for clarify.
We use CoreFX method Path.ChangeExtension()
to change the extention so we shouldn't have many tests which test CoreFX - one test is enough for our code..
@{ extension = '..csv' } | ||
@{ extension = 'txt.csv' } | ||
@{ extension = '.txt.csv' } | ||
) |
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 move to It
block.
It "creates a new temporary file with a specific extension" -TestCases $extensionParameterTestCases -Test @(
@{ extension = 'csv' }
@{ extension = '.csv' }
@{ extension = '..csv' }
@{ extension = 'txt.csv' }
@{ extension = '.txt.csv' }
) {
AfterEach { | ||
if ($null -ne $script:tempFile) | ||
{ | ||
Remove-Item $script:tempFile -ErrorAction SilentlyContinue -Force # variable needs script scope because it gets defined in It block |
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 move the comment on separate line.
{ | ||
temporaryFilePath = Path.GetTempFileName(); // this already creates the temporary file | ||
if (Extension != defaultExtension) | ||
{ |
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 avoid string comparisons using an operator, instead preferring string.Equals
and explicitly specify the comparison with either Ordinal or OrdinalIgnoreCase. In this case, Ordinal is probably fine, but we would want a test to make sure File.Move works fine when only the case of the extension differs (e.g. -Extension .TMP
.)
One other thought - if we compare the extension of temporaryFilePath
with Extension
, we can avoid a file move if GetTempFileName
changed it's algorithm and the new algorithm matches what the user specifies for Extension
- an unlikely scenario I suppose, but I don't see a downside to this code change.
if (temporaryFilePath != null) | ||
{ | ||
File.Delete(temporaryFilePath); | ||
} |
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 if this throws? We won't retry.
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 then simply try File.Delete with an empty catch block instead?
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 the retry count is 10, that seems fine. If it was a huge number, then I'd say probably not. A short sleep in the catch might not hurt either, like 10ms or something.
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.
Would it make sense to write a warning if we're not able to delete it?
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.
At least a warning seems reasonable. But I am slightly more leading to actually throwing a (terminating) error because if deleting the temporary file that just got created failed then I suspect that in most cases there is rather a fundamental problem with permissions. WDYT?
-When using the defaultExtension then fall back to the old behaviour without retry logic -In the custom extension retry loop: if file deletion fails in catch block, catch the error and return error -Test improvements and test specifically for .tmp and .TMP (the latter on Unix only)
I addressed the main points made in the review:
|
…en testing against the path.
I have updated the test to pass since one test did not make sense on Mac due to the the fact that Mac sees files with different casing as the same file similar to Windows. |
- Remove position on Extension parameter since this is the only parameter at the moment. - Move validation logic to BeginProcessing - Move constant test variable to BeforeAll block
…o TemporaryFile # Conflicts: #src/Microsoft.PowerShell.Commands.Utility/resources/UtilityCommonStrings.resx
Currently we have new API request in CoreFX and CoreFX team has post 2.1 plans to enhance the API. Feel free to vote for the API. |
#4215: Add ability to create a temporary file with a specific extension to
New-TemporaryFile
Implemented exactly as described by adding an optional
-Extension
parameter to specify the extension e.g. as.csv
or justcsv
.Because of the required change to call different .NET APIs, the cmdlet had to be slightly rewritten to cater for the special case that the file to be created already exists in the temp folder.