Close-SMBOpenFile throws error and isn't caught in try-catch - powershell

We are using a TeamCity powershell in script execution mode as part of a pipeline with snapshot and artifact dependencies. We have a fairly robust system and have been using this particular process for a couple years, so this isn't brand new code that I'm debugging for the first time. Sadly. It normally works until it randomly doesn't. The TeamCity Agent is different when the error does occur.
This part of our process does some code deploy and some log backups. In order to completely do the backup, we have to ensure the files aren't kept open by QA or Devs at their desk looking at the logs and maybe have them open in read-write mode or the like. Because they would be opening them from their laptop/desktops, they are naturally SMB shares. So we have this function below that is supposed to close the files open on the given server. I say supposed to, because every once in a while it throws this error and I can't seem to either catch it (locally even) or suppress it, so it breaks the TeamCity run. (I've anonymized with ...SNIP anywhere the code is proprietary names or proprietary output)
You can actually test this on your machine by just navigating to \\yourhostname\c$\somefilepath\somefile and see that it will show that the files are open. It shouldn't fail on your machine once you've read through the code and see what it's doing, but if you take out all of the "precautions" you can potentially reproduce the error locally.
function Close-SMBApplicationLocks {
<#
.SYNOPSIS
Closes Active SMB Sessions for Default or User Supplied Paths
.DESCRIPTION
This function is used to prevent interruption to deployments by closing any SMB locks
in application paths. Defaults to closing sessions in folders matching regex
...SNIP
.PARAMETER Paths
[string[]] A string array of paths or path segments to match sessions against.
.EXAMPLE
Close-SMBApplicationLocks
...SNIP
.EXAMPLE
Close-SMBApplicationLocks -Paths #("TEMP")
...SNIP
#>
[CmdletBinding()]
param(
[Alias("SharePaths")]
[Parameter(Mandatory=$false)]
[string[]]$Paths
)
$pathsToUse = Test-IsNull ($Paths -join "|") "...SNIP"
Write-Verbose ("Looking for SMB Sessions Matching Path: {0}" -f $pathsToUse)
$smbSessions = #(Get-SmbOpenFile | Where-Object {$_.Path -match $pathsToUse})
if ((Test-IsCollectionNullOrEmpty $smbSessions)) {
Write-Host ("No Matching SMB Sessions Found")
return
}
Write-Verbose "Found $($smbSessions.Count) Matching SMB Sessions"
$uniqueFileIds = ($smbSessions).FileId | Sort-Object -Unique
foreach ($fileId in $uniqueFileIds) {
$session = #($smbSessions | Where-Object { $_.FileId -eq $fileId })[0]
$sessionId = $session.SessionId
$username = $session.ClientUserName
$path = $session.Path
Write-Verbose "Closing FileId $fileId on SMB Session $sessionId for user $username in path $path"
try {
if ($null -ne (Get-SmbOpenFile -FileId $fileId)) {
## Yes this is FOUR ways to suppress output.
## Microsoft has proven remarkably resilient at showing an error here.
## the ErrorAction Continue still throws an error in TeamCity but not locally
## The try catch doesn't catch
## The Out-Null is because on the off chance the redirect works on the output, it shouldn't show the faux-error
## The output redirection is because this error isn't written to "standard error"
## TeamCity seems to be not honoring this output redirection in the shell it's running under to execute this block
(Close-SmbOpenFile -FileId $fileId -Force -ErrorAction Continue *>&1) | Out-Null
## Run this line instead of the above to actually see the error pretty frequently, by my testing
## Close-SmbOpenFile -FileId $fileId -Force
}
} catch {
$errorMessage = $_.Exception.Message
Write-Warning "An Error Occurred While Trying to Close Session $sessionId : $errorMessage"
}
}
}
We were originally passing the session but I changed to this $fileId version of the code to see if I could clean it up like this with the unique and etc. Those don't seem to have improved things.
We could very well just do Get-SMBOpenFile | Where-Object <pathmatch> | Close-SMBOpenFile (see for example here https://serverfault.com/questions/718875/close-locked-file-in-windows-share-using-powershell-and-openfiles and here https://community.spiceworks.com/topic/2218597-issue-with-close-smbopenfile ) but as you can see we want to log that we are closing it in case we find that something went wrong and this helps us understand what.
Here's the error I have to fight:
[Clearing File Locks] No MSFT_SMBOpenFile objects found with property 'FileId' equal to '825975900669'. Verify the value of the property
[Clearing File Locks] and retry.
[Clearing File Locks] At C:\Program Files\WindowsPowerShell\Modules\...SNIP.psm1:2566 char:34
[Clearing File Locks] + $jobs | ForEach-Object { Receive-Job -Job $_ }
[Clearing File Locks] + ~~~~~~~~~~~~~~~~~~~
[Clearing File Locks] + CategoryInfo : ObjectNotFound: (825975900669:UInt64) [Get-SmbOpenFile], CimJobException
[Clearing File Locks] + FullyQualifiedErrorId : CmdletizationQuery_NotFound_FileId,Get-SmbOpenFile
[Clearing File Locks] + PSComputerName : localhost
[Clearing File Locks]
[Clearing File Locks] Process exited with code 1
But the thing is, just before I do that delete, I check once more to see that the file is open, right? So I say "does this exist? Yes? Close it" and yet, I get this error that makes no sense to me.
I have tried to come up with other ways on the object that's returned to ensure that I need to remove the file or if there's something that says "this should be skipped" but I can't figure anything out there.
Since I seem to be out of options here, is there an alternative method I've not considered? Some sort of CIMInstance command? I've obviously gone snow-blind if there is. This does run locally on the machine, not across a session.
Someone in my org finally noticed that the error does say Get-SmbOpenFile with the FileId parameter is the failure, so that has to be the same redirection error. At this point it looks like I may have an answer.
Snowblindness sucks
Pertinent machine details of note:
PS Z:\git\...SNIP> $PSVersionTable
Name Value
---- -----
PSVersion 5.1.17763.1007
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.17763.1007
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
PS Z:\git\...SNIP> Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, ServicePackMajorVersion, OSArchitecture, CSName, WindowsDirectory
Caption : Microsoft Windows 10 Enterprise LTSC
Version : 10.0.17763
ServicePackMajorVersion : 0
OSArchitecture : 64-bit
CSName : ...SNIP
WindowsDirectory : C:\Windows
But this is also running on Windows Server environments. Same version of PowerShell. Latest Windows patches etc on all servers. We haven't yet moved the fleet over to 2019 Datacenter, I know, but we have some odd 800 servers in production/testing across the fleet that I know of, these things take time of course. If 2016 is the problem, then that's the problem.
PS Z:\git\...SNIP> Get-CimInstance Win32_OperatingSystem -ComputerName ...SNIP | Select-Object Caption, Version, ServicePackMajorVersion, OSArchitecture, CSName, WindowsDirectory
Caption : Microsoft Windows Server 2016 Datacenter
Version : 10.0.14393
ServicePackMajorVersion : 0
OSArchitecture : 64-bit
CSName : ...SNIP
WindowsDirectory : C:\Windows
Maybe my solution is to get TeamCity to honor the output redirection? Is it Server 2016 not honoring the output redirection? Is this just a pipedream of trying to close these connections reliably? Is there a filesystem version I'm not thinking to check?
When I try to create a file at \\mymachine\c$\temp\temp.txt and open it, this is what I get (note that I'm only using notepad to open the file, so there's no lock ongoing)
PS Z:\git\devops_powershell> Get-SMBOpenFile
FileId SessionId Path ShareRelativePath ClientComputerName ClientUserName
------ --------- ---- ----------------- ------------------ --------------
1065151889485 1065151889409 C:\ ...SNIP ...SNIP
1065151889489 1065151889409 C:\ ...SNIP ...SNIP
1065151889613 1065151889409 C:\temp temp ...SNIP ...SNIP
1065151889617 1065151889409 C:\temp temp ...SNIP ...SNIP
1065151889833 1065151889409 C:\temp temp ...SNIP ...SNIP
PS Z:\git\...SNIP> Get-SmbOpenFile -FileId 1065151889833 | Select-Object -Property *
SmbInstance : Default
ClientComputerName : ...SNIP
ClientUserName : ...SNIP
ClusterNodeName :
ContinuouslyAvailable : False
Encrypted : False
FileId : 1065151889833
Locks : 0
Path : C:\temp
Permissions : 1048736
ScopeName : *
SessionId : 1065151889409
ShareRelativePath : temp
Signed : True
PSComputerName :
CimClass : ROOT/Microsoft/Windows/SMB:MSFT_SmbOpenFile
CimInstanceProperties : {ClientComputerName, ClientUserName, ClusterNodeName, ContinuouslyAvailable...}
CimSystemProperties : Microsoft.Management.Infrastructure.CimSystemProperties
PS Z:\git\...SNIP> Get-SmbOpenFile -FileId 1065151889617 | Select-Object -Property *
SmbInstance : Default
ClientComputerName : ...SNIP
ClientUserName : ...SNIP
ClusterNodeName :
ContinuouslyAvailable : False
Encrypted : False
FileId : 1065151889617
Locks : 0
Path : C:\temp
Permissions : 1048705
ScopeName : *
SessionId : 1065151889409
ShareRelativePath : temp
Signed : True
PSComputerName :
CimClass : ROOT/Microsoft/Windows/SMB:MSFT_SmbOpenFile
CimInstanceProperties : {ClientComputerName, ClientUserName, ClusterNodeName, ContinuouslyAvailable...}
CimSystemProperties : Microsoft.Management.Infrastructure.CimSystemProperties
Should I be focused only on the case where Locks -gt 0?

It looks like we may have narrowed down the root cause due to the Get-SmbOpenFile -FileId $fileId failing. This is probably related to the multiple 4-apart concurrent file listings, such that when, in the last example above, 1065151889485 is closed, it "closes" 1065151889489 as well, and then when we try to iterate on the loop for this value, it can't find it, and thus errors out.
PS Z:\git\devops_powershell> Get-SMBOpenFile
FileId SessionId Path ShareRelativePath ClientComputerName ClientUserName
------ --------- ---- ----------------- ------------------ --------------
1065151889485 1065151889409 C:\ ...SNIP ...SNIP
1065151889489 1065151889409 C:\ ...SNIP ...SNIP
1065151889613 1065151889409 C:\temp temp ...SNIP ...SNIP
1065151889617 1065151889409 C:\temp temp ...SNIP ...SNIP
1065151889833 1065151889409 C:\temp temp ...SNIP ...SNIP
I'm going to change that Get-SmbOpenFile -FileId $fileId line in the morning and test with the "error bypass" nonsense and see what happens there too. Or just take that check out and try again.
I'm still very very confused how the try-catch doesn't actively catch the error as thrown. If it did I would just have a Write-Warning instead of the end-process I have now.

Related

Export/Import Printer .DRS file with PowerShell

I have 30+ Windows 2012 r2 servers, each with 30+ Zebra printers installed.
I want to be able to use PowerShell import (and export) the human readable .DRS files that can be generated with the print management UI in both the Preferences and Default locations.
I have seen lots of mentions of this: RUNDLL32.EXE PRINTUI.DLL,PrintUIEntry for import/export settings, but the file is not particularly readable, does not conform with requirements to store .DRS files, and I can't seem to get it to import the settings anyway.
I have admin on these servers, I am open to using the reg. but my understanding is that Zebras do custom things in the Devmode key - so it is not just a simple reg dump and conversion
I can use Get-PrintConfiguration and Get-CimInstance but not seeing Darkness, Speed, etc. in these outputs
$printerName = "Zebra_1"
$compNameSource = $env:COMPUTERNAME
$Printer = Get-CimInstance -ClassName Win32_Printer -Filter "Name='$PrinterName'"
$somePrinterConfig = Get-PrintConfiguration -ComputerName $compNameSource -PrinterName $printerName
$Printer.PSObject.Properties | ForEach-Object {
$_.Name, $_.Value
}
$somePrinterConfig.PSObject.Properties | ForEach-Object {
$_.Name, $_.Value
}
output:
Name : Caption
Value : Zebra_1
CimType : String
Flags : Property, ReadOnly, NotModified
IsValueModified : False
Name : Description
Value :
CimType : String
Flags : Property, ReadOnly, NotModified, NullValue
....
.DRS file:
[FileInfo]
Type=Driver Configuration File
Printer Type=201
[Barcodes]
BarcodeNum=6
[BARFONT0]
Name=EAN 13
UseHeight=1
JoinMode=2
StripTrailingSpaces=1
Kind=0
ID=1
Height=160
Expansion=1
Ratio=16908546
Flags=6164
…
[Parameters]
PaperSize=256
FormName=U
PageWidth=508
PageHeight=254
HDPI=300
VDPI=300
Orientation=3
Quantity=1
Cutter=0
BatchCut=0
Darkness=26
Speed=2
…

how to format Powershell output in specific way?

I need to scan my network for specific processes on servers. I've done this script:
28..31 | ForEach-Object { Get-Process -ComputerName "192.168.0.$_" -Name svc* }
Now how can I format output so it shows on which IP address found process shown? Thank you.
I suggest switching to PowerShell's remoting, because:
it provides a framework for executing any command remotely - rather than relying on individual cmdlets to support a -ComputerName parameter and uses a firewall-friendly transport.
it will continue to be supported in PowerShell [Core] v6+, where the cmdlet-individual -ComputerName parameters aren't supported anymore; this obsolete remoting method uses an older, less firewall-friendly form of remoting that the - obsolete since v3 - WMI cmdlets also use (the latter were replaced by the CIM cmdlets).
It is therefore better to consistently use the firewall-friendly PowerShell remoting with its generic remoting commands (Invoke-Command, Enter-PSSession, ...).
If you use Invoke-Command to target (multiple) remote computers, the objects returned automatically contain and display the name of the originating computer, via the automatically added .PSComputerName property:
# Create the array of IP addresses to target:
$ips = 28..31 -replace '^', '192.168.0.'
# Use a *single* Invoke-Command call to target *all* computers at once.
# Note: The results will typically *not* reflect the input order of
# given IPs / computer names.
Invoke-Command -ComputerName $ips { Get-Process -Name svc* }
You'll see output such as the following - note the PSComputerName column:
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName PSComputerName
------- ------ ----- ----- ------ -- -- ----------- --------------
1013 18 7464 13732 52.72 8 0 svchost 192.168.0.30
...
Note that you can suppress automatic display of the .PSComputerName property with Invoke-Command's -HideComputerName parameter.
However, the property is still available programmatically, which allows you to do the following:
Invoke-Command -ComputerName $ips { Get-Process -Name svc* } -HideComputerName |
Format-Table -GroupBy PSComputerName
This yields display output grouped by computer name / IP:
PSComputerName: 192.168.0.30
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
1013 18 7464 13732 52.72 8 0 svchost
...
PSComputerName: 192.168.0.28
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
1007 17 7112 12632 65.45 11 0 svchost
...
If you wanted to sort by IP address before grouping, you could insert | Sort-Object { [version] $_.PSComputerName }[1] before the Format-Table call.
For sorting by computer names, just
| Sort-Object PSComputerName would do.
[1] Casting to [version] is a trick to ensure proper sorting of IP addresses; IP address strings can be interpreted as [version] (System.Version) instances, and such instances are directly comparable, using the numeric values of the version components (first by .MajorVersion, then by .MinorVersion, ...)
here's one way to do the job. [grin] what it does ...
builds the ip final octet range
sets the IPv4 base octets
builds the list of processes to search for
sets the "no response" text
iterates thru the 4th octet range
builds the IPv4 address
checks to see if it is online/responding
if so, gets the hostname
for my version of PoSh [win7, ps5.1] the Get-Process cmdlet will NOT accept an ip address. a hostname is required.
corrects for the damaged hostname returned when one uses ATT for inet service
creates an ordered hashtable to use for building the property list
builds the various props as needed
converts the hashtable to a PSCustomObject
sends that to the $Results collection
shows it on screen
sends it to a CSV file
here's the code ...
$4thOctetRange = 64..66
$IpBase = '192.168.1'
$ProcessList = #(
'AutoHotKey'
'BetterNotBeThere'
'DisplayFusion'
'Foobar2000'
)
$NoResponse = '__n/a__'
$Results = foreach ($4OR_Item in $4thOctetRange)
{
$Ip = '{0}.{1}' -f $IpBase, $4OR_Item
$Online = Test-Connection -ComputerName $Ip -Count 1 -Quiet
if ($Online)
{
# getting the hostname is REQUIRED by my version of Get-Process
# it will not accept an IP address
# version info = win7, ps5.1
# this may need adjusting for your returned hostname
# mine shows Hostname.attlocal.net
# that is not valid with any query i make, so i removed all after the 1st dot
$HostName = ([System.Net.Dns]::GetHostByAddress($Ip)).HostName.Split('.')[0]
}
else
{
$HostName = $NoResponse
}
$TempProps = [ordered]#{}
$TempProps.Add('IpAddress', $Ip)
$TempProps.Add('Online', $Online)
$TempProps.Add('HostName', $HostName)
foreach ($PL_Item in $ProcessList)
{
if ($TempProps['Online'])
{
# if the process aint found, the "SilentlyContinue" forces a $Null
# the "[bool]" then coerces that to a "False"
$TempProps.Add($PL_Item, [bool](Get-Process -ComputerName $HostName -Name $PL_Item -ErrorAction SilentlyContinue))
}
else
{
$TempProps.Add($PL_Item, $NoResponse)
}
}
# send the object out to the $Results collection
[PSCustomObject]$TempProps
}
# send to screen
$Results
# send to CSV file
$Results |
Export-Csv -LiteralPath "$env:TEMP\Senator14_RemoteProcessFinder.csv" -NoTypeInformation
truncated screen output ...
IpAddress : 192.168.1.65
Online : True
HostName : ZK_01
AutoHotKey : True
BetterNotBeThere : False
DisplayFusion : True
Foobar2000 : True
IpAddress : 192.168.1.66
Online : False
HostName : __n/a__
AutoHotKey : __n/a__
BetterNotBeThere : __n/a__
DisplayFusion : __n/a__
Foobar2000 : __n/a__
csv file content ...
"IpAddress","Online","HostName","AutoHotKey","BetterNotBeThere","DisplayFusion","Foobar2000"
"192.168.1.64","False","__n/a__","__n/a__","__n/a__","__n/a__","__n/a__"
"192.168.1.65","True","ZK_01","True","False","True","True"
"192.168.1.66","False","__n/a__","__n/a__","__n/a__","__n/a__","__n/a__"

Invoke-Command on remote session returns local values

Question
Should the script block of Invoke-Command, when run with a PSSession, always run on the remote computer?
Context
I ran the below powershell against a list of servers:
Clear-Host
$cred = get-credential 'myDomain\myUsername'
$psSessions = New-PSSession -ComputerName #(1..10 | %{'myServer{0:00}' -f $_}) -Credential $cred
Invoke-Command -Session $psSessions -ScriptBlock {
Get-Item -Path 'HKLM:\System\CurrentControlSet\Control\Lsa\Kerberos\Parameters'
} | Sort-Object PSComputerName
# $psSessions | Remove-PSSession
This returned:
Hive: HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos
Name Property PSComputerName
---- -------- --------------
Parameters MaxPacketSize : 1 myServer01
MaxTokenSize : 65535
Parameters MaxPacketSize : 1 myServer02
MaxTokenSize : 65535
Parameters MaxPacketSize : 1 myServer03
MaxTokenSize : 65535
Parameters MaxPacketSize : 1 myServer04
MaxTokenSize : 65535
Parameters MaxPacketSize : 1 myServer05
MaxTokenSize : 65535
Parameters MaxPacketSize : 1 myServer06
MaxTokenSize : 65535
Parameters MaxPacketSize : 1 myServer07
MaxTokenSize : 65535
Parameters MaxPacketSize : 1 myServer08
MaxTokenSize : 65535
Parameters MaxPacketSize : 1 myServer09
MaxTokenSize : 65535
Parameters MaxPacketSize : 1 myServer10
MaxTokenSize : 65535
All looks good; onlyl I'd not expected to see these values / I was running this as a quick sense check before setting the values on these servers to ensure I didn't overwrite anything.
I had a quick look at one of the servers using regedit; and found that MaxTokenSize and MaxPacketSize did not exist.
I then amended the command to use Get-ItemProperty instead of Get-Item:
Invoke-Command -Session $psSessions -ScriptBlock {
Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Lsa\Kerberos\Parameters' -Name 'MaxTokenSize'
} | Sort-Object PSComputerName
This time I got 10 errors:
Property MaxTokenSize does not exist at path HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\Parameters.
+ CategoryInfo : InvalidArgument: (MaxTokenSize:String) [Get-ItemProperty], PSArgumentException
+ FullyQualifiedErrorId : System.Management.Automation.PSArgumentException,Microsoft.PowerShell.Commands.GetItemPropertyCommand
+ PSComputerName : myServer01
# ... (and the same for the other 9 servers, with only PSComputerName changing)
Regarding where the values that were returned came from... they're from my local machine. Amending my local registry entries and rerunning the original command showed all "servers" as having the new value.
I'm guessing this is a bug; but because I've not played with PSSessions much so far wanted to check here in case it's an issue with my understanding / usage of these commands, or if there are known gotchas to watch out for when using PSSessions.
tl;dr:
The root cause is a bug in the formatting instructions for registry keys (as of Windows PowerShell 5.1.18362.125 and PowerShell Core 7.0.0-preview.2) leading to the unexpected mix of remote and local information - see GitHub issue #10341.
As an aside: Similarly, PowerShell's ETS (extended type system) can introduce problems too, as shown in Mathias' answer here.
The best workaround is to simply use Get-ItemProperty (without a -Name argument) instead of Get-Item.
Mathias R. Jessen has provided the crucial pointer in a comment on the question, and js2010's answer provides a limited workaround and a pointer to the root cause, but it's worth providing more background information:
PowerShell comes with formatting instructions for type Microsoft.Win32.RegistryKey, as output by Get-Item with a registry path.
These formatting instructions define a calculated column named Property for the default (tabular) view, which helpfully shows a summary of the output registry key's values, which involves accessing the registry again, using Get-ItemProperty as shown in js2010's answer.
However, that behind-the-scenes Get-ItemProperty call always accesses the local registry - even when the keys were retrieved from a different machine, via PowerShell remoting, so you'll end up with a spurious mix of remote and local information.
Note that, technically, when Get-Item is run remotely, what you receive locally is an approximation of the original Microsoft.Win32.RegistryKey object, due to the serialization and deserialization involved in remoting. This approximation is a custom object with static copies of the original object's property values, and its (simulated) type name is Deserialized.Microsoft.Win32.RegistryKey - note the prefix.
PowerShell applies formatting instructions based on the full type name of output objects, but in the absence of a specific instructions or a given Deserialized.<originalTypeName> type, PowerShell applies the instructions for <originalTypeName>, which is what causes the problems here.
A - cumbersome, but edition-agnostic[1] - way to see the problematic formatting instruction is to run the following command:
(Get-FormatData Microsoft.Win32.RegistryKey -PowerShellVersion $PSVersionTable.PSVersion).FormatViewDefinition.Control | % {
$colNames = $_.Headers.Label
$colValues = $_.Rows.Columns.DisplayEntry.Value
foreach ($i in 0..($colNames.Count-1)) {
[pscustomobject] #{
ColumnName = $colNames[$i]
ColumnValue = $colValues[$i]
}
}
} | Format-Table -Wrap
This yields the column names and definitions for the table view:
ColumnName ColumnValue
---------- -----------
Name PSChildName
Property
$result = (Get-ItemProperty -LiteralPath $_.PSPath |
Select * -Exclude PSPath,PSParentPath,PSChildName,PSDrive,PsProvider |
Format-List | Out-String | Sort).Trim()
$result = $result.Substring(0, [Math]::Min($result.Length, 5000) )
if($result.Length -eq 5000) { $result += "..." }
$result
The workaround suggested in js2010's answer - piping to Format-Table * or Format-List * is effective in the sense that it prevents the inapplicable local information from being displayed: by specifying properties explicitly (even by wildcard pattern *), only those properties are displayed on output - not also the flawed calculated column.
However, while the true Property property of the output objects provides access to the value names in the registry key at hand, it doesn't provide the actual data, the way that the calculated Property column does.
By contrast, using Get-ItemProperty without a -Name argument in lieu of Get-Item as a workaround returns both value names and data (correctly even when remoting) and even does so without restrictions (whereas Get-Item limits output to 5000 chars.)
The output format will be slightly different, but all the information is there.
[1] That is, the command works also in PowerShell Core, where the built-in formatting instructions are no longer maintained as external *.format.ps1xl files and are instead compiled into the executable.
Pipe it to fl * or ft * so it doesn't use the format file to display the registry keys. The format file runs get-itemproperty locally to try to display the properties.
From the bottom of $PSHOME\Registry.format.ps1xml for type Microsoft.Win32.RegistryKey:
<ScriptBlock>
$result = (Get-ItemProperty -LiteralPath $_.PSPath |
Select * -Exclude PSPath,PSParentPath,PSChildName,PSDrive,PsProvider |
Format-List | Out-String | Sort).Trim()
$result = $result.Substring(0, [Math]::Min($result.Length, 5000) )
if($result.Length -eq 5000) { $result += "..." }
$result
</ScriptBlock>

