How to find amount of time mstsc is used and by whom? - powershell

Our team has geographically dispersed and many virtual machine will be connected by them using remote desktop. I would like to find who is accessing a remote desktop session and how long it is being used.
I tried to do it with powershell. I wrote a script where user will invoke mstsc using powershell. It will log who has logged in and when he logged. But i would like to find when some one log off from mstsc or disconnect mstsc . Is there any way to capture that information in log file using powershell. Whether any event will be triggered while closing mstsc which could be used for it?

I wrote a PowerShell module,PSTerminalServices (http://psterminalservices.codeplex.com), that is built on Cassia.
Here's a sample command output:
PS> Get-TSSession | fl *
IPAddress :
State : Active
ApplicationName :
Local : False
RemoteEndPoint :
InitialProgram :
WorkingDirectory :
ClientProtocolType : Console
ClientProductId : 0
ClientHardwareId : 0
ClientDirectory :
ClientDisplay : Cassia.Impl.ClientDisplay
ClientBuildNumber : 0
Server : Cassia.Impl.TerminalServer
ClientIPAddress :
WindowStationName : Console
DomainName : homelab
UserAccount : homelab\shay
ClientName :
ConnectionState : Active
ConnectTime : 12/15/2011 2:47:02 PM
CurrentTime : 12/23/2011 4:35:21 PM
DisconnectTime :
LastInputTime :
LoginTime : 12/15/2011 3:11:58 PM
IdleTime : 00:00:00
SessionId : 1
UserName : shay

You could use Cassia to get rdp session information (which could be periodically logged to a log file).
Here's a quick example of how to use cassia in Powershell:
[reflection.assembly]::loadfile("d:\cassia.dll")
$manager = new-object Cassia.TerminalServicesManager
$server = $manager.GetRemoteServer("<name of your server>")
$server.open()
$server.getsessions()
It will return something like this (for every session):
ClientDisplay : Cassia.Impl.ClientDisplay
ClientBuildNumber : 0
Server : Cassia.Impl.TerminalServer
ClientIPAddress :
WindowStationName :
DomainName : CONTOSO
UserAccount : CONTOSO\admin
ClientName :
ConnectionState : Disconnected
ConnectTime : 22/12/2011 19:02:00
CurrentTime : 23/12/2011 9:00:42
DisconnectTime : 22/12/2011 22:22:35
LastInputTime : 22/12/2011 22:22:35
LoginTime : 22/12/2011 10:40:21
IdleTime : 10:38:06.4220944
SessionId : 33
UserName : admin

If you can establish an RPC connexion with the server itself you can use QWinsta.exe to see who is logon a TS and RWinsta.exe to remote close a connexion (see Managing Terminal Services Sessions Remotely)

I run this function once per 15 minutes, it relies on Module PSTerminalServices. Basically what it does, is it pulls the last time someone RDPed in, then stores it in an XML, overwritting an older value if it exists, if no one is currently logged on, it returns the latest value from the XML instead.
Function Get-LastLogonTime
{
<#
.SYNOPSIS
Get-LastLogonTime returns the last date that someone logged on to a computer.
.DESCRIPTION
Get-LastLogonTime returns the last date that someone logged to a computer.
If admin rights are missing on the server it will return False.
.EXAMPLE
Get-LastLogonTime "nameofcomputer"
.NOTES
gets last access time from the user folder
.LINK
http://winfred.com
#>
Param(
[Parameter(Position=0, Mandatory=$true)]$ComputerName
)
$StoredRDPSessions = Import-Clixml "RDPSessions.xml"
$myobj = "" | select ComputerName, LastAccessedDate, UserName
$myobj.ComputerName = $ComputerName
$LastConnectedUser = Get-TSSession -ComputerName $ComputerName | where `
{
($_.WindowStationName -ne "Services") -and `
($_.State -ne "Listening") -and `
($_.WindowStationName -ne "Console")
} | sort-object -property LastAccessTime -Descending
if($LastConnectedUser -is [array])
{
$myobj.LastAccessedDate = $LastConnectedUser[0].ConnectTime
$myobj.UserName = $LastConnectedUser[0].UserName
}elseif($LastConnectedUser){
$myobj.LastAccessedDate = $LastConnectedUser.ConnectTime
$myobj.UserName = $LastConnectedUser.UserName
}else{
$myobj.LastAccessedDate = $Null
$myobj.UserName = "Unknown"
}
if(($myobj.LastAccessedDate) -and ($myobj.UserName))
{
$StoredRDPSession = $StoredRDPSessions | where {$_.ComputerName -eq $ComputerName}
if($StoredRDPSession)
{
if($myobj.LastAccessedDate -gt $StoredRDPSession.LastAccessedDate)
{
write-verbose "Newer LastAccessedDate, updating XML"
$StoredRDPSession.LastAccessedDate = $myobj.LastAccessedDate
$StoredRDPSession.UserName = $myobj.UserName
$StoredRDPSessions | Export-Clixml "RDPSessions.xml"
}
}else{
write-verbose "No Entry found Adding to XML"
$NewStoredRDPSessions = #()
$StoredRDPSessions | % {$NewStoredRDPSessions += $_}
$NewStoredRDPSessions += $myobj
$NewStoredRDPSessions | Export-Clixml "RDPSessions.xml"
}
}
if((!($myobj.LastAccessedDate)) -and $StoredRDPSessions)
{
write-verbose "no current session, pulling from stored XML"
$StoredRDPSession = $StoredRDPSessions | where {$_.ComputerName -eq $ComputerName}
if($StoredRDPSession)
{
$myobj.LastAccessedDate = $StoredRDPSession.LastAccessedDate
$myobj.UserName = $StoredRDPSession.UserName
}else{
write-verbose "Sadness, nothing stored in XML either."
}
}
write-verbose "Get-LastLogonTime $ComputerName - $($myobj.LastAccessedDate) - $($myobj.UserName)"
Return $myobj
}

Related

Powershell. Run Command for Every Specific String in Array

I have a PowerShell command:
Get-AzWebAppAccessRestrictionConfig -ResourceGroupName RG1 -Name CoolTestWebApp1 | Select -ExpandProperty MainSiteAccessRestrictions
That once is ran outputs array:
RuleName : IP-1
Description :
Action : Allow
Priority : 1
IpAddress : 10.0.0.0/24
SubnetId :
RuleName : IP-2
Description :
Action : Allow
Priority : 2
IpAddress : 10.0.0.1/24
SubnetId :
How can I run a command for every entry in RuleName?
For example, something like:
Get-AzWebAppAccessRestrictionConfig -ResourceGroupName RG1 -Name CoolTestWebApp1 | Select -ExpandProperty MainSiteAccessRestrictions | ForEach-Object { Write-Host $RuleNameX }
That would execute:
Write-Host $RuleName1
Write-Host $RuleName2
Which in turn would output:
IP-1
IP-2
You pretty much had it:
Get-AzWebAppAccessRestrictionConfig -ResourceGroupName RG1 -Name CoolTestWebApp1 | Select -ExpandProperty MainSiteAccessRestrictions | ForEach-Object { Write-Host $_.RuleName }
Within a ForEach-Object, you can access the current objects attributes using $_.
i.e
ForEach-Object { $_.attributeName }
If the RuleName attribute contains an array of values, you could then iterate over them too:
$siteRestrictions = (Get-AzWebAppAccessRestrictionConfig -ResourceGroupName RG1 -Name CoolTestWebApp1).MainSiteAccessRestrictions
# Loop through objects
foreach($item in $siteRestrictions) {
# Loop through each RuleName
foreach($ruleName in $item.RuleName) {
# Do some logic here
}
}

powershell get info about computer

I'm trying to create a powershell script (getting more advanced... JK. Powershell offers more features than the batch file, and I want to use some of them.)
So, here's my batch script:
:Start
#echo off
set /p password="Password:"
:Nextcomp
set /p computer="Computer name:"
wmic /user:username /password:%password% /node:"%computer%" memorychip get capacity
set /P c=Do you want to get info about another computer (y/n)?
if /I "%c%" EQU "y" goto :Nextcomp
if /I "%c%" EQU "n" goto :End goto :choice
pause
:End
And here's what I found: Script
I modified it for my needs, but whenever I try to run this script, I get it the wrong way - it's displaying me the entire script, and only in the end is it asking me about the computer name:
$resultstxt = "C:\Users\user\Documents\results.csv"
Param(
[Parameter(Mandatory=$true, Position=0, HelpMessage="Password?")]
[SecureString]$password
)
$pw = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
$Computer = Read-Host -Prompt 'Computer name'
$out = #()
If (!(Test-Connection -ComputerName $Computer -Count 1 -Quiet)) {
Write-Host "$Computer not on network."
Continue
}
foreach($object in $HostList) {
$RAM = get-wmiobject -user user -password $pw -computername $object.("Computer")-class win32_physicalmemory
$DeviceInfo= #{}
$DeviceInfo.add("RAM", "$([math]::floor($RAM.Capacity/ (1024 * 1024 * 1024 )) )" + " GB" )
$DeviceInfo.add("Computer Name", $vol.SystemName)
$out += New-Object PSObject -Property $DeviceInfo | Select-Object "RAM"
Write-Verbose ($out | Out-String) -Verbose
$out | Export-CSV -FilePath $resultstxt -NoTypeInformation
}
As you might have guessed, I have a lot more fields, but they all are similar, and I borrowed from a lot of sources, but mainly from the "Script" link.
What I want is:
Hide the password
Export the information to CSV, with each new computer (see 3.) added following the current computer (on the next line)
Ask if I want to get the info about another computer, with "y" key for yes, "n" for no.
Make the script work
I found about the problem 1, but I haven't tested it yet, so... will it work? Next, I found about problem 2, but it would display all info in a not-easy-to-read format, and not everything I need, and all in one cell. Finally, about 3, I found, but it wouldn't work. I can't say I dug the entire Internet, but I'm hoping you guys (and gals?) can help me figure it out. It shouldn't be that hard to resolve these 3 issues, it's not a super complicated script after all, right? My current script is only 31 lines, including the whitespaces.
this is a demo of one way to get basic system info from a group of systems. it uses the CIM cmdlets since they are faster than the WMI cmdlets [most of the time], present datetime info as standard datetime objects, AND not somewhat deprecated.
it also uses the Invoke-Command cmdlet for remote parallelism, and is set to ignore errors so that non-responding systems don't waste your time.
#requires -RunAsAdministrator
# fake reading in a list of computer names
# in real life, use Get-Content or (Get-ADComputer).Name
$ComputerList = #'
Localhost
BetterNotBeThere
127.0.0.1
10.0.0.1
::1
'# -split [environment]::NewLine
$IC_ScriptBlock = {
$CIM_ComputerSystem = Get-CimInstance -ClassName CIM_ComputerSystem
$CIM_BIOSElement = Get-CimInstance -ClassName CIM_BIOSElement
$CIM_OperatingSystem = Get-CimInstance -ClassName CIM_OperatingSystem
$CIM_Processor = Get-CimInstance -ClassName CIM_Processor
$CIM_LogicalDisk = Get-CimInstance -ClassName CIM_LogicalDisk |
Where-Object {$_.Name -eq $CIM_OperatingSystem.SystemDrive}
[PSCustomObject]#{
LocalComputerName = $env:COMPUTERNAME
Manufacturer = $CIM_ComputerSystem.Manufacturer
Model = $CIM_ComputerSystem.Model
SerialNumber = $CIM_BIOSElement.SerialNumber
CPU = $CIM_Processor.Name
SysDrive_Capacity_GB = '{0:N2}' -f ($CIM_LogicalDisk.Size / 1GB)
SysDrive_FreeSpace_GB ='{0:N2}' -f ($CIM_LogicalDisk.FreeSpace / 1GB)
SysDrive_FreeSpace_Pct = '{0:N0}' -f ($CIM_LogicalDisk.FreeSpace / $CIM_LogicalDisk.Size * 100)
RAM_GB = '{0:N2}' -f ($CIM_ComputerSystem.TotalPhysicalMemory / 1GB)
OperatingSystem_Name = $CIM_OperatingSystem.Caption
OperatingSystem_Version = $CIM_OperatingSystem.Version
OperatingSystem_BuildNumber = $CIM_OperatingSystem.BuildNumber
OperatingSystem_ServicePack = $CIM_OperatingSystem.ServicePackMajorVersion
CurrentUser = $CIM_ComputerSystem.UserName
LastBootUpTime = $CIM_OperatingSystem.LastBootUpTime
}
}
$IC_Params = #{
ComputerName = $ComputerList
ScriptBlock = $IC_ScriptBlock
ErrorAction = 'SilentlyContinue'
}
$RespondingSystems = Invoke-Command #IC_Params
$NOT_RespondingSystems = $ComputerList.Where({
# these two variants are needed to deal with an ipv6 localhost address
"[$_]" -notin $RespondingSystems.PSComputerName -and
$_ -notin $RespondingSystems.PSComputerName
})
# if you want to remove the PSShowComputerName, PSComputerName & RunspaceID props, use ...
# Select-Object -Property * -ExcludeProperty PSShowComputerName, PSComputerName, RunspaceId
'=' * 40
$RespondingSystems
'=' * 40
$NOT_RespondingSystems
truncated output ...
LocalComputerName : [MySysName]
Manufacturer : System manufacturer
Model : System Product Name
SerialNumber : System Serial Number
CPU : AMD Phenom(tm) II X4 945 Processor
SysDrive_Capacity_GB : 931.41
SysDrive_FreeSpace_GB : 745.69
SysDrive_FreeSpace_Pct : 80
RAM_GB : 8.00
OperatingSystem_Name : Microsoft Windows 7 Professional
OperatingSystem_Version : 6.1.7601
OperatingSystem_BuildNumber : 7601
OperatingSystem_ServicePack : 1
CurrentUser : [MySysName]\[MyUserName]
LastBootUpTime : 2019-01-24 1:49:31 PM
PSComputerName : [::1]
RunspaceId : c1b949ef-93af-478a-b2cf-e44d874c5724
========================================
BetterNotBeThere
10.0.0.1
to get a well structured CSV file, send the $RespondingSystems collection to the file via Export-CSV.
for a demo of a loop to wrap around any given block of code, take a look at this ...
$Choice = ''
while ([string]::IsNullOrEmpty($Choice))
{
$Choice = Read-Host 'Please enter a valid computer name or [x] to exit '
# replace below with real code to check if $ComputerName is valid
if ($Choice -eq $env:COMPUTERNAME)
{
$ValidCN = $True
}
else
{
$ValidCN = $False
}
if (-not $ValidCN -and $Choice -ne 'x')
{
# insert desired error notice
[console]::Beep(1000, 300)
Write-Warning ''
Write-Warning ('Your choice [ {0} ] is not a valid computer name.' -f $Choice)
Write-Warning ' Please try again ...'
pause
$Choice = ''
}
elseif ($Choice -ne 'x')
{
# insert code to do the "ThingToBeDone"
Write-Host ''
Write-Host ('Doing the _!_ThingToBeDone_!_ to system [ {0} ] ...' -f $Choice)
pause
$Choice = ''
}
}
on screen output ...
Please enter a valid computer name or [x] to exit : e
WARNING:
WARNING: Your choice [ e ] is not a valid computer name.
WARNING: Please try again ...
Press Enter to continue...:
Please enter a valid computer name or [x] to exit : [MySysName]
Doing the _!_ThingToBeDone_!_ to system [ [MySysName] ] ...
Press Enter to continue...:
Please enter a valid computer name or [x] to exit : x

How to get trigger details associated with a task in task scheduler from powershell

So, basically i need to get the trigger details associated with a task which is created in task scheduler.
So, basically I want these information which i am going to be set in this trigger window such as its daily or weekly and repeat task duration as well as for a duration of etc.
Right now am able to get following information.
Name : LastTaskResult
Value : 0
CimType : UInt32
Flags : Property, ReadOnly, NotModified
IsValueModified : False
Name : NextRunTime
Value : 23-09-2015 11:26:56
CimType : DateTime
Flags : Property, ReadOnly, NotModified
IsValueModified : False
Name : NumberOfMissedRuns
Value : 0
CimType : UInt32
Flags : Property, ReadOnly, NotModified
IsValueModified : False
Name : TaskName
Value : test_Task
CimType : String
Flags : Property, Key, NotModified
IsValueModified : False
Name : TaskPath
Value :
CimType : String
Flags : Property, Key, NotModified, NullValue
IsValueModified : False
So, basically my requirement is i have two servers. One is primary and other one is backup. I have scheduled the tasks in primary servers and periodically mirroring(robocopy) these tasks to backup server which works absolutely fine.
But when i change the trigger details or arguments in action tab it does not appear in backup server as i am just checking the task name is already present or not in backup server, if not am creating those tasks.
So is there any way to check the details regarding trigger(Daily or weekly etc, repetition details) or action(script and argument details) so that i can update the tasks accordingly in my seconadary server.
Is something like this what you are after:
$task = Get-ScheduledTask -TaskName "Adobe Flash Player Updater"
$taskTrigger = $task.Triggers[0]
$taskTrigger
It should give you an output similar to:
Enabled : True
EndBoundary :
ExecutionTimeLimit :
Id :
Repetition : MSFT_TaskRepetitionPattern
StartBoundary : 2000-01-01T09:58:00+09:30
DaysInterval : 1
RandomDelay :
PSComputerName :
Edit: Another way of doing this, using a ComObject connection instead
You could do it something like this:
$taskService = New-Object -ComObject "Schedule.Service"
$taskService.Connect($env:COMPUTERNAME)
$rootTaskFolder = $taskService.GetFolder("\")
$task = $rootTaskFolder.GetTask("Adobe Flash Player Updater")
$task
This will return the definition of the task. You could then use Compare-Object to see if it's the same on the backup server, and if not, export/import the task.
If you wanted to parse the XML you could do something like:
$parsedXML = [xml]$task.xml
You can then compare triggers by doings something like:
Compare-Object -DifferenceObject $remoteServerParsedXML.GetElementsByTagName("Triggers") -ReferenceObject $parsedXML.GetElementsByTagName("Triggers")
Does this get closer to what you are trying to achieve?
You'll probably think this is ugly, but it gets you all the pertinent info on the screen. Or you can keep it as an object you can manipulate or format from there...
$ScheduledTasks = get-scheduledtask | ? {$_.TaskPath -like '*Cool*'}
foreach ($item in $ScheduledTasks) {
[string]$Name = ($item.TaskName)
[string]$Action = ($item.Actions | select -ExpandProperty Execute)
[datetime]$Start = ($item.Triggers | select -ExpandProperty StartBoundary)
[string]$Repetition = ($item.Triggers.Repetition | select -ExpandProperty interval)
[string]$Duration = ($item.triggers.Repetition | select -ExpandProperty duration)
$splat = #{
'Name' = $Name
'Action' = $Action
'Start' = $start
'Repetition' = $Repetition
'Duration' = $Duration
}
$obj = New-Object -TypeName PSObject -property $splat
$obj | Write-Output
}
It will get you something like this:
Repetition : PT1H
Duration : P1D
Name : MyCoolTask
Action : C:\MyPath\MyCoolTask\MyCoolTask.exe
Start : 1/10/2014 3:00:00 AM
The repetition is every hour (hence the 1H in the PT1H)
The Duration is the max run time (1 day)
The Name is obviously the name of your task, action and start time should also be self explanatory.
I think what you need is "basically" ;) an export/import function of your tasks.
Here is a sample code :
#connect to scheduler of you master server
$sch = New-Object -ComObject("Schedule.Service")
$sch.connect("$computername")
$root=$sch.GetFolder("\")
$folder =$sch.GetFolder("\subfolder") #if you tasks are defined in a subfolder
#Export all tasks in the subfoder to $path folder in xml format
$folder.getTasks(0) | % {
$path="c:\temp\tasks\$($_.name).xml"
New-Item -ItemType file -Path $path
Set-Content -Path $path -Value $_.xml
}
#connect to scheduler of you backup server
$sch.connect("$backupcomputername")
$folder =$sch.GetFolder("\subfolder")
#import .xml from $task_path
$cred=get-credential # will ask for the credential of the user who run the tasks
Get-childItem -path $task_path -Filter *.xml | %{
$task_name = $_.Name.Replace('.xml', '')
$task_xml = Get-Content $_.FullName
$task = $sch.NewTask($null)
$task.XmlText = $task_xml
$folder.RegisterTaskDefinition($task_name, $task, 6, $cred.UserName, $cred.GetNetworkCredential().password, 1, $null)
}

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

Cannot execute same powershell function twice, gives error

I'm fairly new to powershell and I'm basically writing a script which performs a join on several .csv files based on a primary column. I am using the Join-Collections script from here: http://poshcode.org/1461
As I need to combine 5 .csv files, I need to run this function 4 times.
On the first run, it works fine, but then trying to run the function again gives 'No object specified to the cmd-let' errors.
In trying to debug, I've literally copy-and-pasted the line and only changed the variable name to make a new variable.
I must be doing something fundamentally wrong...
$SumFile = "VMSummary.csv"
$MemFile = "VMMemory.csv"
$ProcFile = "VMProcessor.csv"
$OSFile = "VMOS.csv"
$NicFile = "VMNics.csv"
$SumFileCSV = Import-Csv $SumFile | Select VMElementName,GuestOS,Heartbeat,MemoryUsage,IpAddress
$MemFileCSV = Import-Csv $MemFile | Select VMElementName,Reservation
$ProcFileCSV = Import-Csv $ProcFile
$OSFileCSV = Import-Csv $OSFile
$NicFileCSV = Import-Csv $NicFile
$JoinColumn = "VMElementName"
function Join-Collections {
PARAM(
$FirstCollection
, [string]$FirstJoinColumn
, $SecondCollection
, [string]$SecondJoinColumn=$FirstJoinColumn
)
PROCESS {
$ErrorActionPreference = "Inquire"
foreach($first in $FirstCollection) {
$SecondCollection | Where{ $_."$SecondJoinColumn" -eq $first."$FirstJoinColumn" } | Join-Object $first
}
}
BEGIN {
function Join-Object {
Param(
[Parameter(Position=0)]
$First
,
[Parameter(ValueFromPipeline=$true)]
$Second
)
BEGIN {
[string[]] $p1 = $First | gm -type Properties | select -expand Name
}
Process {
$Output = $First | Select $p1
foreach($p in $Second | gm -type Properties | Where { $p1 -notcontains $_.Name } | select -expand Name) {
Add-Member -in $Output -type NoteProperty -name $p -value $Second."$p"
}
$Output
}
}
}
}
$Temp = Join-Collections $SumFileCSV $JoinColumn $MemFileCSV $JoinColumn
$Temp
##BREAKS HERE
$Temp2 = Join-Collections $SumFileCSV $JoinColumn $MemFileCSV $JoinColumn
UPDATE
It gives the following error:
No object has been specified to the get-member cmdlet
+ foreach($p) in $Second | gm <<<< -type Properties | Where { $p1 -notcontains $_.Name } | select -expand Name)
The csv data is pretty straight forward. When I print out $Temp just before it breaks, it spits out:
GuestOS : Windows Server (R) 2008 Standard
Heartbeat : OK
IpAddress : 192.168.48.92
MemoryUsage : 1024
VMElementName : VM015
Reservation : 1024
GuestOS : Windows Server (R) 2008 Standard
Heartbeat : OK
IpAddress : 192.168.48.151
MemoryUsage : 1028
VMElementName : VM053
Reservation : 1028
GuestOS : Windows Server (R) 2008 Standard
Heartbeat : OK
IpAddress : 192.168.48.214
MemoryUsage : 3084
VMElementName : VM065
Reservation : 3084
GuestOS :
Heartbeat :
IpAddress :
MemoryUsage :
VMElementName : VM074
Reservation : 1024
GuestOS : Windows Server 2008 R2 Standard
Heartbeat : OK
IpAddress : 192.168.48.32
MemoryUsage : 3072
VMElementName : VM088
Reservation : 3072
GuestOS : Windows Server (R) 2008 Enterprise
Heartbeat : OK
IpAddress : 192.168.48.81
MemoryUsage : 3084
VMElementName : VM090
Reservation : 3084
GuestOS : Windows Server 2008 R2 Enterprise
Heartbeat : OK
IpAddress : 192.168.48.82
MemoryUsage : 5120
VMElementName : VM106
Reservation : 5120
The rest of the .csv data is the same sort of stuff - just stats on different servers.
Ideally what I want to do is this :
$Temp = Join-Collections $SumFileCSV $JoinColumn $MemFileCSV $JoinColumn
$Temp = Join-Collections $Temp $JoinColumn $ProcFileCSV $JoinColumn
$Temp = Join-Collections $Temp $JoinColumn $OSFileCSV $JoinColumn
$Temp = Join-Collections $Temp $JoinColumn $NicFileCSV $JoinColumn | Export-Csv "VMJoined.csv" -NoTypeInformation -UseCulture
This code works fine on Powershell v3 CTP 2 (which is probably what #manojlds is using). In Powershell V2 however the parameter $second of the Join-Object function is not bound when invoking Join-Collections the second time. This can be easily verified by adding the following line to the process block inside the Join-Object function:
$psboundparameters | out-host
You will notice that when invoking Join-Collections for the first time both parameters (of Join-Object are bound, however the second time $second is no longer bound.
It is unclear what is causing this behaviour, but since it seems to be working in Powershell V3 I'm guessing it's a bug.
To make the function work in Powershell V2 one could explicitly bind the parameters by replacing this line:
$SecondCollection | Where{ $_."$SecondJoinColumn" -eq $first."$FirstJoinColumn" } | Join-Object $first
by this line:
$SecondCollection | Where{ $_."$SecondJoinColumn" -eq $first."$FirstJoinColumn" } | %{Join-Object -first $first -second $_}