Search code examples
powershelloopstreamreaderxmlreader

Class based streamReader & xmlReader locks files, function doesn't


I am refactoring some function based XML reader code to class methods, and seeing some issues. With the function, I can run a test and verify the XML loaded right, then change the XML and test for error conditions. But this class based approach fails due to "the file is open in another program", forcing me to close the console before I can revise the XML. Initially I was using the path directly in the xmlReader. So I moved to a StreamReader input to the xmlReader. And I even played with creating an all new xmlDocument and importing the root node of the loaded XML into that new xmlDocument. None works. I suspect the reason the function based version works is because the xmlReader variable is local scope, so it goes out of scope when the function completes. But I'm grasping at straws there. I also read that Garbage Collection could be an issue, so I added [system.gc]::Collect() right after the Dispose and still no change.

class ImportXML {
    # Properties
    [int]$status = 0
    [xml.xmlDocument]$xml = ([xml.xmlDocument]::New())
    [collections.arrayList]$message = ([collections.arrayList]::New())

    # Methods
    [xml.xmlDocument] ImportFile([string]$path) {
        $importError = $false
        $importFile = ([xml.xmlDocument]::New())
        $xmlReaderSettings = [xml.xmlReaderSettings]::New()
        $xmlReaderSettings.ignoreComments = $true
        $xmlReaderSettings.closeInput = $true
        $xmlReaderSettings.prohibitDtd = $false
        try {
            $streamReader = [io.streamReader]::New($path)
            $xmlreader = [xml.xmlreader]::Create($streamReader, $xmlReaderSettings)
            [void]$importFile.Load($xmlreader)
            $xmlreader.Dispose
            $streamReader.Dispose
        } catch {
            $exceptionName = $_.exception.GetType().name
            $exceptionMessage = $_.exception.message
            switch ($exceptionName) {
                Default {
                    [void]$this.message.Add("E_$($exceptionName): $exceptionMessage")
                    $importError = $true
                }
            }
        }

        if ($importError) {
            $importFile = $null
        }

        return $importFile
    }
}

class SettingsXML : ImportXML {
    # Constructor
    SettingsXML([string]$path){
        if ($this.xml = $this.ImportFile($path)) {
            Write-Host "$path!"
        } else {
            Write-Host "$($this.message)"
        }
    }
}

$settingsPath = '\\Mac\iCloud Drive\Px Tools\Dev 4.0\Settings.xml'
$settings = [SettingsXML]::New($settingsPath)

EDIT: I also tried a FileStream rather than a StreamReader, with FileShare of ReadWrite, like so

$fileMode = [System.IO.FileMode]::Open
$fileAccess = [System.IO.FileAccess]::Read
$fileShare = [System.IO.FileShare]::ReadWrite
$fileStream = New-Object -TypeName System.IO.FileStream $path, $fileMode, $fileAccess, $fileShare

Still no luck.


Solution

  • I think you're on the right lines with Dispose, but you're not actually invoking the method - you're just getting a reference to it and then not doing anything with it...

    Compare:

    PS> $streamReader = [io.streamReader]::New(".\test.xml");
    PS> $streamReader.Dispose
    
    OverloadDefinitions
    -------------------
    void Dispose()
    void IDisposable.Dispose()
    PS> _
    

    with

    PS> $streamReader = [io.streamReader]::New(".\test.xml");
    PS> $streamReader.Dispose()
    PS> _
    

    You need to add some () after the method name so your code becomes:

    $xmlreader.Dispose()
    $streamReader.Dispose()
    

    And then it should release the file lock ok.