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

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.

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.

Related

How do I call methods within workflow?

How do I call methods within workflow?
I am trying to call "Task" from within a workflow and it seems to be getting ignored? I have provided a watered down nonsense code to illustrate what I'm trying to do. basically call a method within the class in parallel and return the results.
Sample Code
Class Something {
[string]Task($item) {
Start-sleep -Seconds 10
Return "Result"
}
[System.Array]GetSomething($list) {
workflow GetWF {
param($listarr)
ForEach -parallel ($item in $listarr) {
$res = InlineScript {
Write-Host("Starting.." + $using:item)
$this.Task($using:item)
}
}
$res
}
Return GetWF -listarr $list
}
}
$list = #('host1','host2','host3','host4')
$Something = [Something]::New()
$Something.GetSomething($list)
Output :
Starting..host1
Starting..host4
Starting..host2
Starting..host3
Desired Result from Example
The issue is how do I get my results back as an array ? In this example above I would like to see the final result to be this:
$Result = #("Result","Result","Result","Result")

converting javascript "new RegExp" into powershell [regex]::new()

how would i implement the following javascript code snippet in powershell?
String.prototype.regexCount = function (pattern) {
if (pattern.flags.indexOf("g") < 0) {
pattern = new RegExp(pattern.source, pattern.flags + "g");
}
return (this.match(pattern) || []).length;
};
I'm thinking its something like this:
$regexCount = {
param(
$pattern
)
# ??????
if ($pattern.flags.indexOf("g") -lt 0) {
# ????
# $pattern = new RegExp(pattern.source, pattern.flags + "g");
$pattern = [regex]::new($pattern)
}
# ????
return ($this.match($pattern) || []).length;
}
I have almost the entire script converted into powershell except for this little nugget of code... Actually, i'm a little bit clueless when javascript starts creating lambda functions with regular expression objects...
for instance what's the significants of string.prototype.somename? wouldn't you just save the lambda to any variable name?
Using Update-TypeData, create a type-level ScriptMethod ETS member for the .NET string type (System.String):
Update-TypeData -TypeName System.String -MemberName RegexCount -MemberType ScriptMethod -Value {
param([regex] $Regex)
$Regex.Matches($this).Count
}
Now you can call the .RegexCount() method on any string instance, analogous to what your JavaScript code does.
Sample call:
'foo'.RegexCount('.') # -> 3
That is, 3 matches for regex . were found in the input string.

Using Windows Forms in powershell class

I'm trying to make my current Powershell code a little cleaner by using classes. I'm trying to make a Window class using Windows.Forms but I seem to be having trouble with it. This is what I currently have so far:
Window.psm1
using assembly System.Windows.Forms;
class Window {
[System.Windows.Forms.Form]$Form;
Window() {
$this.Form = New-Object System.Windows.Forms.Form;
}
[void]BuildWindow() {
$this.Form.ClientSize = New-Object System.Drawing.Point(600, 400)
$this.Form.Text = "Orion"
$this.Form.TopMost = $false
$this.Form.FormBorderStyle = "Fixed3D"
$this.Form.MaximizeBox = $false
}
[void]ShowWindow() {
$this.Form.ShowDialog();
}
}
I'm then instantiating it in my start.ps1:
using module ".\Window.psm1";
$WindowObject = New-Object Window;
$WindowObject.BuildWindow();
$WindowObject.ShowWindow();
However, I'm getting the following error:
At C:\Orion\scripts\Window.psm1:4 char:6
+ [System.Windows.Forms.Form]$Form;
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
Unable to find type [System.Windows.Forms.Form].
+ CategoryInfo : InvalidOperation: (C:\Orion\scripts\Window.psm1:String) [], ParentContainsErrorRecordExc
eption
+ FullyQualifiedErrorId : TypeNotFound
I'm new to Powershell scripting and classes especially, so I'm not sure what is going wrong here.
To clarify, your code as-is, works perfectly fine in a newer version of PowerShell (tested on the latest, 7.2.3). The issue is exclusively on Windows PowerShell, and to be honest, it's hard to explain why the error occurs however I can provide you what worked for me on that version.
using assembly System.Windows.Forms
class Window {
[System.Windows.Forms.Form] $Form = [System.Windows.Forms.Form]::new()
Window() { } # Removing the instantiation of the form from the ctor
[void] BuildWindow() {
$this.Form.ClientSize = [System.Drawing.Point]::new(600, 400)
$this.Form.Text = "Orion"
$this.Form.TopMost = $false
$this.Form.FormBorderStyle = "Fixed3D"
$this.Form.MaximizeBox = $false
}
[void] ShowWindow() {
$this.Form.ShowDialog();
}
}
On a personal note, I don't think having classes is a good idea when working with Windows Forms, it will make your code harder to read and by the looks of it, each form control will have their own hard-coded values which will be very hard to maintain and very hard to debug.
# would make more sense:
$form = [System.Windows.Forms.Form]#{
ClientSize = [System.Drawing.Point]::new(600, 400)
Text = "Orion"
TopMost = $false
FormBorderStyle = "Fixed3D"
MaximizeBox = $false
}
$form.ShowDialog()

