How can I optimize the command Get-ComputerInfo? - windows

Every time I get a snippet of info from this command, Windows will run the whole function, which takes time. The script works, but its slow. Because of the way the rest of this code is written, I cannot store Get-ComputerInfo as a string because I still need to pull strings from it without the tediousness of using substrings. What are my options here?
A sample of my code is here:
# Get username and format
$USRNAME = Get-ComputerInfo -Property WindowsRegisteredOwner | Out-String
$USRNAME = $USRNAME.Split('',[System.StringSplitOptions]::RemoveEmptyEntries)
$USRNAME = $USRNAME[2]
# Get BIOS S/N and format
$BIOSSN = Get-ComputerInfo -Property BiosSeralNumber | Out-String
$BIOSSN = $BIOSSN.Split('',[System.StringSplitOptions]::RemoveEmptyEntries)
$BIOSSN = $BIOSSN[2]
# Get BIOS Manufacturer and format
$MANUFAC = Get-ComputerInfo -Property CsManufacturer | Out-String
$MANUFAC = $MANUFAC.Split('',[System.StringSplitOptions]::RemoveEmptyEntries)
$MANUFAC = $MANUFAC[2]
# Get BIOS Model and format
$MODEL = Get-ComputerInfo -Property CsModel | Out-String
$MODEL = $MODEL.Split('',[System.StringSplitOptions]::RemoveEmptyEntries)
$MODEL = $MODEL[2..25]
As mentioned previously, I tried to output as a string and it got messy.

Call Get-ComputerInfo once, the store the output object in a variable, and then finally reuse that:
I still need to pull strings from it without the tediousness of using substrings
You really don't - Get-ComputerInfo outputs an object that has each piece of information neatly stored in separate properties - to get the WindowsRegisteredOwner value, simply use the . member access operator to dereference the WindowsRegisteredOwner property, and so on for the remaining properties:
$allInfo = Get-ComputerInfo -Property WindowsRegisteredOwner, BiosSeralNumber, CsManufacturer, CsModel
$USRNAME = $allInfo.WindowsRegisteredOwner
$BIOSSN = $allInfo.BiosSeralNumber
$MANUFAC = $allInfo.CsManufacturer
$MODEL = $allInfo.CsModel

Related

Extract text from command line script

I have a mining application that shows the data I need but the application doesn't have an api to grab it. How can I extract the string to parse the data with powershell or equivalent?
The data drops a line like below every second,
ID(grabbed from board) hash:(label) hashrate(variable) errors:(label) #(variable) temp(variable) volts(variable) solutions:(label) #(variable) shares:(label) #(variable)
Example:
ABC1000234 hash: 9.8Gh/s errors: 0.000% 26.3C 0.74V solutions: 539/539
shares: 33
I need the hashrate, temp and volts or even better a way to send every string out to a port I can listen on to a url like "strings". If I can get the string to post to a port such as 4068. Then I could use powershell and netcat to listen to the port on http://127.0.0.1:4068.
Here is what I was going to do for powershell:
$serveraddress = '127.0.0.1'
$serverport = '4068'
$threadinfo = echo 'strings' | nc $serveraddress $serverport
$mineridstring = $stringsinfo.Split(';')[1] $minderid =
$mineridstring.Split('=')[0]
$hashstring = $stringsinfo.Split(';')[2] $hash =
$hashstring.Split('=')[1]
$tempstring = $stringsinfo.Split(';')[4] $tempc =
$tempstring.Split('=')[0]
$voltstring = $stringsinfo.Split(';')[5] $volts =
$voltsstring.Split('=')[0]
Invoke-RestMethod -Uri https://www.rigmanager.xyz/rig.php -Method Post `
-Body #{minerid = $minerid; hashrate = $hashrate; tempc = $temp; $volts = $volts} -UseBasicParsing
Push them to a message queue, and then you can subscribe any number of users/applications to that stream.
Check out Apache Kafka or any of the cloud-based equivalents on AWS, IBM Cloud, GCP, etc.
Parsing your string is something regex can handle, although unless you need the data indexed for querying/searching, you can pushing that off to the end user/application and just serve them the whole message.
An easy way to do this is with named captures in a regex.
PS C:\src\t> type exttext.ps1
$s = 'ABC1000234 hash: 9.8Gh/s errors: 0.000% 26.3C 0.74V solutions: 539/539 shares: 33'
$doesit = $s -match '^(?<id>.*) hash: (?<hashrate>.*) errors: (?<errors>[0-9.]+%) (?<temp>.*) (?<volts>.*) solutions: .* shares: \d+$'
$Matches.id
$Matches.hashrate
$Matches.errors
$Matches.temp
$Matches.volts
PS C:\src\t> .\exttext.ps1
ABC1000234
9.8Gh/s
0.000%
26.3C
0.74V

