How to investigate an unknown method in powershell? - powershell

I'm learning Powershell by myself .There are tons of information around about the most common things, however when I'm faced with a slightly too specific thing, either I follow someone's previous steps or I'm lost. For instance, I don't know how to get help for specific methods. I'll give you my current situation as an example, however the question is about any case, not only this one.
I'm trying to automate some Internet Explorer browsing, and for that I'm using this guide.
However it only shows an example on GetElementsByTagName. I already know how to use Get-MemberSo I follow his code like this:
$ie = new-object -ComObject "InternetExplorer.Application"
$ie.silent = $true
$ie.navigate($requestUri)
while($ie.Busy) { Start-Sleep -Milliseconds 100 }
$doc = $ie.Document
After that I use Get-Member -InputObject $doc so I know all possible methods and I see the one I need: GetElementByID. I try to use it like this:
PS C:\Users\Myuser> $main=$doc.getElementById("main")
Cannot find an overload for "getElementById" and the argument count: "1".
At line:1 char:1
+ $main=$doc.getElementById("main")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodCountCouldNotFindBest
I get that it expects more than one argument, but I don't know how to get or find any documentation about it.
What's your go-to when you want to know what a function/method does and what does it expect?

PowerShell offers a very convenient way to inspect method and even constructor signatures (overloads):
Simply access the method name without parentheses, and PowerShell will show you all overloads (signatures) defined for this method; .e.g:
# Method
PS> 'foo'.ToUpper # Inspect the .ToUpper() method - NO () at the end.
OverloadDefinitions
-------------------
string ToUpper()
string ToUpper(cultureinfo culture)
# Constructors, via a type literal (`[...]`) and its pseudo static new() method:
PS> [xml]::new # Constructors for [xml] (System.Xml.XmlDocument)
OverloadDefinitions
-------------------
xml new()
xml new(System.Xml.XmlNameTable nt)
This technique generally also works for COM objects, such as in your case:
$ie = New-Object -ComObject InternetExplorer.Application
$ie.Navigate('http://example.org')
$ie.Document.getElementById # Get signatures for .getElementById()
In Windows PowerShell v5.1 on Windows 10 this yields (abridged):
OverloadDefinitions
-------------------
mshtml.IHTMLElement ... getElementById(string v)
mshtml.IHTMLElement ... DispHTMLDocument.getElementById(string v)
mshtml.IHTMLElement ... IHTMLDocument3.getElementById(string v)
Caveat: For some COM objects, the signatures are listed without parameter names in versions before PowerShell Core v7, which can make it much harder to understand what it does.
For instance, COM Automation server WScript.Shell is affected:
Windows PowerShell and PowerShell Core 6.x:
PS> (New-Object -ComObject WScript.Shell).Popup
OverloadDefinitions
-------------------
int Popup (string, Variant, Variant, Variant) # !! No parameter names.
In PowerShell Core 7.0 (preview 5):
PS> (New-Object -ComObject WScript.Shell).Popup
OverloadDefinitions
-------------------
int Popup (string Text, Variant SecondsToWait, Variant Title, Variant Type) # OK