Cannot index into a null array, but there is no array, it's a hash table, and the hash table isn't empty

I have the following code, which is a bit more than minimally functional, but I hope it's still understandable.
$data = 'PATH TO SOME FILE OR FOLDER'
$rule = #{
property = 'size'
operator = 'lt'
value = '1000gb'
note = $null
logOnForgo = $false
}
$ruleOperationArguments = #{
operator = $rule.operator
operand1 = $Null # always the current state to be tested, and depends on specific rule
logString1 = $rule.property
operand2 = $rule.value
logString2 = $rule.value # sometimes modified by rule
note = $rule.note
logOnForgo = $rule.logOnForgo
}
if (($item = Get-Item $data).PSIsContainer) {
$actualSize = 0
foreach ($childItem in (Get-ChildItem $item -recurse | Where {-not $_.PSIsContainer} | ForEach-Object {$_.FullName})) {
$actualSize += (Get-Item $childItem).length
}
} else {
$actualSize = $item.length
}
$ruleOperationArguments.operand1 = $actualSize
if ($actualSize -gt 1TB) {
$actualSizeString = "$([math]::Round(($actualSize/1TB),2))TB"
} elseif ($actualSize -gt 1GB) {
$actualSizeString = "$([math]::Round(($actualSize/1GB),2))GB"
} elseif ($actualSize -gt 1mb) {
$actualSizeString = "$([math]::Round(($actualSize/1MB),2))MB"
} elseif ($actualSize -gt 1kb) {
$actualSizeString = "$([math]::Round(($actualSize/1KB),2))KB"
} else {
$actualSizeString = "$([math]::Round(($actualSize),2))Bytes"
}
$ruleOperationArguments.logString1 = "$($ruleOperationArguments.logString1) ($actualSizeString)"
# test size
switch ($interval[0].Groups['unit'].Value) {
kb {$ruleOperationArguments.operand2 = [System.Int64]($interval[0].Groups['number'].Value) * 1kb}
mb {$ruleOperationArguments.operand2 = [System.Int64]($interval[0].Groups['number'].Value) * 1mb}
gb {$ruleOperationArguments.operand2 = [System.Int64]($interval[0].Groups['number'].Value) * 1gb}
tb {$ruleOperationArguments.operand2 = [System.Int64]($interval[0].Groups['number'].Value) * 1tb}
}
$ruleOperationArguments
I am getting a very odd error
Cannot index into a null array. At line:39 char:41
that line is
$ruleOperationArguments.logString1 = "$($ruleOperationArguments.logString1) ($actualSizeString)"
But here is the odd thing. the code WORKS. The actual final value of $ruleOperationArguments.logString1 IS updated to include the actual size data.
I have also tried not modifying the hash table value by appending to itself, instead using, same result.
$ruleOperationArguments.logString1 = "$($rule.property) ($actualSizeString)"
It's not actually related to the data being put into the hash table, because
$ruleOperationArguments.logString1 = "WTAF?"
also throws the error. What is different about the logStrings that causes problems, while the operands are working without issue? I even tried running the code in a new instance of the ISE, thinking I could have issues with persisting variables. Nope, same issue. I am utterly stumped. I should not that I got a lot fo hits searching on 'Cannot index into a null array' but they all reference actual situations where an array is empty. But $ruleOperationArguments sent to the console will show that the only part of the hash table that is empty is the note key that I am not even using, and operand1 that get's populated before the error condition. Both logStrings have values, and I can successfully update them, but this false error still gets thrown.
EDIT: To minimize the complexity I tried this
$rule = #{
property = 'size'
operator = 'lt'
value = '1000gb'
note = $null
logOnForgo = $false
}
$ruleOperationArguments = #{
operator = $rule.operator
operand1 = $Null
logString1 = $rule.property
operand2 = $rule.value
logString2 = $rule.value
}
$ruleOperationArguments
$ruleOperationArguments.logString1 = "$($ruleOperationArguments.logString1) changed"
$ruleOperationArguments
And thworks with no errors. So something is the rest fo the code is causing the issue, but I still have no clue what.
I think somewhere, you swizzled the line mentioned in the error. When I run the original sample locally, up to the line mentioned in the error and question, the error doesn't occur, and there is nothing that should cause this above. However, running the whole thing results in the same error... but for the switch statement condition. Ignore the line number mismatch here:
Cannot index into a null array.
At line:1 char:9
+ switch ($interval[0].Groups['unit'].Value) {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : NullArray
Looking more closely at this, $interval isn't defined anywhere. You need to initialize this as an Array, List, or some other type of collection.

[Class]::New() causing blank line on the console

I am working on implementing a singleton class to store some regularly accessed status information for my script, including hacking around the issue of $myInvocation only being populated in the main script. All working as planned with this.
class pxStatus {
static [pxStatus] $singleton = $null
[string]$Context = 'machine'
[string]$Path = $null
[datetime]$StartTime = (Get-Date)
pxStatus ([string]$path) {
if ([pxStatus]::singleton -eq $null) {
$this.Path = $path
[pxStatus]::singleton = $this
} else {
Throw "Singleton already initialized"
}
}
static [pxStatus] Get() {
if ([pxStatus]::singleton -eq $null) {
Throw "Singleton not yet initialized"
} else {
return [pxStatus]::singleton
}
}
}
CLS
[void]([pxStatus]::New((Split-Path ($myInvocation.myCommand.path) -parent)))
([pxStatus]::Get()).StartTime
([pxStatus]::Get()).Context
([pxStatus]::Get()).Path
With one exception. Even with that [void] on the [pxStatus]::New() line, I am getting a blank line in the console. Even $null = ([pxStatus]::New((Split-Path ($myInvocation.myCommand.path) -parent))) is echoing a blank line to the console. And for the life of me I can't see what is causing it.
It's not new that causes a blank line but ([pxStatus]::Get()).StartTime.
To fix the issue, you may output it as string, i.e. not formatted, e.g. ([pxStatus]::Get()).StartTime.ToString()
You problem has already been diagnosed, but I wanted to take a second to show how to actually implement a singleton-like type in PowerShell (see inline comments):
class pxStatus {
# hide backing field from user
hidden static [pxStatus] $singleton = $null
[string]$Context = 'machine'
[string]$Path = $null
[datetime]$StartTime = (Get-Date)
# hide instance constructor, no one should call this directly
hidden pxStatus ([string]$path) {
# Only allow to run if singleton instance doesn't exist already
if ($null -eq [pxStatus]::singleton) {
$this.Path = $path
} else {
Throw "Singleton already initialized - use [pxStatus]::Get()"
}
}
# Use a static constructor to initialize singleton
# guaranteed to only run once, before [pxStatus]::Get() or [pxStatus]::singleton
static pxStatus () {
# grab the path from context, don't rely on user input
if(-not $PSScriptRoot){
throw "[pxStatus] can only be used in scripts!"
}
# this will only succeed once anyway
[pxStatus]::singleton = [pxStatus]::new($PSScriptRoot)
}
static [pxStatus] Get() {
# No need to (double-)check ::singleton, static ctor will have run already
return [pxStatus]::singleton
}
}
[pxStatus]::Get().StartTime