Dynamic parameter value depending on another dynamic parameter value

Starting premise: very restrictive environment, Windows 7 SP1, Powershell 3.0. Limited or no possibility of using external libraries.
I'm trying to re-write a bash tool I created previously, this time using PowerShell. In bash I implemented autocompletion to make the tool more user friendly and I want to do the same thing for the PowerShell version.
The bash version worked like this:
./launcher <Tab> => ./launcher test (or dev, prod, etc.)
./launcher test <Tab> => ./launcher test app1 (or app2, app3, etc.)
./launcher test app1 <Tab> => ./launcher test app1 command1 (or command2, command3, etc.).
As you can see, everything was dynamic. The list of environments was dynamic, the list of application was dynamic, depending on the environment selected, the list of commands was also dynamic.
The problem is with the test → application connection. I want to show the correct application based on the environment already selected by the user.
Using PowerShell's DynamicParam I can get a dynamic list of environments based on a folder listing. I can't however (or at least I haven't found out how to) do another folder listing but this time using a variable based on the existing user selection.
Current code:
function ParameterCompletion {
$RuntimeParameterDictionary = New-Object Management.Automation.RuntimeDefinedParameterDictionary
# Block 1.
$AttributeCollection = New-Object Collections.ObjectModel.Collection[System.Attribute]
$ParameterName = "Environment1"
$ParameterAttribute = New-Object Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$ParameterAttribute.Position = 1
$AttributeCollection.Add($ParameterAttribute)
# End of block 1.
$parameterValues = $(Get-ChildItem -Path ".\configurations" -Directory | Select-Object -ExpandProperty Name)
$ValidateSetAttribute = New-Object Management.Automation.ValidateSetAttribute($parameterValues)
$AttributeCollection.Add($ValidateSetAttribute)
$RuntimeParameter = New-Object Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
# Block 2: same thing as in block 1 just with 2 at the end of variables.
# Problem section: how can I change this line to include ".\configurations\${myVar}"?
# And what's the magic incantation to fill $myVar with the info I need?
$parameterValues2 = $(Get-ChildItem -Path ".\configurations" -Directory | Select-Object -ExpandProperty Name)
$ValidateSetAttribute2 = New-Object Management.Automation.ValidateSetAttribute($parameterValues2)
$AttributeCollection2.Add($ValidateSetAttribute2)
$RuntimeParameter2 = New-Object
Management.Automation.RuntimeDefinedParameter($ParameterName2, [string], $AttributeCollection2)
$RuntimeParameterDictionary.Add($ParameterName2, $RuntimeParameter2)
return $RuntimeParameterDictionary
}
function App {
[CmdletBinding()]
Param()
DynamicParam {
return ParameterCompletion "Environment1"
}
Begin {
$Environment = $PsBoundParameters["Environment1"]
}
Process {
}
}
I would recommend using argument completers, which are semi-exposed in PowerShell 3 and 4, and fully exposed in version 5.0 and higher. For v3 and v4, the underlying functionality is there, but you have to override the TabExpansion2 built-in function to use them. That's OK for your own session, but it's generally frowned upon to distribute tools that do that to other people's sessions (imagine if everyone tried to override that function). A PowerShell team member has a module that does this for you called TabExpansionPlusPlus. I know I said overriding TabExpansion2 was bad, but it's OK if this module does it :)
When I needed to support versions 3 and 4, I would distribute my commands in modules, and have the modules check for the existence of the 'Register-ArgumentCompleter' command, which is a cmdlet in v5+ and is a function if you have the TE++ module. If the module found it, it would register any completer(s), and if it didn't, it would notify the user that argument completion wouldn't work unless they got the TabExpansionPlusPlus module.
Assuming you have the TE++ module or PSv5+, I think this should get you on the right track:
function launcher {
[CmdletBinding()]
param(
[string] $Environment1,
[string] $Environment2,
[string] $Environment3
)
$PSBoundParameters
}
1..3 | ForEach-Object {
Register-ArgumentCompleter -CommandName launcher -ParameterName "Environment${_}" -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
$PathParts = $fakeBoundParameter.Keys | where { $_ -like 'Environment*' } | sort | ForEach-Object {
$fakeBoundParameter[$_]
}
Get-ChildItem -Path ".\configurations\$($PathParts -join '\')" -Directory -ErrorAction SilentlyContinue | select -ExpandProperty Name | where { $_ -like "${wordToComplete}*" } | ForEach-Object {
New-Object System.Management.Automation.CompletionResult (
$_,
$_,
'ParameterValue',
$_
)
}
}
}
For this to work, your current working directory will need a 'configurations' directory contained in it, and you'll need at least three levels of subdirectories (reading through your example, it looked like you were going to enumerate a directory, and you would go deeper into that structure as parameters were added). The enumerating of the directory isn't very smart right now, and you can fool it pretty easy if you just skip a parameter, e.g., launcher -Environment3 <TAB> would try to give you completions for the first sub directory.
This works if you will always have three parameters available. If you need a variable # of parameters, you could still use completers, but it might get a little trickier.
The biggest downside would be that you'd still have to validate the users' input since completers are basically just suggestions, and users don't have to use those suggestions.
If you want to use dynamic parameters, it gets pretty crazy. There may be a better way, but I've never been able to see the value of dynamic parameters at the commandline without using reflection, and at that point you're using functionality that could change at the next release (the members usually aren't public for a reason). It's tempting to try to use $MyInvocation inside the DynamicParam {} block, but it's not populated at the time the user is typing the command into the commandline, and it only shows one line of the command anyway without using reflection.
The below was tested on PowerShell 5.1, so I can't guarantee that any other version has these exact same class members (it's based off of something I first saw Garrett Serack do). Like the previous example, it depends on a .\configurations folder in the current working directory (if there isn't one, you won't see any -Environment parameters).
function badlauncher {
[CmdletBinding()]
param()
DynamicParam {
#region Get the arguments
# In it's current form, this will ignore parameter names, e.g., '-ParameterName ParameterValue' would ignore '-ParameterName',
# and only 'ParameterValue' would be in $UnboundArgs
$BindingFlags = [System.Reflection.BindingFlags] 'Instance, NonPublic, Public'
$Context = $PSCmdlet.GetType().GetProperty('Context', $BindingFlags).GetValue($PSCmdlet)
$CurrentCommandProcessor = $Context.GetType().GetProperty('CurrentCommandProcessor', $BindingFlags).GetValue($Context)
$ParameterBinder = $CurrentCommandProcessor.GetType().GetProperty('CmdletParameterBinderController', $BindingFlags).GetValue($CurrentCommandProcessor)
$UnboundArgs = #($ParameterBinder.GetType().GetProperty('UnboundArguments', $BindingFlags).GetValue($ParameterBinder) | where { $_ } | ForEach-Object {
try {
if (-not $_.GetType().GetProperty('ParameterNameSpecified', $BindingFlags).GetValue($_)) {
$_.GetType().GetProperty('ArgumentValue', $BindingFlags).GetValue($_)
}
}
catch {
# Don't do anything??
}
})
#endregion
$ParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
# Create an Environment parameter for each argument specified, plus one extra as long as there
# are valid subfolders under .\configurations
for ($i = 0; $i -le $UnboundArgs.Count; $i++) {
$ParameterName = "Environment$($i + 1)"
$ParamAttributes = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParamAttributes.Add((New-Object Parameter))
$ParamAttributes[0].Position = $i
# Build the path that will be enumerated based on previous arguments
$PathSb = New-Object System.Text.StringBuilder
$PathSb.Append('.\configurations\') | Out-Null
for ($j = 0; $j -lt $i; $j++) {
$PathSb.AppendFormat('{0}\', $UnboundArgs[$j]) | Out-Null
}
$ValidParameterValues = Get-ChildItem -Path $PathSb.ToString() -Directory -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name
if ($ValidParameterValues) {
$ParamAttributes.Add((New-Object ValidateSet $ValidParameterValues))
$ParamDictionary[$ParameterName] = New-Object System.Management.Automation.RuntimeDefinedParameter (
$ParameterName,
[string[]],
$ParamAttributes
)
}
}
return $ParamDictionary
}
process {
$PSBoundParameters
}
}
The cool thing about this one is that it can keep going as long as there are folders, and it automatically does parameter validation. Of course, you're breaking the laws of .NET by using reflection to get at all those private members, so I would consider this a terrible and fragile solution, no matter how fun it was to come up with.