Wbadmin & powershell - latest backup version identifier

I need to get the latest backups version identifier in a powershell script. If I run wbadmin get versions, I get a list of backups and the last one is the one I need.
Is there a way to do a kind of select top 1 version identifier from backups order by date or parsing the wbadmin output and getting this.
edit
It may be the windows.serverbackup module and versionId of Get-WBBackupSet I'm looking for but still need help parsing this.
VersionId : 04/17/2013-21:00
BackupTime : 17/04/2013 22:00:55
BackupTarget : U:
RecoverableItems : Volumes, SystemState, Applications, Files, BareMetalRecovery
Volume : {System Reserved, Local disk (C:), Local disk (I:), Local disk (O:)...}
Application : {"Cluster", "Registry", "Microsoft Hyper-V VSS Writer"}
VssBackupOption : VssFullBackup
SnapshotId : 58999c7d-dfbf-4272-a5b9-21361d171486
Give this a try, Use -Last instead of -First to get the last item:
Get-WBBackupSet |
Sort-Object BackupTime |
Select-Object -First 1 -ExpandProperty VersionId
You can also play with the order of sorting with the -Ascending switch
Edit: revised version
For use with mixed environments (Windows Server 2008, 2008R2, 2012, 2012R2 as of this writing):
function Get-MyWBSummary
{
<#
.SYNOPSIS
Retrieves the history of backup operations on the local or any number of remote computers.
.DESCRIPTION
The Get-MyWBSummary cmdlet retrieves the history of backup operations on the local or any number of remote computers with remoting enabled. This information includes backuptime, backuplocation, bersion identifier and recovery information.
To use this cmdlet, you must be a member of the Administrators group or Backup Operators group on the local or remote computer, or supply credentials that are.
.PARAMETER ComputerName
Retrives backup results on the specified computers. The default is the local computer.
Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more computers. To specify the local computer ignore the ComputerName parameter.
This parameter rely on Windows PowerShell remoting, so your computer has to be configured to run remote commands.
.PARAMETER Credential
Specifies a user account that has permission to perform this action. The default is the current user. Type a user name, such as "User01", "Domain01\User01", or User#Contoso.com. Or, enter a PSCredential object, such as an object that is returned by the Get-Credential cmdlet. When you type a user name, you are prompted for a password.
.PARAMETER Last
Specifies the last (newest/latest) backup versions.
.EXAMPLE
Get-MyWBSummary
Retrieves all Windows Server backupversions from the local computer
.EXAMPLE
Get-MyWBSummary | Where BackupTime -gt (Get-Date).AddDays(-7)
Retrieves all Windows Server backupversions from the local computer within the last week
.EXAMPLE
Get-MyWBSummary -ComputerName $server1, $server2 -Last 1 -Credential $credential -ErrorAction SilentlyContinue -ErrorVariable sessionErrors
Retrieves the last (newest) Windows Server Backup backupversion from remote servers $server1 and $server2
.NOTES
Written by Anders Præstegaard (#aPowershell).
Version 1.0 (20-01-2016)
#>
[CmdletBinding()]
[OutputType([PSCustomObject])]
param
(
[string[]]$ComputerName = $env:COMPUTERNAME,
[System.Management.Automation.PSCredential]$Credential,
[int]$Last
)
begin
{
if ($Credential)
{
$PSDefaultParameterValues['New-PSSession:Credential'] = $Credential
}
$psSession = New-PSSession -ComputerName $ComputerName
}
Process
{
$scriptBlock = {
if (-not (Test-Path -Path 'C:\Windows\System32\wbadmin.exe'))
{
## Windows Server Backup not installed
continue
}
$content = WBAdmin.exe GET VERSIONS
if (-not $content)
{
## no versions found
continue
}
## Get linenumbers for each entity
$newJobLines = #($content | Select-String -Pattern 'Backup time: ')
if ($Using:Last -and $using:Last -lt $newJobLines.Count)
{
$newJobLines = $newJobLines[- $using:Last.. -1]
}
$newJobLines |
ForEach-Object{
## Location
$lineNumberLocation = $_.LineNumber
$backupLocation = $content[$lineNumberLocation] -replace 'Backup location: '
## Version Identifier
$lineNumberVersionIdentifier = $_.LineNumber + 1
$backupVersionIdentifier = $content[$lineNumberVersionIdentifier] -replace 'Version identifier: '
## Backuptime UTC
# Version identifier string in WBAdmin output represents the UTC datetime formated in 'MM/dd/yyyy-HH:mm'
$wbAdminDateStringFormat = 'MM\/dd\/yyyy-HH:mm'
$backupDateTimeFromVersionIdentifier = [DateTime]::ParseExact($backupVersionIdentifier, $wbAdminDateStringFormat, $null)
$backupDateTimeUtcSpecified = [DateTime]::SpecifyKind($backupDateTimeFromVersionIdentifier, [System.DateTimeKind]::Utc)
# NB WBAdmin calculates the time statically compared to your timezone (ie +1 hour)
# If your timezone support "Daylight Saving Time" then WBAdmin calculation is wrong
# ~ half of the year (as far as I can perceive)
$backupDateTimeLocalTime = $backupDateTimeUtcSpecified.ToLocalTime()
## Can recover
$lineNumberCanRecover = $_.LineNumber + 2
$backupVersionCanRecover = $content[$lineNumberCanRecover] -replace 'Can recover: '
[PSCustomObject]#{
BackupTime = $backupDateTimeLocalTime
BackupTimeUtc = $backupDateTimeUtcSpecified
BackupLocation = $backupLocation
VersionIdentifier = $backupVersionIdentifier
CanRecover = $backupVersionCanRecover
}
}
} # Scriptblock
Invoke-Command -Session $psSession -ScriptBlock $scriptBlock |
Select-Object -Property * -ExcludeProperty RunspaceId
}
end
{
if ($psSession)
{
Remove-PSSession -Session $psSession
}
}
}

