Steve can help
Steve can help

Reputation: 512

Powershell StreamReader - how to wait for a new file to be readable

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?

Upvotes: 1

Views: 3197

Answers (3)

Steven
Steven

Reputation: 7087

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

Upvotes: 1

f6a4
f6a4

Reputation: 1782

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)

Upvotes: 1

Sage Pourpre
Sage Pourpre

Reputation: 10333

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.

Upvotes: 2

Related Questions