Find accounts innactive for X days in specific OUs

I am trying to get a PowerShell script running that will display a list of all users who have been inactive (or not logged in) in x days. That part was easy enough to find and modify a script for, but I am having trouble setting it so I can specify only certain OUs and sub OUs within the domain. This is what I have so far, though I think I might have to use another method to accomplish this:
#Import Ad Module
Import-Module ActiveDirectory
#SearchBase
$searchB = (Get-Content -Path C:\scripts\ous.txt)
#Time accounts have been inactive
$tSpan = "145"
Search-ADAccount -SearchBase $searchB -AccountInactive -UserOnly -Timepsan $tSpan |
Where {($_.DistinguishedName -notlike "specific sub-ou I don't want to check")} |
FT name,ObjectClass -A
The text file is in the format:
OU=first ou,OU=Parent OU,DC= thisDC,DC=dc,DC=DC
OU=third ou,OU=Parent OU,DC= this DC,DC=dc,DC=DC
OU=fourthou,OU=Parent OU,DC= thisDC,DC=dc,DC=DC
When I run this I get an error
Search-ADAccount : Directory object not found
Looks like you have a type mismatch in your -SearchBase parameter.
See Get-Help Search-ADAccount
Note that the value type for -SearchBase is string. You have three OUs in your text file, so Get-Content on that file is going to produce a string array (string[]).
Since the -SearchBase parameter will only accept a single value, you'll need to foreach through the OU list, giving on one OU at a time:
foreach ($OU in $SearchB)
{
search-adaccount -searchbase $OU -accountinactive -useronly -timepsan $tSpan.....
}