PowerShell's Write-Debug won't output arrays, but Write-Output does. Is this on purpose?

Shown below, an array works fine as input for Write-Output but not for Write-Debug (I expected them to be more similar than that).
PS C:\> [string[]]$test = #("test1", "test2", "test3")
PS C:\> Write-Output $test
test1
test2
test3
PS C:\> $DebugPreference = "Inquire"
PS C:\> Write-Debug $test
Write-Debug : Cannot convert 'System.String[]' to the type 'System.String' required by parameter 'Message'. Specified method is not supported.
At line:1 char:12
+ Write-Debug <<<< $test
+ CategoryInfo : InvalidArgument: (:) [Write-Debug], ParameterBindingException
+ FullyQualifiedErrorId : CannotConvertArgument,Microsoft.PowerShell.Commands.WriteDebugCommand
PS C:\>
I'm thinking this is just an unfortunate design, but hoping for a sensible explanation. Am I using Write-Debug correctly? If so, anyone have a favorite simple workaround?
I kept having the same problem, and none of the solutions I found above or anywhere else would work in the general case.
For example, the first answer above works only because the array is an array of strings. If it's an array of anything else, that solution breaks, and Write-Debug will output the object type, and not its value as one would expect.
Finally I found a general solution: The key point is to first convert the input object to a string using the Out-String command. Once everything is a string, the above solution works. Using "Out-String -stream" improves the output alignment.
Example:
PS C:\> gwmi win32_bios
SMBIOSBIOSVersion : 786F3 v01.34
Manufacturer : Hewlett-Packard
Name : Default System BIOS
SerialNumber : CZC8196Q8S
Version : HPQOEM - 20120709
PS C:\> gwmi win32_bios | ft -auto
SMBIOSBIOSVersion Manufacturer Name SerialNumber Version
----------------- ------------ ---- ------------ -------
786F3 v01.34 Hewlett-Packard Default System BIOS CZC8196Q8S HPQOEM - ...
PS C:\> $DebugPreference = "Continue"
PS C:\> gwmi win32_bios | ft -auto | Write-Debug
DEBUG: Microsoft.PowerShell.Commands.Internal.Format.FormatStartData
DEBUG: Microsoft.PowerShell.Commands.Internal.Format.GroupStartData
DEBUG: Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData
DEBUG: Microsoft.PowerShell.Commands.Internal.Format.GroupEndData
PS C:\> gwmi win32_bios | ft -auto | Out-String | Write-Debug
DEBUG: SMBIOSBIOSVersion Manufacturer Name SerialNumber Version
----------------- ------------ ---- ------------ -------
786F3 v01.34 Hewlett-Packard Default System BIOS CZC8196Q8S HPQOEM - ...
PS C:\> gwmi win32_bios | ft | Out-String -stream | Write-Debug
DEBUG:
DEBUG: SMBIOSBIOSVersi Manufacturer Name SerialNumber Version
DEBUG: on
DEBUG: --------------- ------------ ---- ------------ -------
DEBUG: 786F3 v01.34 Hewlett-Packard Default Syst... CZC8196Q8S HPQOEM - 201...
DEBUG:
DEBUG:PS C:\>
If you want write-debug to handle each one separately:
[string[]]$test = #("test1", "test2", "test3")
Write-Output $test
test1
test2
test3
$DebugPreference = "Inquire"
$test | Write-Debug
DEBUG: test1
DEBUG: test2
DEBUG: test3
Write-Debug is designed for outputting simple messages when debug preferences are set in a particular way. It takes only a string, not just anything like Write-Host does (and magically formats). You will have to format your output yourself into a single string.
You could combine Write-Host and Write-Debug if you have extra info to output before prompting the user:
if ($DebugPreference -ne 'SilentlyContinue') {
Write-Host 'such-and-such array:' $array
}
Write-Debug 'such-and-such array dumped'
Write-Host is used because it will always write to the console host, rather than to the script's output, as Write-Output does. If you where redirecting standard output of the script to a file, Write-Output would end up in the file, while Write-Host would still be seen in the console.
You could also try doing something like this if your array is of simply enough types that an automatic call to ToString() on them (if they're not strings already) gets you what you want:
$array = 'Alice','Bob','Charlie'
Write-Debug ([String]::Join("`n", $array))
Write-Debug:
Write-Debug [-Message] <string> [<CommonParameters>]
It expects a string. It is unable to convert a string array to a string as the error says. The reason why it expects a string is because it writes debug messages to the console from a script or command.
Note that Write-Output and Write-Host take an object:
Write-Output [-InputObject] <PSObject[]> [<CommonParameters>]
and
Write-Host [[-Object] <Object>] ...