Building a Chart Using PowerShell and Chart Controls

Reports are one of those things that everyone has done at some point in time using a variety of methods to include, Excel, XML, CSV, HTML, etc.… A lot of times, while the data is one thing to present, there are times when we need to just present a snapshot of information, usually in the form of a chart of some kind. This is where we usually will dig into Excel and its COM object to take the data and create a quick chart to show the data. But doing this can be slow as working with COM objects are notoriously slow (they are much faster with PowerShell V5), so another approach might be better instead. Enter the .Net Chart Controls which gives us a way to take the data that we have and quickly create a chart such as a pie chart or a line chart and present the data with little to no effort on the system.

What Kind of Charts are Available?

There are a little over 30 possible charts at your disposal ranging from a pie chart to a bar chart or even a bubble chart if you wanted one.

image

Some of these charts like the Pie and Doughnut have a lot in common and you can use the same type of approach with the data to quickly present information while working with a Line chart might require a little more work to ensure the data is presented accurately and also provides support for multiple “Series” of data which allows for you to provide a comparison between different points of time for your data (useful in column or bar charts). For a better look at each chart type and the expectations associate with each chart (such as number of series allowed), the following link has the information to look at: https://msdn.microsoft.com/en-us/library/dd489233.aspx

What is a Series of Data?

A series of data can be looked at as different captures of data that will be applied to a chart. One example is that you can track the current capacity (Series1) of a hard drive as well as its current drive usage (Series2) over the course of several months and see how the current drive usage changes during the course of the time. Given, the capacity may not change at all if it is a physical drive, but may change if the drive is a virtual drive or SAN attached. Something like this would make for a good line chart.

Another example would be to track the memory (or CPU) utilization of several processes. Here you would take a reading at the beginning (Series1) and then wait maybe a minute or so and take another reading (Series2). From these two samples, you can then display the results as a Bar chart or a Column chart to get an idea of the differences in values, if there happen to be differences.

Where do I begin?

Glad you asked. If you are running PowerShell V3+ then you are good to go and have everything already installed, but if you happen to be running PowerShell 2.0, then odds are you might need to download and install the required bits for the Microsoft Chart Controls for Microsoft .NET Framework 3.5 here.

Let’s Build a Pie Chart!

Building a Pie chart is pretty simply as we only require a single series of data which will consist of a label for the data and its value. In this case we are going to chart out our processes by their WorkingSet (WS) property to see what our top 10 memory hogs are.

$Processes = Get-Process | 
Sort-Object WS -Descending | 
Select-Object -First 10

Now we need to do a few other things before we start diving into the world of chart controls. First off I am going to define a couple of helper functions that will assist in some areas.

#region Helper Functions
function ConvertTo-Hashtable{ 
    param([string]$key, $value)

    Begin 
    { 
        $hash = @{} 
    } 
    Process 
    { 
        $thisKey = $_.$Key
        $hash.$thisKey = $_.$Value 
    } 
    End 
    { 
        Write-Output $hash 
    }

}
Function Invoke-SaveDialog {
    $FileTypes = [enum]::GetNames('System.Windows.Forms.DataVisualization.Charting.ChartImageFormat')| ForEach {
        $_.Insert(0,'*.')
    }
    $SaveFileDlg = New-Object System.Windows.Forms.SaveFileDialog
    $SaveFileDlg.DefaultExt='PNG'
    $SaveFileDlg.Filter="Image Files ($($FileTypes))|$($FileTypes)|All Files (*.*)|*.*"
    $return = $SaveFileDlg.ShowDialog()
    If ($Return -eq 'OK') {
        [pscustomobject]@{
            FileName = $SaveFileDlg.FileName
            Extension = $SaveFileDlg.FileName -replace '.*\.(.*)','$1'
        }

    }
}
#endregion Helper Functions

These functions will help to build a hashtable that I can apply later on to my chart and the other will assist in letting me save the chart as an image file for use later on.

Next up is loading the required types to work with the chart controls as well as the windows forms.

 

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms.DataVisualization

If you are still using the old way ([void][Reflection.Assembly]::LoadWithPartialName(“System.Windows.Forms.DataVisualization”)) then you should look at using Add-Type instead.

Next up is to set create our Chart, ChartArea and Series objects as well as making it easier to find all of our available charts by saving the Enum to a variable.

$Chart = New-object System.Windows.Forms.DataVisualization.Charting.Chart
$ChartArea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea 
$Series = New-Object -TypeName System.Windows.Forms.DataVisualization.Charting.Series
$ChartTypes = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]

Now picking a chart is as simple as using $ChartTypes:

$Series.ChartType = $ChartTypes::Pie

With this, we have defined our series as being a Pie chart (default is a line chart). What is interesting here is that we are not defining our chart type where you would have expected it to be at (in the $Chart) but instead define it within the Series object. Now we do end up placing the Series object within the chart and then the Chart within the ChartArea. Note:I don’t actually need a ChartArea with a Pie chart, but am including this for the sake of covering all of the pieces of the chart build.

$Chart.Series.Add($Series)
$Chart.ChartAreas.Add($ChartArea)

You can almost visually see how these all stack within one another.

image

 

Because of how I plan on adding the data to the series to be displayed on the pie chart, I need to convert the current list of processes that I have into  a hashtable for use.

$HashTable = $Processes | ConvertTo-Hashtable -key Name -value WS

image

I want to ensure that the Key (X axis) is the Name of the process and that the Value (Y axis) is the WS property which holds the data value. This way, when we apply it to the series, the pie chart control will understand how to present the data.

Note that I am using the DataBindXY method to load my data. The first item in the method parameter has to be the X value which is my label and the Y axis is the corresponding data.

image

$Chart.Series['Series1'].Points.DataBindXY($HashTable.Keys, $HashTable.Values)

The ‘Series1’ is a default name for the series (you can name it something else if you wish) and any subsequent series added will be Series2,3,4 and so forth if left at the default names.

With the data added for our pie chart, I can now work to make some adjustments to the size of the chart as well as its position and background color.

$Chart.Width = 700 
$Chart.Height = 400 
$Chart.Left = 10 
$Chart.Top = 10
$Chart.BackColor = [System.Drawing.Color]::White
$Chart.BorderColor = 'Black'
$Chart.BorderDashStyle = 'Solid'