Keep Leading Zero In Export

I am using powershell to run a sql query and export to a csv file. Process works great, but it is dropping a leading 0 in one of my columns. Field type in SQL Server is a varchar (not an option to change it unfortunately), and here is my syntax. Is it possible to continue to use my powershell export process and keep the leading zero?
$GoodSyntax = "Select * From tableunknown"
$extractFile = "C:\Test.csv"
Execute-SQLquery
if (Execute-SQLquery $GoodSyntax)
Function Execute-SQLquery {
param ($GoodSyntax)
$server = "Server01"
$database = "database01"
$connectionTemplate = "Data Source={0};Integrated Security=SSPI;Initial Catalog={1};"
$connectionString = [string]::Format($connectionTemplate, $server, $database)
$connection = New-Object System.Data.SqlClient.SqlConnection
$connection.ConnectionString = $connectionString
$command = New-Object System.Data.SqlClient.SqlCommand
$command.CommandText = $QueryString
$command.Connection = $connection
$SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
$SqlAdapter.SelectCommand = $command
$DataSet = New-Object System.Data.DataSet
$rowCount = $SqlAdapter.Fill($DataSet)
$DataSet.Tables[0] | Export-Csv $extractFile -NoTypeInformation
$connection.Close()
EDIT ---
Upon further examination, and opening my CSV file in a raw editor like notepad++ the leading zeros are their! They are just dropped when you attempt to view the file in Excel. So Excel is the culprit here.
I have never seen sql drop any data from varchar. How are you testing this? Maybe the problem is not in the sql but in whatever you use to look at the data.
I know that Excel does remove leading 0's because it tries to be clever and convert chars to numbers but sql does not.
Try viewing the result of $DataSet.GetXml() which will show the raw data more clearly.
$connection.Close()
$DataSet.GetXml()
Update: Excel strips the leading 0's and is confusing you. The sql data is correct.
How to specify formatting when opening a csv file in excel
http://www.upenn.edu/computing/da/bo/webi/qna/iv_csvLeadingZeros.html
Normally I create an xml/html file from the data. Excel will open it and supports various attributes with the data that control formatting, but its messy.
You should be able to do something like:
$a = "1000"
$a = $a.substring($a.length - 3, 37)
Ref. - https://technet.microsoft.com/en-us/library/ee176945.aspx

PowerShell : GetNewClosure() and Cmdlets with validation

I'm trying to understand how .GetNewClosure() works within the context of a script cmdlet in PowerShell 2.
In essence I have a function that returns an object like so:
function Get-AnObject {
param(
[CmdletBinding()]
[Parameter(....)]
[String[]]$Id
..
[ValidateSet('Option1','Option2')]
[String[]]$Options
)
...
$T = New-Object PSCustomObject -Property #{ ..... }
$T | Add-Member -MemberType ScriptProperty -Name ExpensiveScriptProperty -Value {
$this | Get-ExpensiveStuff
}.GetNewClosure()
..
}
Providing I do not have the validate set options the closure appears to work fine. If it is included however the new closure fails with the following error.
Exception calling "GetNewClosure" with "0" argument(s): "Attribute cannot be added because it would cause the variable Options with value to become invalid."
Presumably the closure is trying to capture the context of the call to the Cmdlet. Since the parameter "Options" is not bound at all this is not nicely with the parameter validation.
I imagine it's possible to avoid this by placing validation as code within the body of the Cmdlet instead of making use of the [Validate*()] decorators -- but this seems nasty and quite obscure. Is there a way of fusing these two ideas?
The "Attribute cannot be added" message is (or was) a PowerShell bug, I've submitted it to Microsoft with this bug report. That particular issue seems to have been fixed, (perhaps around V5.1. but anyone interested in Powershell Closures may still find info below interesting.
There is a workaround which works in earlier versions, but first here's a simplified repro case that produces the same error:
function Test-ClosureWithValidation {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('Option1','Option2')]
[String[]]$Options
)
[scriptblock] $closure = {"OK"}.GetNewClosure();
$closure.Invoke()
}
Test-ClosureWithValidation -Options Option1
The workaround depends on the fact that GetNewClosure() works by iterating over the local variables in the calling script's context, binding these local variables into the script's context. The bug occurs because its copying the $Options variable including the validation attribute. You can work around the bug by creating a new context with only the local variables you need. In the simple repro above, it is a one-line workaround:
[scriptblock] $closure = &{ {"OK"}.GetNewClosure();}
The line above now creates a scope with no local variables. That may be too simple for your case; If you need some values from the outer scope, you can just copy them into local variables in the new scope, e.g:
[scriptblock] $closure = &{
$options = $options;
{"OK $options"}.GetNewClosure();
}
Note that the second line above creates a new $options variable, assigning it the value of the outer variable, the attributes don't propagate.
Finally, I'm not sure in your example why you need to call GetNewClosure at all. The variable $this isn't a normal local variable, it will be available in your script property whether or not you create a closure. Example:
function Test-ScriptPropertyWithoutClosure {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('Option1','Option2')]
[String[]]$Options
)
[pscustomobject]#{ Timestamp= Get-Date} |
Add-Member ScriptProperty ExpensiveScriptProperty {
$this | get-member -MemberType Properties| % Name
} -PassThru
}
Test-ScriptPropertyWithoutClosure -Options Option1 | fl
I believe this might work:
function Get-AnObject {
param(
[CmdletBinding()]
[Parameter(....)]
[String[]]$Id
..
[ValidateSet('Option1','Option2')]
[String[]]$Options
)
...
$sb = [scriptblock]::create('$this | Get-ExpensiveStuff')
$T = New-Object PSCustomObject -Property #{ ..... }
$T | Add-Member -MemberType ScriptProperty -Name ExpensiveScriptProperty -Value $sb
.. }
That delays creation of the script block until run time.

Resources