For cmdlets, I look up the cmdlet online documentation (you can check on https://learn.microsoft.com or just google it). A shortcut to get to online documentation for most cmdlets is to use Get-Help cmdlet -Online. For example:
Get-Help Start-Process -Online
You can also use the help alias in place of Get-Help if you wish.
For .NET classes I also use https://learn.microsoft.com, but to make sure you get the right class and member, I will search for the full name including the namespace (you can see this when you pipe an object to Get-Member. From the class documentation, you can navigate to the member documentation on that class.
For WMI classes, once again I also look at https://learn.microsoft.com, searching for the WMI class name, and navigating to any member properties or functions attributed to that class.
Of course, this is all assuming you are looking for information on Microsoft constructs. If you are looking for third-party library documentation, you will need to reference that third-party's documentation.
In general, if you're not quite sure where to find API documentation on something, a good thing to try searching via any search engine is "LIBRARYNAME api documentation", where LIBRARYNAME could be either the library or class you want to find documentation for. Usually you'll find something relevant in the first few hits.

Related

Does Powershell remoting recursively serialize the return value? [duplicate]

I'm trying to execute the Invoke-Sqlcmd command (from the SqlServer module) to run a query as a different AD user. I know there's the -Credential argument, but that doesn't seem to work.
Thus, I thought using Start-Job might be an option, as shown in the snippet below.
$username = 'dummy_domain\dummy_user'
$userpassword = 'dummy_pwd' | ConvertTo-SecureString -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential ($username, $password)
$job = Start-Job -ScriptBlock {Import-Module SqlServer; Invoke-Sqlcmd -query "exec sp_who" -ServerInstance 'dummy_mssql_server' -As DataSet} -Credential $credential
$data = Receive-Job -Job $job -Wait -AutoRemoveJob
However, when looking at the variable type that the job returned, it isn't what I expected.
> $data.GetType().FullName
System.Management.Automation.PSObject
> $data.Tables[0].GetType().FullName
System.Collections.ArrayList
If I run the code in the ScriptBlock directly, these are the variable types that PS returns:
> $data.GetType().FullName
System.Data.DataSet
> $data.Tables[0].GetType().FullName
System.Data.DataTable
I tried casting the $data variable to [System.Data.DataSet], which resulted in the following error message:
Cannot convert value "System.Data.DataSet" to type "System.Data.DataSet".
Error: "Cannot convert the "System.Data.DataSet" value of type
"Deserialized.System.Data.DataSet" to type "System.Data.DataSet"."
Questions:
Is there a better way to run SQL queries under a different AD account, using the Invoke-Sqlcmd command?
Is there a way to get the correct/expected variable type to be returned when calling Receive-Job?
Update
When I run $data.Tables | Get-Member, one of the properties returned is:
Tables Property Deserialized.System.Data.DataTableCollection {get;set;}
Is there a way to get the correct/expected variable type to be returned when calling Receive-Job?
Due to using a background job, you lose type fidelity: the objects you're getting back are method-less emulations of the original types.
Manually recreating the original types is not worth the effort and may not even be possible - though perhaps working with the emulations is enough.
Update: As per your own answer, switching from working with System.DataSet to System.DataTable resulted in serviceable emulations for you.[1]
See the bottom section for more information.
Is there a better way to run SQL queries under a different AD account, using the Invoke-Sqlcmd command?
You need an in-process invocation method in order to maintain type fidelity, but I don't think that is possible with arbitrary commands if you want to impersonate another user.
For instance, the in-process (thread-based) alternative to Start-Job - Start-ThreadJob - doesn't have a -Credential parameter.
Your best bet is therefore to try to make Invoke-SqlCmd's -Credential parameter work for you or find a different in-process way of running your queries with a given user's credentials.
Serialization and deserialization of objects in background jobs / remoting / mini-shells:
Whenever PowerShell marshals objects across process boundaries, it employs XML-based serialization at the source, and deserialization at the destination, using a format known as CLI XML (Common Language Infrastructure XML).
This happens in the context of PowerShell remoting (e.g., Invoke-Command calls with the
-ComputerName parameter) as well as in background jobs (Start-Job) and so-called mini-shells (which are implicitly used when you call the PowerShell CLI from inside PowerShell itself with a script block; e.g., powershell.exe { Get-Item / }).
This deserialization maintains type fidelity only for a limited set of known types, as specified in MS-PSRP, the PowerShell Remoting Protocol Specification. That is, only instances of a fixed set of types are deserialized as their original type.
Instances of all other types are emulated: list-like types become [System.Collections.ArrayList] instances, dictionary types become [hasthable] instances, and other types become method-less (properties-only) custom objects ([pscustomobject] instances), whose .pstypenames property contains the original type name prefixed with Deserialized. (e.g., Deserialized.System.Data.DataTable), as well as the equally prefixed names of the type's base types (inheritance hierarchy).
Additionally, the recursion depth for object graphs of non-[pscustomobject] instances is limited to 1 level - note that this includes instance of PowerShell custom classes, created with the class keyword: That is, if an input object's property values aren't instance of well-known types themselves (the latter includes single-value-only types, including .NET primitive types such as [int], as opposed to types composed of multiple properties), they are replaced by their .ToString() representations (e.g., type System.IO.DirectoryInfo has a .Parent property that is another System.IO.DirectoryInfo instance, which means that the .Parent property value serializes as the .ToString() representation of that instance, which is its full path string); in short: Non-custom (scalar) objects serialize such that property values that aren't themselves instances of well-known types are replaced by their .ToString() representation; see this answer for a concrete example.
By contrast, explicit use of CLI XML serialization via Export-Clixml defaults to a depth of 2 (you can specify a custom depth via -Depth and you can similarly control the depth if you use the underlying System.Management.Automation.PSSerializer type directly).
Depending on the original type, you may be able to reconstruct instances of the original type manually, but that is not guaranteed.
(You can get the original type's full name by calling .pstypenames[0] -replace '^Deserialized\.' on a given custom object.)
Depending on your processing needs, however, the emulations of the original objects may be sufficient.
[1] Using System.DataTable results in usable emulated objects, because you get a System.Collections.ArrayList instance that emulates the table, and custom objects with the original property values for its System.DataRow instances. The reason this works is that PowerShell has built-in logic to treat System.DataTable implicitly as an array of its data rows, whereas the same doesn't apply to System.DataSet.
I can't say for question 2 as I've never used the job commands but when it comes to running the Invoke-Sqlcmd I always make sure that the account that runs the script has the correct access to run the SQL.
The plus to this is that you don't need to store the credentials inside the script, but is usually a moot point as the scripts are stored out of reach of most folks, although some bosses can be nit picky!
Out of curiosity how do the results compare if you pipe them to Get-Member?
For those interested, below is the code I implemented. Depending on whether or not $credential is passed, Invoke-Sqlcmd will either run directly, or using a background job.
I had to use -As DataTables instead of -As DataSet, as the latter seems to have issues with serialisation/deserialisation (see accepted answer for more info).
function Exec-SQL($server, $database, $query, $credential) {
$sqlData = #()
$scriptBlock = {
Param($params)
Import-Module SqlServer
return Invoke-Sqlcmd -ServerInstance $params.server -Database $params.database -query $params.query -As DataTables -OutputSqlErrors $true
}
if ($PSBoundParameters.ContainsKey("credential")) {
$job = Start-Job -ScriptBlock $scriptBlock -Credential $credential -ArgumentList $PSBoundParameters
$sqlData = Receive-Job -Job $job -Wait -AutoRemoveJob
} else {
$sqlData = & $scriptBlock -params $PSBoundParameters
}
return $sqlData
}

PowerShell HtmlAgilityPack within class method

I use HtmlAgilityPack in my PowerShell script. As per the documentation my code is like:
[Reflection.Assembly]::LoadFile("d:\Apl\HtmlAgilityPack.1.11.29\lib\Net40\HtmlAgilityPack.dll")
[HtmlAgilityPack.HtmlDocument]$htmlDoc = #{}
$htmlDoc.LoadHtml($resp.content)
...
This works fine as expected as long as I use simple script with functions only (no classes). Now I have refactored my code so that the [HtmlAgilityPack.HtmlDocument] type is used within a class method.
Now the compiler complains "Unable to find type [HtmlAgilityPack.HtmlDocument].". How do I load/import the type in order to be able to use it within a class method?
I use PowerShell 7.0 in Windows 10 environment.
I have found the solution
Add-Type -path "d:\Apl\HtmlAgilityPack.1.11.29\lib\Net40\HtmlAgilityPack.dll"
$htmlDoc = New-Object HtmlAgilityPack.HtmlDocument

Import modules like `import <module> as <alias>` in Powershell

So here's what I'm trying to do - in Java, for instance, if I want to use an ArrayList<> in my program I just write
...
import java.util.ArrayList
...
And then from there on I can just write ArrayList<String> arr = new ArrayList<String>... etc.
Also, in Python I can, for instance, write something like
from numpy.random import randint
And again, in my program I can just write randint(something).
Is there an equivalent way of doing this in PowerShell? I'd like to use a System.Collections.ArrayList, but I can't find any syntax online.
Reference a namespace with a using namespace statement:
using namespace System.Collections
Then you can omit the namespace when creating a new object
$arr = New-Object ArrayList
You have to create New-Object forSystem.Collections.ArrayList :
$arr= New-Object System.Collections.ArrayList
The New-Object cmdlet creates an instance of a .NET Framework or COM
object.
After that you can use ArrayList methods :
$arr.Add('test')
I will throw this in just for fun. First, create a function:
function New-ArrayList{
New-Object System.Collections.ArrayList
}
Next create an alias:
New-Alias -Name al -Value New-ArrayList
Then use it:
$myArrayList = al
The best option is probably the one given by #laika, but you can also use Type Accelerators. These used to be easy to work with, but from PSv3, things were made a bit more difficult (probably to force you to use better techniques). Built-in accelerators include the standard .NET types such as int, string, DateTime, etc, and more obscure things such as System.Management.ManagementObject. You can get a full list like this:
[PsObject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::Get
To create your own, do this:
[PsObject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::Add('ArrayList',"System.Collections.ArrayList")
You can then use the new accelerator like this:
$arrayList = New-Object ArrayList
$arrayList.Add(1)
$arrayList.Add(2)
$arrayList.Add(3)
$arrayList
1
2
3
Note that these last only for the current session, so would need recreated everytime you wanted to use them.

Is there a simple way to create custom types in Powershell?

Just want to make a simple custom type from [System.Collections.ArrayList] to, say, just shorter[arrayList] or something like that and put it into a module for convenience. Looked into Add-Type but couldn't figure out if it fits and how to do it exactly. What I want to get is:
[ArrayList]<-[System.Collections.ArrayList] #Something like that
$myArList=New-Object ArrayList
$myArList.Add(1,2,3)
You're looking for a type accelerator.
[accelerators]::add('arrayList','System.Collections.ArrayList')
I would avoid using non-standard accelerators. PowerShell has good tab completion support for classes since at least v3.
So if you type [arraylTAB then it will complete the full name for you.
Ryan Bemrose brought up a great point; the [accelerators] type accelerator is not available by default, but you can create it like so:
$acc = [psobject].assembly.gettype("System.Management.Automation.TypeAccelerators")
$acc::Add('accelerators', $acc)
If you simply want to avoid re-typing System.Collections.ArrayList all the time, you can simply assign a type literal to a variable and use that:
$ListType = [System.Collections.ArrayList]
$MyArrayList = New-Object $ListType
# more code
$AnotherArrayList = New-Object $ListType
or, using the v5.0 new() constructor:
$MyArrayList = $ListType::new()

How to specify custom COM enum as PowerShell method parameter

How can I create a reference for a enum declared inside a COM object to pass to a method that requires it? Specifically, there is a method SetHomeDir on a 3rd-party COM object that takes in an enum as the only parameter. The definition that parameter expects is basically:
typedef enum
{
abFalse = 0,
abTrue = 1,
abInherited = -2
} SFTPAdvBool;
It appears to be defined somewhere in the COM object, but not as an object that I can create with New-Object. I have tried the following and they all give the same MX Error: 7 response:
$obj.SetHomeDir($true)
$obj.SetHomeDir(1)
$obj.SetHomeDir([Object]1)
$obj.SetHomeDir([Int16]1)
Exception calling "SetHomeDir" with "1" argument(s): "MX Error: 7 (00000007)"
Here are the results from trying some other approaches:
PS C:\> New-Object -ComObject SFTPCOMINTERFACELib.SFTPAdvBool
New-Object : Cannot load COM type SFTPCOMINTERFACELib.SFTPAdvBool.
PS C:\> [SFTPAdvBool]::abTrue
Unable to find type [SFTPAdvBool]:
make sure that the assembly containing this type is loaded.
PS C:\> [SFTPCOMINTERFACELib.SFTPAdvBool]::abTrue
Unable to find type [SFTPCOMINTERFACELib.SFTPAdvBool]:
make sure that the assembly containing this type is loaded.
The method signature that COM exposes to PowerShell looks like this:
PS C:\> $user.SetHomeDir
MemberType : Method
OverloadDefinitions : {void SetHomeDir (SFTPAdvBool)}
TypeNameOfValue : System.Management.Automation.PSMethod
Value : void SetHomeDir (SFTPAdvBool)
Name : SetHomeDir
IsInstance : True
Note: This is running under PowerShell 2.0 on Windows Server 2008 R2, but I would consider upgrading the Windows Management Framework to a newer version if necessary.
Update: Here is a screenshot from the Visual Studio object explorer, in case that offers up any clues.
We ended up using a compiled C# interop wrapper around the COM object. I was able to specify an int as the parameter and just used a case statement to pass the correct value from the enum. As far as I can tell there isn't a way to do this directly from Powershell and requires wrapping the COM object in managed code.
We have opened a dialog with Globalscape and hopefully this will be something they address in a future release.
We can try to fool the com object by creating the enum on your own and pass it to the function:
If you can upgrade to Powershell 5 try (in Powershell - enum is a new keyword in ver 5):
Enum SFTPAdvBool
{
abFalse = 0
abTrue = 1
abInherited = -2
}
And call:
$obj.SetHomeDir([SFTPAdvBool]::abTrue)
for anything older than PS 5 you can try:
$code = #"
namespace SFTPCOMINTERFACELib {
public enum SFTPAdvBool {
abFalse = 0,
abTrue = 1,
abInherited = -2
}
}
"#
Add-Type -TypeDefinition $code -Language CSharpVersion3
And call:
$obj.SetHomeDir([SFTPCOMINTERFACELib.SFTPAdvBool]::abTrue)
This will basically create a C# enum and will add it as a type into Powershell.
PowerShell v2.0 is archaic at this point, but it shouldn't be stopping you here. SFTPAdvBool appears to be from the GlobalScape EFT Server COM API, so that's what I'm assuming.
The issue is that you need a value of type SFTPAdvBool, according to the C# scripting examples (See ConfigureUser.cs for one use). For a .Net object, you'd define that as [SFTPAdvBool]::abTrue or [SFTPCOMINTERFACELib.SFTPAdvBool]::abTrue, but I'm not sure if that will work. I've not worked with COM enums in PowerShell before. You might need New-Object -ComObject SFTPCOMINTERFACELib.SFTPAdvBool or some variant.
If nothing works, you could use VBScript, or C#, or contact GlobalScape in the hopes they join the 21st century and drop COM, or use WinSCP's .Net library... but I'm betting you can't since you're working someplace that paid for EFT Server.
There are some hints in their knowledge base that the library doesn't always work correctly out of process, so that would apply remotely, or when using the 32-bit library from a 64-bit process.
For example:
http://forums.globalscape.com/PrintTopic38567.aspx
The solution would be to use the 32-bit powershell in C:\Windows\SysWOW64\WindowsPowerShell\ If you need to call that remotely you may have to do it explicitly using the & operator.