All good charts should have a title, right? How else would we know what the chart might be about if a title is not there to tell us what is going on. With that in mind, we will add a title that gives a brief description about what is being displayed.

$ChartTitle = New-Object System.Windows.Forms.DataVisualization.Charting.Title
$ChartTitle.Text = 'Top 5 Processes by Working Set Memory'
$Font = New-Object System.Drawing.Font @('Microsoft Sans Serif','12', [System.Drawing.FontStyle]::Bold)
$ChartTitle.Font =$Font
$Chart.Titles.Add($ChartTitle)  

Typically, if I want to add a legend along with a pie chart, I will avoid having anything on the actual chart itself and leave the description for each piece to be in the legend. This is just a personal preference, but if you want, you can certainly have both. With that in mind, I will show two alternative approaches for the chart display with and without the legend.

Using a Legend

As I am using a legend here, I want to avoid any data from being displayed on the chart itself, so I will make sure to disable the pie chart styles.

$Chart.Series[‘Series1’][‘PieLabelStyle’] = ‘Disabled’

The next step is to set up my legend so it displays useful information.

$Legend = New-Object System.Windows.Forms.DataVisualization.Charting.Legend
$Legend.IsEquallySpacedItems = $True
$Legend.BorderColor = 'Black'
$Chart.Legends.Add($Legend)
$chart.Series["Series1"].LegendText = "#VALX (#VALY)"

And now I have my configurations completed for including a legend with my chart. Note that the VALX will display the values of the X axis while the VALY displays the Y value. So in this case I will have the Process name as VALX and the Working Set (WS) memory as VALY in the parentheses.

Avoiding a Legend

Ok, so  adding a legend wasn’t really in the cards and we just want to show the chart, but at the still time have the items labeled so we know what the pieces of the pie mean. Simple enough, we will just add some more configurations to add the data point labels.

$Chart.Series['Series1']['PieLineColor'] = 'Black'
$Chart.Series['Series1']['PieLabelStyle'] = 'Outside'                         
$Chart.Series['Series1'].Label = "#VALX (#VALY)"

Now we are set! All that is really left to do is display the results of our work. But before we do  that, we need to define a WinForm object that will host the chart object and properly display our work.

