1

My script generally assumes the existence of a *.txt file with settings to help it function better. However, if the script doesn't exist, it creates a local file to hold these settings. I realise there's no logical need to then read this file, but I'd like to understand why I can't.

[void][System.IO.File]::Create($PSFileName)
$ReadPS = New-Object System.IO.StreamReader($PSFileName)

Immediately after the script may (rarely) create the file, it attempts to read it, which generates the following error: New-Object : Exception calling ".ctor" with "1" argument(s): "The process cannot access the file 'C:\Temp\MyFile.txt' because it is being used by another process."

So I have to wait for the file to be available, right? Yet a simple start-sleep for 5s doesn't work. But if I wrap it in a loop with a try-catch, it works within a fraction of a second every time:

[void][System.IO.File]::Create($PSFileName)
$RCount = 0                                                             # if new file created, sometimes it takes a while for the lock to be released. 
Do{
    try{
        $ReadPS = New-Object System.IO.StreamReader($PSFileName)
        $RCount+=100
    }catch{                                                             # if error encountered whilst setting up StreamReader, try again up to 100 times.
        $RCount++
        Start-Sleep -Milliseconds 1                                     # Wait long enough for the process to complete. 50ms seems to be the sweet spot for fewest loops at fastest performance
    }
}Until($RCount -ge 100)
$ReadPS.Close()
$ReadPS.Dispose()

This is overly convoluted. Why does the file stay locked for an arbitrary length of time that seems to increase the more I wait for it? Is there anything I can adjust or add between the file creation and the StreamReader to ensure the file is available?

6
  • if i run your create code and then run Get-Content on that file, i get a red error text saying Get-Content : The process cannot access the file 'C:\Temp\Testing.txt' because it is being used by another process. if i do 3 calls in a row, the 1st fails, but the 2nd & 3rd show no errors. if i add Start-Sleep before the 1st G-C call, i get an error on that one. ///// i suspect that it is because the whole script STOPS for 5 seconds ... and the file handle is held open while the script is stopped. that seems to agree with your try/catch solution ... Commented Jan 14, 2020 at 14:59
  • 2
    File.Create produces an open file handle encapsulated by a FileStream. If you don't dispose it, that will cause a locking conflict (despite the fact that you, the original process, opened it). Use [System.IO.File]::Create($PSFileName).Dispose(), or (a little more posh) $null | Out-File $PSFileName -Encoding ascii. Commented Jan 14, 2020 at 14:59
  • Another alternative for your approval, in case the Out-File is a little too obscure: [System.IO.File]::WriteAllBytes($PSFileName, @()). Note that both this and Out-File will truncate the file if it already exists; if this is not desirable, use -Append for Out-File and AppendAllText($PSFileName, "") for File (there is, oddly enough, no File.AppendAllBytes method). Commented Jan 14, 2020 at 15:08
  • Stupid Question: Why do we need a StreamReader on an known empty file? I suppose any subsequent answer could use write functionality somewhere under System.IO... just as easily. Just thought I'd point it out... Commented Jan 14, 2020 at 15:30
  • @Steven - basically, I've spent a while refactoring all my code to run in the most logical order of 'ifs' and 'whiles', and at the end of it all, 99% of the time it's picked up an existing config file. As a final redundancy step, I create the file if it can't be sourced. And after all that work, it would be really nice to just run the same file-read step regardless of whether it's an empty config file. I can put in extra logic to omit the step or to ensure it happens successfully, but it 'feels' like the sensible thing to do is just figure out why it's locked and avoid the error. Commented Jan 14, 2020 at 19:56

3 Answers 3

2

As it was already mentioned in the comments, the method you are using does create a lock on the file, which stays until you call the close / dispose method or the powershell session end.

That's why the more you wait for it, the longer your session stays open and the longer the lock on the file is maintained.

I'd recommend you to just use New-Item instead which is the Powershell native way to do it.

Since you are creating a StreamReader object though, don't forget to close / dispose the object once you are over.

New-Item -Path $PSFileName -ItemType File
$ReadPS = New-Object System.IO.StreamReader($PSFileName)

#Stuff

$ReadPS.Close()
$ReadPS.Dispose()

Finally, if for some reason you still wanted to use [System.IO.File]::Create($PSFileName), you will also need to call the close method to free the lock.

Sign up to request clarification or add additional context in comments.

4 Comments

To be more precise: the handle remains until the object is disposed, which normally happens on the next garbage collection since the object is no longer reachable right after the statement that creates it. The time at which this happens is not deterministic; in particular, it is possible for the handle to get disposed before the end of the script or session. The nondeterminism of this just makes it harder to diagnose.
Well, I feel embarrassed for not remembering that New-Item is a thing. You may want to include the hint that -ErrorAction Ignore can be used if an existing file should not be a problem.
Ah, thanks @JeroenMostert I edited my answer so specify the dispose. Since I use Powershell so heavily, my .net became a little rusty. .
All the answers were really helpful, but this was definitely the best - thanks @SagePourpre and @Jeroen. As a relatively new Powershell user, I'd fell into the trap of using the [System.IO.File] because I was using its Exists() function elsewhere in the code, and I'd never explicitly requested a file creation before. This solution allowed fit best without modifying other code, though for completeness I'd add that | Out-Null was also useful to suppress user feedback during the file creation.
1

You have simply to close the file handle. Try:

$fh = [System.IO.File]::Create($PSFileName)
[void]$fh.Close()
[void]$fh.Dispose()
$ReadPS = New-Object System.IO.StreamReader($PSFileName)

Comments

1

The Create method returns a FileStream object. Since StreamReader is derived from Stream, My Solution was to recast as astream reader. Almost a one-liner...:

$PSFileName = 'c:\temp\testfile.txt'
$Stream = [System.IO.StreamReader][System.IO.File]::Create($PSFileName)

OR, Suggestion From Jeroen Mostert :

$PSFileName = 'c:\temp\testfile.txt'
$Stream = [System.IO.StreamReader]::New( [System.IO.File]::Create($PSFileName) )

You don't have to worry about Garbage Collection with this approach because the resulting object is referenced to the variable...

Honestly I'm not too sure about this, I believe the FileStream object can be leveraged directly to read & write, but I'm less familiar than I am with StreamReader & Writer objects, so if it were me I'd do the re-cast so I can move on, but research further later.

Also, if you use another approach I would use .CLose() instead of .Dispose(). My understanding based on the .Net documentation is close is more thorough, and calls Dispose internally anyhow...

6 Comments

This works but seems needlessly confusing -- the conversion invokes the StreamReader constructor, but is not a "real" conversion. I'd prefer an explicit [System.IO.StreamReader]::new(...) in recent versions of PowerShell, or New-Object System.IO.StreamReader (...) in older ones.
I too prefer ::New() over New-Object. But I've noticed with the System.IO.* classes they are often friendly to casting. That Said, I don't have a preference between the latter 2. At any rate, I'm going to edit with your suggestion...
This was a neat solution that I nearly implemented - but because the file creation is a minority case in my logic, it didn't feel right to modify the more common cases to fit with this solution. In this case was easier to replace the minority cases with the accepted answer, but in any other case this is a really tidy approach and would have been my preference.
Use of the .Net stuff is usually to get better performance. In fact I recently wrote an article about that. But I agree and practice the same. I don't use them unless there's a need and a lot to gain... Thanks!
Good to know. Always bugs me that so few people on SO bother up-voting good answers and questions - so you'll notice I've upvoted a few of your good answers on other Powershell questions to give you proper recognition for the work you're doing here. Have a great week
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.