#region Windows Form to Display Chart
$AnchorAll = [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Right -bor 
    [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left
$Form = New-Object Windows.Forms.Form  
$Form.Width = 740 
$Form.Height = 490 
$Form.controls.add($Chart) 
$Chart.Anchor = $AnchorAll

# add a save button 
$SaveButton = New-Object Windows.Forms.Button 
$SaveButton.Text = "Save" 
$SaveButton.Top = 420 
$SaveButton.Left = 600 
$SaveButton.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Right
# [enum]::GetNames('System.Windows.Forms.DataVisualization.Charting.ChartImageFormat') 
$SaveButton.add_click({
    $Result = Invoke-SaveDialog
    If ($Result) {
        $Chart.SaveImage($Result.FileName, $Result.Extension)
    }
})

$Form.controls.add($SaveButton)
$Form.Add_Shown({$Form.Activate()}) 
[void]$Form.ShowDialog()
#endregion Windows Form to Display Chart

The result is a chart that we can display to people with the added bonus of being able to save it via a save button.

With Legend

image

Without Legend

image

A 3D Touch

If you want to give this a little better look by making the chart 3D, then you can add the following code to your chart configuration to make it a little more eye popping. And yes, we finally managed to sneak in some use of the $ChartArea in this demo.

$ChartArea.Area3DStyle.Enable3D=$True
$ChartArea.Area3DStyle.Inclination = 50

image

And just like that, instant 3D chart!

Saving a File

But what if I wanted to save a file instead? That’s fine, we can completely skip the process of creating the WinForm and instead make use of the builtin SaveImage method and supplying the file name as well as the extension of the file to save the image as a specific file type.

image

We can find the supported values here:

[enum]::GetValues([System.Windows.Forms.DataVisualization.Charting.ChartImageFormat])

image

Now we can save the chart using the code below:

$Chart.SaveImage('C:\temp\chart.jpeg', 'jpeg')

Where is My Cool Function at?

Yea, so about that function. I decided instead of just building a function to display a pie chart, that I would instead work on and build a module that would allow you to use a variety of charts instead! Stay tuned to https://github.com/proxb/PoshCharts (look at the Dev branch) and you will soon see a working module that not only does pie charts like shown today, but others such as a bar or line chart! Being that this is still in development, I don’t really have any help put together…yet. But as soon as this is more polished I will be updating this blog post (and posting another blog) so you can check it out! And as always, if anyone wants to dive in and help with this, then fork the repo and submit some Pull Requests and I will work to get them merged.

Posted in powershell | Tagged , , , | 1 Comment

What’s New in PoshRSJob

I’ve been hard at work on PoshRSJob to address some some bugs as well as adding some new features that have been reported in the Issues page on the GitHub site for the module. Some of the feature requests on the site didn’t just come from me as other folks had some great ideas and some of those said folks took it a step further by submitting a Pull Request with the needed update to add a particular feature (or fixing a bug). For those of you who submitted a PR,  I definitely thank you as that allows me more time to work on other things, such as ensuring that PoshRSJob works with PowerShell Core so it can run on Linux and MacOS.

In the last 5 months, PoshRSJob has had around 19 version updates ranging from simple bug fixes to more complex things such as accounting for when a user is using Start-RSJob within a ForEach loop. If you have been working with this module, you know that doing this was considered a bad idea because it would create a runspacepool for each iteration of the ForEach loop. Well, that has been resolved in my series of updates. Being that there have been 19 updates, I won’t be covering all of these in great detail, if at all but I will make sure to highlight what I think were noteworthy.

Support for Use on PowerShell Core (Linux and MacOS)

As soon as the announcement was made about PowerShell going open source and being made available on Linux and MacOS,  I made it my goal to ensure that PoshRSJob would work on those systems. Turns out the process wasn’t as bad as what I was seeing it to be initially.

Starting out, I had errors right away during the module import process where I am attempting to load a Type file into a runspace for my RSJob monitor code.

image

The problem here is that there are a couple of types which are now marked as private, meaning that my usual means of accessing them will end in failure and errors. The types which are no longer playing nice are:

  • System.Management.Automation.Runspaces.TypeConfigurationEntry
  • System.Management.Automation.Runspaces.RunspaceConfiguration

This didn’t mean that I was at a stopping point because they could no longer  be found. It just meant that I had to dig into some more advanced methods called reflection to dig into the types and pull out what I needed. Getting the RunspaceConfiguration property was pretty simple because I simply had to get the value of the property as shown below:

$Runspace = [runspacefactory]::CreateRunspace()
#Use the necessary flags to locate the property
$Flags = 'nonpublic','static','instance'
$_RunspaceConfig = $Runspace.GetType().GetProperty('RunspaceConfiguration',$Flags)
#Pull the RunspaceConfiguration property
$RunspaceConfig = $_RunspaceConfig.GetValue($Runspace,$Null)

Specifying the proper flags allows me to locate the private properties and with that, I have the first step of this puzzle completed. The tougher (at least for me) part was now figuring out how I can create the TypeEntry object that is required for me to load the object for my monitor code. After a little bit of testing, I was able to figure out how to create the constructor and use it to create the object that I needed.

#Locate the constructor of the TypeConfigurationEntry object 
$ctor = $RunspaceConfig.Types[0].GetType().GetConstructor([string])

#Invoke the constructor with the type file to build the object
$TypeConfigEntry = $ctor.Invoke($TypeFile)

Finally I add that into the RunspaceConfiguration.Types property.

#Load the type files into the RunspaceConfiguration
$RunspaceConfig.Types.Append($TypeConfigEntry)

What I have is a working approach that covers PowerShell V2+ as well as working in PowerShell Core. Naturally I wanted to see if this could be a public api instead of a private one so I went to the GitHub issues on PowerShell and filed an issue. After talking with PowerShell Team member Jason Shirk, he mentioned that the types that I was targeting were looking to be deprecated and that I should look at the more modern (and very much public) apis to accomplish what I was looking for.

image

After some testing, they worked like a champ and I began work to replace the private apis with the public ones.

$InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()

#Create Type Collection so the object will work properly
$Types = Get-ChildItem "$($PSScriptRoot)\TypeData" -Filter *Types* | Select -ExpandProperty Fullname
ForEach ($Type in $Types) {
    $TypeConfigEntry = New-Object System.Management.Automation.Runspaces.SessionStateTypeEntry -ArgumentList $Type
    $InitialSessionState.Types.Add($TypeConfigEntry)
}
$PoshRS_RunspacePoolCleanup.Runspace =[runspacefactory]::CreateRunspace($InitialSessionState)

After this change was made, I brought down my module into my Ubuntu VM and it now works like I was hoping it would!

PoshRSJob

Remove the need for a DLL File to build classes

This was a workaround originally to provide not only support for Nano server, but also to keep its existing support for down-level support for PowerShell V2. The problem that this brought wasn’t really anything technical, but people who wanted to use this module had reservations to use compiled code that didn’t at least have the source code along with it. Of course, this is perfectly understandable because you don’t know what you are getting (even if the “source code” is provided). I finally went with an approach that would help to mitigate this issue but either using the Class keyword for PowerShell Core and Add-Type for everything else to create the same types needed by the module.

This did not come without its own set of issues as the parser very quickly lets you know if the class keyword is not allowed, even if you have it within an If statement. Kind of annoying but still not a show stopper as my solution for this was to create a here-string that contained the class code and invoked that code in the If statement using Invoke-Expression.

If ($PSVersionTable.PSEdition -eq 'Core') {
#PowerShell V4 and below will throw a parser error even if I never use classes
@'
    class V2UsingVariable {
        [string]$Name
        [string]$NewName
        [object]$Value
        [string]$NewVarName
    }

    class RSRunspacePool{
        [System.Management.Automation.Runspaces.RunspacePool]$RunspacePool
        [System.Management.Automation.Runspaces.RunspacePoolState]$State
        [int]$AvailableJobs
        [int]$MaxJobs
        [DateTime]$LastActivity = [DateTime]::MinValue
        [String]$RunspacePoolID
        [bool]$CanDispose = $False
    }

    class RSJob {
        [string]$Name
        [int]$ID
        [System.Management.Automation.PSInvocationState]$State
        [string]$InstanceID
        [object]$Handle
        [object]$Runspace
        [System.Management.Automation.PowerShell]$InnerJob
        [System.Threading.ManualResetEvent]$Finished
        [string]$Command
        [System.Management.Automation.PSDataCollection[System.Management.Automation.ErrorRecord]]$Error
        [System.Management.Automation.PSDataCollection[System.Management.Automation.VerboseRecord]]$Verbose
        [System.Management.Automation.PSDataCollection[System.Management.Automation.DebugRecord]]$Debug
        [System.Management.Automation.PSDataCollection[System.Management.Automation.WarningRecord]]$Warning
        [System.Management.Automation.PSDataCollection[System.Management.Automation.ProgressRecord]]$Progress
        [bool]$HasMoreData
        [bool]$HasErrors
        [object]$Output
        [string]$RunspacePoolID
        [bool]$Completed = $False
        [string]$Batch
    }
'@ | Invoke-Expression
}
Else {
    Add-Type @"
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Management.Automation;

    public class V2UsingVariable
    {
        public string Name;
        public string NewName;
        public object Value;
        public string NewVarName;
    }

    public class RSRunspacePool
    {
        public System.Management.Automation.Runspaces.RunspacePool RunspacePool;
        public System.Management.Automation.Runspaces.RunspacePoolState State;
        public int AvailableJobs;
        public int MaxJobs;
        public DateTime LastActivity = DateTime.MinValue;
        public string RunspacePoolID;
        public bool CanDispose = false;
    }

    public class RSJob
    {
        public string Name;
        public int ID;
        public System.Management.Automation.PSInvocationState State;
        public string InstanceID;
        public object Handle;
        public object Runspace;
        public System.Management.Automation.PowerShell InnerJob;
        public System.Threading.ManualResetEvent Finished;
        public string Command;
        public System.Management.Automation.PSDataCollection<System.Management.Automation.ErrorRecord> Error;
        public System.Management.Automation.PSDataCollection<System.Management.Automation.VerboseRecord> Verbose;
        public System.Management.Automation.PSDataCollection<System.Management.Automation.DebugRecord> Debug;
        public System.Management.Automation.PSDataCollection<System.Management.Automation.WarningRecord> Warning;
        public System.Management.Automation.PSDataCollection<System.Management.Automation.ProgressRecord> Progress;
        public bool HasMoreData;
        public bool HasErrors;
        public object Output;
        public string RunspacePoolID;
        public bool Completed = false;
        public string Batch;
    }
"@
}

FunctionsToImport now Imports Aliases with Function

This was something that I found out while using the module along with another custom function. I cheated in the scriptblock of the RSJob and just tried to use the custom alias that I had given my function, but quickly found out via a nice error message that the alias didn’t exist in the context of the RSJob. Fortunately it didn’t take too long or provide a challenge to quickly add the alias for each imported function into the RSJob.

#Check for an alias and add it as well
If ($Alias = Get-Alias -Definition $Function) {
    $AliasEntry = New-Object System.Management.Automation.Runspaces.SessionStateAliasEntry -ArgumentList $Alias.Name,$Alias.Definition
    $InitialSessionState.Commands.Add($AliasEntry)
}

 

Using the same runspacepool with ForEach Loops

This was a fairly popular request as folks familiar with PSJobs would use a ForEach loop to create a new job for each thing that they wanted to run. This didn’t work out too well with RSJobs as it would create a new runspacepool for each iteration, meaning there was no throttling happening and could potentially bog the system down as everything would be running at the same time. The way that Start-RSJob would work is that you pipe directly into the function and it would handle the work behind the scenes. Well, the requests that I had wanted a way to provide support for both approaches while still delivering the same result of throttled jobs. After some work I was able to handle this by taking the Batch parameter that I had implemented a while back to group sets of jobs and merging that with the runspacepoolID in my RSRunspacePool type (now a string property instead of a GUID) and creating a default value for both that they shared (in the form of a GUID) and also allowed a user defined entry as well. Now during the RSJob creation process, the command will look for existing runspacepools and use that if the user hasn’t defined one.

#region RunspacePool Creation        
If (-NOT $PSBoundParameters.ContainsKey('Batch')) {
    If ($PoshRS_RunspacePools.Count -gt 0) {
        [System.Threading.Monitor]::Enter($PoshRS_RunspacePools.syncroot) 
        $RSPObject = $PoshRS_RunspacePools[0]
        $Batch = $RSPObject.RunspacePoolID
        Write-Verbose "Using current runspacepool <$($__RSPObject.RunspacePoolID)>"
        #Update LastActivity so it doesn't get removed prematurely; runspacepool cleanup will update once it detects running jobs
        $RSPObject.LastActivity = $RSPObject.LastActivity.AddMinutes(5)
        [System.Threading.Monitor]::Exit($PoshRS_RunspacePools.syncroot)
    }
    Else {
        $Batch = $RunspacePoolID = [guid]::NewGuid().ToString()
        Write-Verbose "Creating new runspacepool <$Batch>"
        $RunspacePool = [runspacefactory]::CreateRunspacePool($InitialSessionState)
        Try {
            #ApartmentState doesn't exist in Nano Server
            $RunspacePool.ApartmentState = 'STA'
        } 
        Catch {}
        [void]$RunspacePool.SetMaxRunspaces($Throttle)
        If ($PSVersionTable.PSVersion.Major -gt 2) {
            $RunspacePool.CleanupInterval = [timespan]::FromMinutes(5)    
        }
        $RunspacePool.Open()
        $RSPObject = New-Object RSRunspacePool -Property @{
            RunspacePool = $RunspacePool
            MaxJobs = $RunspacePool.GetMaxRunspaces()
            RunspacePoolID = $RunspacePoolID
        }
        [System.Threading.Monitor]::Enter($PoshRS_RunspacePools.syncroot) 
        [void]$PoshRS_RunspacePools.Add($RSPObject)
        [System.Threading.Monitor]::Exit($PoshRS_RunspacePools.syncroot)
    }
}
Else {            
    [System.Threading.Monitor]::Enter($PoshRS_RunspacePools.syncroot) 
    $__RSPObject = $PoshRS_RunspacePools | Where {
        $_.RunspacePoolID -eq $Batch
    }
    If ($__RSPObject) {
        Write-Verbose "Using current runspacepool <$($__RSPObject.RunspacePoolID)>"
        $RSPObject = $__RSPObject
        $RSPObject.LastActivity = $RSPObject.LastActivity.AddMinutes(5)
    }
    Else {
        Write-Verbose "Creating new runspacepool <$Batch>"
        $RunspacePoolID = $Batch
        $RunspacePool = [runspacefactory]::CreateRunspacePool($InitialSessionState)
        Try {
            #ApartmentState doesn't exist in Nano Server
            $RunspacePool.ApartmentState = 'STA'
        } 
        Catch {}
        [void]$RunspacePool.SetMaxRunspaces($Throttle)
        If ($PSVersionTable.PSVersion.Major -gt 2) {
            $RunspacePool.CleanupInterval = [timespan]::FromMinutes(2)    
        }
        $RunspacePool.Open()
        $RSPObject = New-Object RSRunspacePool -Property @{
            RunspacePool = $RunspacePool
            MaxJobs = $RunspacePool.GetMaxRunspaces()
            RunspacePoolID = $RunspacePoolID
        }
        [void]$PoshRS_RunspacePools.Add($RSPObject)
    }
    [System.Threading.Monitor]::Exit($PoshRS_RunspacePools.syncroot)            
}
#endregion RunspacePool Creation

That’s all for today. This was just a handful of the things that I have worked on with this module to try and make it as useful as possible. There are still many more things to do with it and I am always open to new suggestions so be sure to hit up the Issues page on the GitHub site and let me know what you want to see, or fork the repo and submit a Pull Request with the change and I will look  to get it into the main build.

You can find the rest of the issues that I had fixed (or had help via a PR to fix) below.

Release Notes For PoshRSJob


|1.7.2.4|

* Fixed Issue #92 (Cannot load module in PS4 due to “class” keyword)

|1.7.2.3|

  • Fixed Issue #87 (Stop-RSJob gives an error if it has no input)

|1.7.2.2|

  • Fixed Issue #59 (Receive-RSJob doesn’t clear a job’s HasMoreData state)

|1.7.2.1|

* Fixed Issue #83 (FunctionsToImport should include the function’s Alias where applicable)

|1.7.1.0|

  • Replaced private apis with public apis (#85 Update RunspaceConfiguration apis to use InitialSessionState instead)

|1.7.0.0|

  • Remove need for DLL file for building out the classes. Using pure PowerShell (mostly) via means of here-strings and Add-Type for PowerShell V2-4 and the new Classes keywords for PowerShell V5 which includes PowerShell Core/Nano.
  • Remove the prefixes for custom objects so they no longer start with PoshRS.PowerShell.. Now they are V2UsingVariable, RSJob and RSRunspacePool.

|1.6.2.1|

  • Add support for PowerShell Core on Linux/MacOS (this still needs more work but should load types within a runspace now!)

|1.6.1.0|

  • Fixed Issue #75 (Feature Request: Add RunspaceID handling to Start-RSJob for better throttling support)
  • Fixed Issue #82 (Exception setting “RunspacePool” in 1.6.0.0 build)

|1.5.7.7|

  • Fixed Issue #69 (Module produces error if imported more than once (PS v2 only))
  • Fixed Issue #64 (HadErrors in PoshRS.PowerShell.RSJob throws errors in PowerShell V2)
  • Fixed Issue #67 (Converted Add-Type code for C# classes to be created via Reflection for Nano server support) <- Created custom dll
  • Fixed Issue #61 (Receive-RSJob not allowing -Job parameter input)
  • Fixed Issue #63 (Replaced Global variables with Script scope)
  • Fixed Issue #66 (Parameters don’t work with PowerShell V2)
  • Fixed Issue #65 (Bug with v2 variable substitution – single-quoted strings get $var references replaced)
  • Fixed Issue #68 (ApartmentState Does Not Exist in Nano)
  • Fixed Issue #76 (Jobs don’t have output when using ADSI WinNT provider (Receive-RSJob))
Posted in powershell | Tagged , , , , | 2 Comments

Revisiting NetSession Function using PSReflect

In an article that I wrote a little while ago, I talked about how you can view the net sessions on systems by making use of some pinvoke magic to call some Win32 apis. If you have ever worked with this type of approach, you know that it can be sometimes painful in writing out the necessary code needed to build out the required methods, enums, structs and anything else that is required just to pull a particular piece of information.

So you might be asking why don’t we just use Add-Type and use the very handy pinvoke signatures that are available? Well, this is the easiest approach as it is all taken care of for you, but if you are more on the security side, you want to ensure that nothing is written to disk (which is what Add-Type does) whereas using reflection can give you the option to write to memory instead, making it leave less artifacts laying around.

Fortunately for all of us in the community, fellow MVP Matt Graeber (Blog | Twitter) has made an awesome module called PSReflect which seeks to make this process much simpler by handling all of the internal calls and building of the methods from the Win32 apis while leaving us with the necessary code to use the methods.

What I am going to do in this blog post is to highlight just how easy it is to build working code that uses pinvoke signatures by taking what I did in my post on viewing net sessions and applying the same process this time using the PSReflect module. The Github repository that I referenced earlier is one way to get a hold of the module, but if you are running PowerShell V5, you can install is simply by calling Install-Module.

Install-Module –Name PSReflect

image

And now we have our module that we can use in building out the necessary code. I won’t be using all of the commands here, but in case you are curious as to  what is available, here they are:

image

I want to show the code that is handling the reflection to dynamically build out the methods that will be used to grab the net sessions and won’t be showing everything else that happens afterwards to pull the net sessions. So with that, here is the code that I used in my Get-NetSession function to build up the types and methods:

#region Reflection
Try {
    [void][Net.Session]
}
Catch {
    Write-Verbose &quot;Building pinvoke via reflection&quot;
    #region Module Builder
    $Domain = [AppDomain]::CurrentDomain
    $DynAssembly = New-Object System.Reflection.AssemblyName('NetSession')
    $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run) # Only run in memory
    $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('NetSessionModule', $False)
    #endregion Module Builder

    #region Custom Attribute Builder
    $ctor = [System.Runtime.InteropServices.MarshalAsAttribute].GetConstructor(@([System.Runtime.InteropServices.UnmanagedType]))
    $CustomAttribute = [System.Runtime.InteropServices.UnmanagedType]::LPWStr
    $CustomAttributeBuilder = New-Object System.Reflection.Emit.CustomAttributeBuilder -ArgumentList $ctor, $CustomAttribute
    #endregion Custom Attribute Builder

    #region Struct
    #region SESSION_INFO_10
    $Attributes = 'AutoLayout, AnsiClass, Class, Public, SequentialLayout, Sealed, BeforeFieldInit'
    $STRUCT_TypeBuilder = $ModuleBuilder.DefineType('SESSION_INFO_10', $Attributes, [System.ValueType], 8, 0x0)
    $Field = $STRUCT_TypeBuilder.DefineField('OriginatingHost', [string], 'Public')
    $Field.SetCustomAttribute($CustomAttributeBuilder)
    $Field = $STRUCT_TypeBuilder.DefineField('DomainUser', [string], 'Public')
    $Field.SetCustomAttribute($CustomAttributeBuilder)
    [void]$STRUCT_TypeBuilder.DefineField('SessionTime', [uint32], 'Public')
    [void]$STRUCT_TypeBuilder.DefineField('IdleTime', [uint32], 'Public')
    [void]$STRUCT_TypeBuilder.CreateType()
    #endregion SESSION_INFO_10
    #endregion Struct

    $TypeBuilder = $ModuleBuilder.DefineType('Net.Session', 'Public, Class')

    #region Methods
    #region NetSessionEnum Method
    $PInvokeMethod = $TypeBuilder.DefineMethod(
        'NetSessionEnum', #Method Name
        [Reflection.MethodAttributes] 'PrivateScope, Public, Static, HideBySig, PinvokeImpl', #Method Attributes
        [int32], #Method Return Type
        [Type[]] @(
            [string],
            [string],
            [string],
            [int32],
            [intptr].MakeByRefType(),
            [int],
            [int32].MakeByRefType(),
            [int32].MakeByRefType(),
            [int32].MakeByRefType()
        ) #Method Parameters
    )

    #Define first three parameters with custom attributes
    1..3 | ForEach {
        $Parameter = $PInvokeMethod.DefineParameter(
            $_,
            [System.Reflection.ParameterAttributes]::In,
            $Null
        )
        $Parameter.SetCustomAttribute(
            $CustomAttributeBuilder
        )
    }

    $DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
    $FieldArray = [Reflection.FieldInfo[]] @(
        [Runtime.InteropServices.DllImportAttribute].GetField('EntryPoint'),
        [Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')
        [Runtime.InteropServices.DllImportAttribute].GetField('ExactSpelling')
        [Runtime.InteropServices.DllImportAttribute].GetField('PreserveSig')
    )

    $FieldValueArray = [Object[]] @(
        'NetSessionEnum', #CASE SENSITIVE!!
        $True,
        $True,
        $True
    )

    $CustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder(
        $DllImportConstructor,
        @('Netapi32.dll'),
        $FieldArray,
        $FieldValueArray
    )

    $PInvokeMethod.SetCustomAttribute($CustomAttribute)
    #endregion NetSessionEnum Method
    #region NetApiBufferFree Method
    $PInvokeMethod = $TypeBuilder.DefineMethod(
        'NetApiBufferFree', #Method Name
        [Reflection.MethodAttributes] 'PrivateScope, Public, Static, HideBySig, PinvokeImpl', #Method Attributes
        [int], #Method Return Type
        [Type[]] @(
            [intptr]
        ) #Method Parameters
    )

    $DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
    $FieldArray = [Reflection.FieldInfo[]] @(
        [Runtime.InteropServices.DllImportAttribute].GetField('EntryPoint'),
        [Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')
        [Runtime.InteropServices.DllImportAttribute].GetField('ExactSpelling')
        [Runtime.InteropServices.DllImportAttribute].GetField('PreserveSig')
    )

    $FieldValueArray = [Object[]] @(
        'NetApiBufferFree', #CASE SENSITIVE!!
        $True,
        $True,
        $True
    )

    $CustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder(
        $DllImportConstructor,
        @('Netapi32.dll'),
        $FieldArray,
        $FieldValueArray
    )

    $PInvokeMethod.SetCustomAttribute($CustomAttribute)
    #endregion NetApiBufferFree Method
    #endregion Methods

    [void]$TypeBuilder.CreateType()
}
#endregion Reflection

Yes, that is 129 lines of code that handles the building of 1 Struct and 2 Methods. Depending on the amount of data in a Struct, it could be a greater number of lines to account for more properties. Methods are usually going to be the same number of lines (~50) depending on your style of building it out. In my case this is what I would expect even for a simple method that might only take a single parameter and doesn’t return anything when called.

I want to make sure that the PSReflect module is available before running the function so I am going to add a REQUIRES statement and then import the module.

#REQUIRES -Module PSReflect
Import-Module -Name PSReflect

The first part is to create the module builder that we will then be referencing throughout the rest of the creations of the various components related to our structs and methods.

No PSReflect

#region Module Builder
$Domain = [AppDomain]::CurrentDomain
$DynAssembly = New-Object System.Reflection.AssemblyName('NetSession')
$AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run) # Only run in memory
$ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('NetSessionModule', $False)
#endregion Module Builder

#region Custom Attribute Builder
$ctor = [System.Runtime.InteropServices.MarshalAsAttribute].GetConstructor(@([System.Runtime.InteropServices.UnmanagedType]))
$CustomAttribute = [System.Runtime.InteropServices.UnmanagedType]::LPWStr
$CustomAttributeBuilder = New-Object System.Reflection.Emit.CustomAttributeBuilder -ArgumentList $ctor, $CustomAttribute
#endregion Custom Attribute Builder

 

With PSReflect

$Module = New-InMemoryModule -ModuleName NetSessions

I am including the Custom Attribute Builder as it is required for my Struct. Next up is to build out the Struct that the method will require for handling the net sessions object.

No PSReflect

$Attributes = 'AutoLayout, AnsiClass, Class, Public, SequentialLayout, Sealed, BeforeFieldInit'
$STRUCT_TypeBuilder = $ModuleBuilder.DefineType('SESSION_INFO_10', $Attributes, [System.ValueType], 8, 0x0)
$Field = $STRUCT_TypeBuilder.DefineField('OriginatingHost', [string], 'Public')
$Field.SetCustomAttribute($CustomAttributeBuilder)
$Field = $STRUCT_TypeBuilder.DefineField('DomainUser', [string], 'Public')
$Field.SetCustomAttribute($CustomAttributeBuilder)
[void]$STRUCT_TypeBuilder.DefineField('SessionTime', [uint32], 'Public')
[void]$STRUCT_TypeBuilder.DefineField('IdleTime', [uint32], 'Public')
[void]$STRUCT_TypeBuilder.CreateType()

With PSReflect

$SESSION_INFO_10 = struct -Module $Module -FullName SESSION_INFO_10 -StructFields @{
    OriginatingHost = field -Position 0 -Type ([string]) -MarshalAs @('LPWStr')
    DomainUser = field -Position 1 -Type ([string]) -MarshalAs @('LPWStr')
    SessionTime = field -Position 2 -Type ([int32])
    IdleTime = field -Position 3 -Type ([int32])
}

You can already start to see the fewer lines of code that is being used and I can assure you that this trend will only continue. In fact, I don’t need to worry about a custom attribute builder with PSReflect as it is handled via the –MarshalAs parameter.

Up next are the method creations that we will be creating to round out all of the components.

No PSReflect

$TypeBuilder = $ModuleBuilder.DefineType('Net.Session', 'Public, Class')

#region Methods
#region NetSessionEnum Method
$PInvokeMethod = $TypeBuilder.DefineMethod(
    'NetSessionEnum', #Method Name
    [Reflection.MethodAttributes] 'PrivateScope, Public, Static, HideBySig, PinvokeImpl', #Method Attributes
    [int32], #Method Return Type
    [Type[]] @(
        [string],
        [string],
        [string],
        [int32],
        [intptr].MakeByRefType(),
        [int],
        [int32].MakeByRefType(),
        [int32].MakeByRefType(),
        [int32].MakeByRefType()
    ) #Method Parameters
)

#Define first three parameters with custom attributes
1..3 | ForEach {
    $Parameter = $PInvokeMethod.DefineParameter(
        $_,
        [System.Reflection.ParameterAttributes]::In,
        $Null
    )
    $Parameter.SetCustomAttribute(
        $CustomAttributeBuilder
    )
}

$DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
$FieldArray = [Reflection.FieldInfo[]] @(
    [Runtime.InteropServices.DllImportAttribute].GetField('EntryPoint'),
    [Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')
    [Runtime.InteropServices.DllImportAttribute].GetField('ExactSpelling')
    [Runtime.InteropServices.DllImportAttribute].GetField('PreserveSig')
)

$FieldValueArray = [Object[]] @(
    'NetSessionEnum', #CASE SENSITIVE!!
    $True,
    $True,
    $True
)

$CustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder(
    $DllImportConstructor,
    @('Netapi32.dll'),
    $FieldArray,
    $FieldValueArray
)

$PInvokeMethod.SetCustomAttribute($CustomAttribute)
#endregion NetSessionEnum Method
#region NetApiBufferFree Method
$PInvokeMethod = $TypeBuilder.DefineMethod(
    'NetApiBufferFree', #Method Name
    [Reflection.MethodAttributes] 'PrivateScope, Public, Static, HideBySig, PinvokeImpl', #Method Attributes
    [int], #Method Return Type
    [Type[]] @(
        [intptr]
    ) #Method Parameters
)

$DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
$FieldArray = [Reflection.FieldInfo[]] @(
    [Runtime.InteropServices.DllImportAttribute].GetField('EntryPoint'),
    [Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')
    [Runtime.InteropServices.DllImportAttribute].GetField('ExactSpelling')
    [Runtime.InteropServices.DllImportAttribute].GetField('PreserveSig')
)

$FieldValueArray = [Object[]] @(
    'NetApiBufferFree', #CASE SENSITIVE!!
    $True,
    $True,
    $True
)

$CustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder(
    $DllImportConstructor,
    @('Netapi32.dll'),
    $FieldArray,
    $FieldValueArray
)

$PInvokeMethod.SetCustomAttribute($CustomAttribute)
#endregion NetApiBufferFree Method
#endregion Methods

[void]$TypeBuilder.CreateType()

With PSReflect

$Functions = @(
    func -DllName Netapi32 -FunctionName NetSessionEnum -ReturnType ([int32]) -ParameterTypes @([string],[string],[string],[int32],
        [intptr].MakeByRefType(),[int],[int32].MakeByRefType(),[int32].MakeByRefType(),[int32].MakeByRefType())
    func -DllName Netapi32 -FunctionName NetApiBufferFree -ReturnType ([int]) -ParameterTypes @([intptr])
)

$Functions | Add-Win32Type -Module $Module -Namespace 'Net.Session'

This is, in my opinion, where you see the biggest gains with PSReflect in the amount of lines of code that you save using this module to build out the methods. The total lines of code that it takes to build out my code using PSReflect is 23 lines of code. Compare that to the 129 lines not using PSReflect and you can see that I saved over 100 lines! This was a pretty small amount of pinvoke happening here as some of my larger functions (Get-ChildItem2 and Get-Service2) require a lot more code to build out everything that I need.

If you also didn’t notice, the quality of the code is also much cleaner to read and understand as you are working with PowerShell functions, not just a lot of .Net code to build everything out. Defining a method via PowerShell code is so much easier to write and read than dynamically building everything out.

After this, the rest of the code is practically the same as I still need to perform the same actions to build the object, query the remote system and marshal the data back to the object.

I hope that if you work with pinvoke,  that you give this a try and see how much easier it will make your life writing code without having to worry about writing a bunch of code to create the necessary components to make everything work together.

References

http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/

Posted in powershell | Tagged , , , , , , | 2 Comments

PowerShell Open Sourced and Available on Linux and MacOS

No joke here. It was announced today at 11:14am (fun fact: this is when PowerShell V1 was released which was on November 14th,  2006) that PowerShell is not only being open sourced, but also is now available to install on Linux and MacOS. This is huge as being able to run PowerShell on anything other than Windows seemed like a pipe dream. But here it is! I won’t go crazy here listing out everything that has already been said before and those links are located at the bottom of this article. I just wanted to highlight some awesome things about this announcement.

You can head out to Github (https://github.com/PowerShell/PowerShell) right now and take a look at it in its open source glory! You will also notice a flurry of activity on its Issues page and also add your own as well!

image

Now, I mentioned that Linux is now an OS that supports running PowerShell. Currently there are a few distributions that it seems to support in Ubuntu and CentOS, but I imagine that more will be added as time progresses. Also MacOS is included in the list that you can install it on.

image

With this being an Alpha release, there will be bugs…lots of them…but that is OK because in its current state, it is pretty awesome! And that only means things will continue to be more and more better as the team takes in all of the issues (both internal and external) and fixes those as well as other things that they want to work on.

Installing on Ubuntu 14.04

Let’s go ahead and perform an install on a Linux system. In my case, I went ahead and installed Ubuntu 14.04 on my virtual environment so I could try this out!

The download package for this is at https://github.com/PowerShell/PowerShell/releases/download/v6.0.0-alpha.9/powershell_6.0.0-alpha.9-1ubuntu1.14.04.1_amd64.deb which I placed in the downloads folder under my home share.

I then navigate to that location and run the following commands:

sudo apt-get install libunwind8 libicu52
sudo dpkg -i powershell_6.0.0-alpha.9-1ubuntu1.14.04.1_amd64.deb

Once that has completed, I simply run PowerShell to load the shell.

image

The first thing I notice when running $PSVersionTable is that this is the alpha for version 6.0.0. Also note that the PSEdition is Core, meaning that this is running on .Net Core and probably missing some features that we are used to on the Windows side.

Looking at the variables, I see 4 that really stand out to that help to determine whether this system is running CoreCLR, is either Linux, Windows or OSX.

image

Some cmdlets are missing, such as Get-EventLog and Get-Service. Also noticeably missing are the CIM cmdlets.

image

Some things such as Desired State Configuration and Pester are available to use.

Aliases are also not as they seem in this version of PowerShell.

image

The Linux commands for ls and ps are not treated like PowerShell aliases currently, so you definitely want to keep that in mind. Also, case sensitivity, while not worried about in Windows should be respected here not for the commands, but for the file/folder structure.

image

Community Support

So with that announcement, when are we going to see some community involvement with porting their modules over to support Linux? Well, that is happening now! For instance, I am working on my PoshRSJob module to bring it into the Linux fold.

ubuntu_runspace

Needless to say that it is not there yet. I am seeing issues with what are usually public properties now being made private.

ubuntu_runspaceconfig

Will that change? I don’t know but I am working to work around that so I can have my module provide cross platform support to the masses Smile.

So with that, go ahead and give PowerShell a try on Linux or OSX and let everyone know how you like it. Got a bug or issue, drop over to GitHub and let the team know so they can prioritize it and make it better!

Reference Links

http://www.powershellmagazine.com/2016/08/18/open-source-powershell-on-windows-linux-and-osx/

https://blogs.msdn.microsoft.com/powershell/2016/08/18/powershell-on-linux-and-open-source-2/

https://azure.microsoft.com/en-us/blog/powershell-is-open-sourced-and-is-available-on-linux/

Posted in powershell | Tagged , , , , , | Leave a comment

Viewing Net Sessions using PowerShell and PInvoke

We are all used to view net sessions on a local or remote system by calling net session to see what clients are connected to another system such as a file server or even a domain controller. This provides a nice way to determine where a user might be logged in at assuming that they have an active session or one that is just idle. The problem with net session is that it requires admin rights on the system that you are running the command against in order for it to provide the data you need.

image

If you are a server admin, then this really isn’t that much of a deal as you can just use PowerShell remoting to query a bunch of  systems and get the information that you need. I wanted a way to  do this without worrying about admin rights or being able to remote into a system to get the information so I decided to look beyond the usual .Net and native PowerShell approaches to see if anything was available. The next logical step was to look lower in the stack and the Win32API and see what functions were available for me to use. I’ve done a fair amount of work with PInvoke and wasn’t afraid to see what kind of fun would await me if I happened to find something that might fit what I need.

After a decent amount of research,  I came across the NetSessionEnum function which provided me exactly the type of data that I am looking for. As with many of these functions, it isn’t enough to create the method to use but also to set up the Structs and/or Enums that the functions require to support the marshaling of data in and out of managed memory. In this case,  I have to make use of the SESSION_INFO_10 structure as I am looking to only return back data on the session which provides the client, username, active time and idle time. The image below highlights the structure I picked and also the other available Structs if I wanted different types of data.

image

The last thing that I need to look at is adding NetApiBufferFree so I can be sure to free up the buffer and avoid any unnecessary memory leaks.

Now that we have this out of the way, we can look to put these pieces together and make a function that we can use repeatedly.

If you want to see more about using pinvoke with PowerShell and a better look at what you need to do, then check out this article I did a while back. For this article, I am going to just point out one part that  I had to do in order to allow the parameters within the NetSessionEnum to properly work with the method.

Looking at the pinvoke signature for this function out on http://www.pinvoke.net/default.aspx/netapi32/NetSessionEnum.html, you can see that the first 3 parameters are a little more unique than what I typically see in that they unmanaged type of LPWStr which is a 32 bit pointer.

SNAGHTML31a8e4

That means that my usual approach to dynamically building the method has to account for this, otherwise just adding a string type will result in the method throwing an error when used.

I will build the method just like I normally would and give it all of the proper parameter and return value types that it needs. Instead of proceeding by adding some custom attributes to the method, I am going to focus first on defining the parameters that have special requirements by building custom attributes for those first.

 

#region Custom Attribute Builder
$ctor = [System.Runtime.InteropServices.MarshalAsAttribute].GetConstructor(@([System.Runtime.InteropServices.UnmanagedType]))
$CustomAttribute = [System.Runtime.InteropServices.UnmanagedType]::LPWStr
$CustomAttributeBuilder = New-Object System.Reflection.Emit.CustomAttributeBuilder -ArgumentList $ctor, $CustomAttribute
#endregion Custom Attribute Builder

 

Notice here that I am defining the LPWStr unmanaged type within the custom attribute. This will be important to add to my parameters that need it. Fortunately, these happen to be the first 3 parameters of the method (Servername, UserClientName and Username).  I need to make use of the DefineParameter method that is within the method object that I created earlier. All I need is the position of the parameter, a parameter attribute and supply $Null for the last parameter. After that I use SetCustomAttribute and give the parameter the extra attribute that it needs and there will be no problems with these parameters causing issues on the method.

 

#Define first three parameters with custom attributes
1..3 | ForEach {
    $Parameter = $PInvokeMethod.DefineParameter(
        $_,
        [System.Reflection.ParameterAttributes]::In,
        $Null
    )
    $Parameter.SetCustomAttribute(
        $CustomAttributeBuilder
    )
}

My function, Get-NetSession will get around that limitation that we saw earlier when trying to view the sessions on a domain controller. By design, I exclude sessions that this command creates on the remote system by looking at the current user and computer and performing the exclusion. If the user happens to be on a different system also, then it will be displayed.

Get-NetSession –Computername vdc1

image

I can use the –IncludeSelf parameter to include my own sessions create by the command.

Get-NetSession –Computername vdc1 –IncludeSelf

image

I also have a UserName parameter that accepts a single string to filter for a particular user if to avoid looking at a large dump of data.

You can find the script to download below as well as the source code from my GitHub repo at the bottom.

Download Script

https://gallery.technet.microsoft.com/scriptcenter/View-Net-Sessions-locally-d6eb2ba0

Source Code

https://github.com/proxb/PInvoke/blob/master/Get-NetSession.ps1

Posted in powershell | Tagged , , , | 2 Comments