I've read that objects are serialized when you pass them into a script block called with start-job. This seems to work fine for strings and things, but I'm trying to pass an xml.XmlElement object in. I'm sure that the object is an XMLElement before I invoke the script block, but in the job, I get this error:
Cannot process argument transformation on parameter 'node'. Cannot convert the "[xml.XmlElement]System.Xml.XmlElement"
value of type "System.String" to type "System.Xml.XmlElement".
+ CategoryInfo : InvalidData: (:) [], ParameterBindin...mationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError
+ PSComputerName : localhost
So, how do I get my XmlElement back. Any ideas?
For what it's worth, this is my code:
$job = start-job -Name $node.LocalName -InitializationScript $DEFS -ScriptBlock {
param (
[xml.XmlElement]$node,
[string]$folder,
[string]$server,
[string]$user,
[string]$pass
)
sleep -s $node.startTime
run-action $node $folder $server $user $pass
} -ArgumentList $node, $folder, $server, $user, $pass
Apparently you can't pass XML nodes into script blocks because you can't serialize them. According to this answer you need to wrap the node into a new XML document object and pass that into the script block. Thus something like this might work:
$wrapper = New-Object System.Xml.XmlDocument
$wrapper.AppendChild($wrapper.ImportNode($node, $true)) | Out-Null
$job = Start-Job -Name $node.LocalName -InitializationScript $DEFS -ScriptBlock {
param (
[xml]$xml,
[string]$folder,
[string]$server,
[string]$user,
[string]$pass
)
$node = $xml.SelectSingleNode('/*')
sleep -s $node.startTime
run-action $node $folder $server $user $pass
} -ArgumentList $wrapper, $folder, $server, $user, $pass
Related
I need to write a powershell script that asynchronously reads from and writes to a System.IO.Ports.SerialPort object. However when writing the code to simply read from the object using Start-Job I'm getting an error. Here's my code so far:
$func = {
function CheckPort
{
param (
[parameter(Mandatory=$true,ValueFromPipeline=$true)]
[System.IO.Ports.SerialPort]$port
)
Write-Output $port.ReadLine()
}
}
$port = new-Object System.IO.Ports.SerialPort COM4, 9600, None, 8, one
$port.Open()
Start-Job -ScriptBlock {CheckPort $args[0]} -ArgumentList $port -Name “$computerName” -InitializationScript $func
When I run the code above, after I use Receive-Object to check the output of the subprocess, I see an error. It seems that instead of the $port object being passed as-is, it is first serialized then unserialized:
Error: "Cannot convert the "System.IO.Ports.SerialPort" value of type "Deserialized.System.IO.Ports.SerialPort" to type "System.IO.Ports.SerialPort"."
+ CategoryInfo : InvalidData: (:) [CheckPort], ParameterBindin...mationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,CheckPort
+ PSComputerName : localhost
Is there anyway to pass the an argument by reference using Start-Job? $port.ReadLine() is blocking and there isn't an alternative method that can just check whether there is something to read, and I need to occasionally write to the port, so I definitely need asynchronous execution here.
I get the same error if I try $using:
$port = new-Object System.IO.Ports.SerialPort COM4, 9600, None, 8, one
$port.Open()
Start-Job -ScriptBlock {
$myPort = $using:port
Write-Output $myPort.ReadLine()
}
These two methods don't serialize the objects. You need PS 7 for foreach-object -parallel, but you can download start-threadjob in PS 5.
Start-ThreadJob {
$myPort = $using:port
$myPort.ReadLine()
} | receive-job -wait -auto
foreach-object -parallel {
$myPort = $using:port
$myPort.ReadLine()
}
I have a function that is used to purge a message queue on a machine but I'm looking to adapt it and make it a little more robust. I'd like to be able to fire the command to a machine even if the current machine doesn't have MSMQ installed.
The local command works without issue but when the invoke-command is called, the check to see if the queue exists returns false (even though the queue does exist). Has anyone run into anything like this before? Any suggestions?
This is my function:
Function Purge-MessageQueue
{
<#
.Synopsis
Use hostname to purge message queue
.DESCRIPTION
Checks if MSMQ is locally installed otherwise fire the purge
command to the machine you are purging
.EXAMPLE
Purge-MessageQueue -targetMachine $env:computername -queueName 'test'
#>
Param
(
[Parameter(Mandatory=$true)]
[String]$targetMachine,
[Parameter(Mandatory=$true)]
[string]$queueName
)
Write-Verbose "Purging $queueName queue on: $targetMachine"
$queueName = "$targetMachine\$queueName"
$error.clear()
[void] [Reflection.Assembly]::LoadWithPartialName("System.Messaging")
try {[void][System.Messaging.MessageQueue]::Exists($queueName)}
catch
{
if ($_.exception.ToString() -like '*Message Queuing has not been installed on this computer.*')
{
#push command to machine
$RemoteSuccess = Invoke-Command -ComputerName $targetMachine -ScriptBlock { Param($queueName)
[void] [Reflection.Assembly]::LoadWithPartialName("System.Messaging")
If([System.Messaging.MessageQueue]::Exists($queueName))
{
$queue = new-object -TypeName System.Messaging.MessageQueue -ArgumentList $queueName
Try{$queue.Purge()}
Catch{$error}
}
} -ArgumentList $queueName
}
}
If(!$error)
{
If([System.Messaging.MessageQueue]::Exists($queueName))
{
$queue = new-object -TypeName System.Messaging.MessageQueue -ArgumentList $queueName
$queue.Purge()
}
}
If(!$Error -and !$RemoteSuccess)
{
Write-Host "$queueName queue on $targetMachine cleared"
}
Else
{
Write-Warning "Failed locating queue $queueName on $targetMachine"
}
}
NOTES:
In order to identify what exactly is going on, I used write-host on the exists statement and it returns false. The queue is not being found when I pass the scriptblock. It is executing on the other machine (tested writing a file which succeeded). When I run:
Write-Host "$([System.Messaging.MessageQueue]::Exists($queueName))`n$queueName"
$objqueue = new-object -TypeName System.Messaging.MessageQueue -ArgumentList $queueName
I get the false, the correct queue name, and the following error:
Exception calling "Purge" with "0" argument(s): "The queue does not
exist or you do not have sufficient permissions to perform the
operation."
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : MessageQueueException
+ PSComputerName : XXXXXX
Running the same command directly on the machine works without issue.
I also found someone else trying to do something similar on serverfault:
https://serverfault.com/questions/399178/how-to-retrieve-names-of-all-private-msmq-queues-efficiently
And when I try this:
Invoke-Command -ComputerName $targetMachine -ScriptBlock { Get-MsmqQueue }
I get the following result:
Cannot find specified machine.
+ CategoryInfo : ObjectNotFound: (:) [Get-MsmqQueue], MessageQueueException
+ FullyQualifiedErrorId : MachineNotFound,Microsoft.Msmq.PowerShell.Commands.GetMSMQQueueCommand
This following command does return the data, but it doesn't allow me to send a purge command:
Invoke-Command -ComputerName $targetMachine -ScriptBlock {Get-WmiObject -class Win32_PerfRawData_MSMQ_MSMQQueue}
I also tried to write the content to a script file and then call the file, which when run on the machine, works without issue but not when called via invoke-command:
$filewriter = #"
[Reflection.Assembly]::LoadWithPartialName("System.Messaging")
If([System.Messaging.MessageQueue]::Exists('$queueName'))
{
`$objqueue = new-object -TypeName System.Messaging.MessageQueue -ArgumentList $queueName
Try{`$objqueue.Purge()}
Catch{`$error}
}
"#
$session = New-PSSession -ComputerName $targetMachine
Invoke-Command -Session $session -ScriptBlock {Param($FileWriter)
$FileWriter | Out-File "C:\Temp\PurgeQueue.ps1"
} -ArgumentList $filewriter
$test = Invoke-Command -Session $session -scriptblock {Pushd "C:\Temp\"
.\PurgeQueue.ps1}
I have not found the cause for this, but I will summarize what I have found and my workaround
Summary:
When invoking msmq commands via invoke-command, only private queues appear and can be manipulated.
Workaround:
I've build a function to deal with purging and adding message to queues by creating scheduled tasks on the remote machine to call the script created by the command.
Function Push-MSMQRemoteCommand
{
Param(
[Parameter(Mandatory=$true)]
$targetMachine,
[Parameter(Mandatory=$true)]
$password,
[Parameter(Mandatory=$true)]
$queueName,
[Switch]$purge,
$user,
$message)
Begin
{
If(!$user){$user = "$env:USERDOMAIN\$env:USERNAME"}
If($purge -and $message.Length -ne 0){Write-Error "Choose to purge or add... not both" -ErrorAction Stop}
$queuepath = "$targetMachine\$queueName"
#build commands to push
If($purge)
{
$scriptblock = #"
[void] [Reflection.Assembly]::LoadWithPartialName("System.Messaging")
If ([System.Messaging.MessageQueue]::Exists('$queuePath')) {
`$queue = new-object -TypeName System.Messaging.MessageQueue -ArgumentList $queuePath
`$queue.Purge()
}
"#
}
ElseIf($message)
{
If($message.Length -eq 0){Write-Error "No message provided to add message" -ErrorAction Stop}
$scriptblock = #"
[void] [Reflection.Assembly]::LoadWithPartialName("System.Messaging")
`$queue = new-object System.Messaging.MessageQueue "$queuepath"
`$utf8 = new-object System.Text.UTF8Encoding
`$msgBytes = `$utf8.GetBytes('$message')
`$msgStream = new-object System.IO.MemoryStream
`$msgStream.Write(`$msgBytes, 0, `$msgBytes.Length)
`$msg = new-object System.Messaging.Message
`$msg.BodyStream = `$msgStream
`$msg.Label = "RemoteQueueManagerPowershell"
`$queue.Send(`$msg)
"#
}
#Push Commands
Invoke-Command -ComputerName $targetMachine -ScriptBlock {
Param($user,$password,$scriptblock)
$scriptblock | Out-file -FilePath "C:\temp\ManageQueue.ps1" -Force
$action = New-ScheduledTaskAction -execute 'powershell.exe' -Argument '-File "C:\temp\ManageQueue.ps1"'
#scheudling action to start 2 seconds from now
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date)+(New-TimeSpan -Seconds 2))
Register-ScheduledTask -TaskName RemoteQueueManager `
-Action $action `
-Trigger $trigger `
-User "$user"`
-Password $password
#Start-Sleep -Seconds 10
Unregister-ScheduledTask -TaskName RemoteQueueManager -Confirm:$false
Remove-Item -Path "C:\temp\ManageQueue.ps1" -Force
} -ArgumentList $user,$password,$scriptblock
}
}
From your analysis I have feeling that it is issue of rights.
Did you check the rights for your user?
If you are a normal user you have to do the following (not an Administrator) on the
destination computer/server/VM:
1) first create a group and add there users
net localgroup "Remote PowerShell Session Users" /add
net localgroup "Remote PowerShell Session Users" the-user /add
2) Invoke GUI
Set-PSSessionConfiguration microsoft.powershell -ShowSecurityDescriptorUI
3) Add Remote PowerShell Session Users group and grant it execute (invoke) rights
4) Restart the service:
Set-PSSessionConfiguration microsoft.powershell -ShowSecurityDescriptorUI
5) the user now should be able to run remote session
The original source is here.
I am getting the following error,
Errors caught - TRAPPED: System.Management.Automation.RemoteException with message TRAPPED: The term 'D:\ServiceNow\RDC-
Dev-All\agent\scripts\PowerShell\ImMigration_script.ps1' is not recognized as the name of a cmdlet, function, script fil
e, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and t
ry again.
The issue appears to be with the invoke-command
Invoke-Command -Session $Session -ScriptBlock $theCommand2
i have tired using -FilePath with no luck.
Also tired passing the command and param separately :
Invoke-Command -Session $Session -ScriptBlock $theCommand2 -argumentlist $leName
I am triggering the script using:
D:\ServiceNow\RDC-Dev-All\agent\scripts\PowerShell\invokLyncUAdd.ps1 -param1 'CN=lync2013testuser1,CN=Users,DC=test,DC=COMPANY,DC=com' -param2 AD\sys-LyncProATSC -param3 Z0185-XAP0007-S.test.COMPANY.com
###############################################################################
param( $param1, $param2, $param3 )
$ErrorActionPreference = "Stop"
# trap {
# write-output $("TRAPPED: " + $_.Exception.GetType().FullName);
# write-output $("TRAPPED: " + $_.Exception.Message);
# break
#}
$leName = $param1
$leName = ("'" + "$leName" + "'")
$thePath = 'D:\ServiceNow\RDC-Dev-All\agent\scripts\PowerShell'
$theCommand = $thePath+"\ImMigration_script.ps1 -param1 $leName"
$theCommand2 = [Scriptblock]::Create($theCommand)
# Write-Host "We use string $theCommand below"
$Account = $param2
$useP = Get-Content $thePath\'Information.txt'
$Prompt = convertto-securestring $useP -AsPlainText -Force
$leHost = $param3
try{
$Credential = new-object -typename System.Management.Automation.PSCredential
-argumentlist $Account, $Prompt
$Timeout = New-PSSessionOption -IdleTimeout 60000
$Session = New-PSSession -ComputerName $leHost -Credential $Credential -
Authentication Credssp -SessionOption $Timeout -ErrorAction Stop
Invoke-Command -Session $Session -ScriptBlock $theCommand2
}
catch
{
$exceptType = $("TRAPPED: " + $_.Exception.GetType().FullName);
$exceptMess = $("TRAPPED: " + $_.Exception.Message);
}
finally
{
if($exceptType) { "Errors caught - $exceptType with message $exceptMess " } }
Any help would be great, Thanks
The session is being executed on the remote computer, and I believe that's where PowerShell will expect the file to exist.
I would approach it by attempting to load the local script as a scriptblock so that it is in memory:
$thePath = 'D:\ServiceNow\RDC-Dev-All\agent\scripts\PowerShell'
$theCommand = $thePath+"\ImMigration_script.ps1"
$theCommand2 = [Scriptblock]::Create(Get-Content $theCommand)
Then, from your question:
Invoke-Command -Session $Session -ScriptBlock $theCommand2 -argumentlist $leName
Please let me know if this works.
If the file is in local, then
powershell.exe -noexit -file 'D:\ServiceNow\RDC-Dev-All\agent\scripts\PowerShell\invokLyncUAdd.ps1' -param1 'CN=lync2013testuser1,CN=Users,DC=test,DC=COMPANY,DC=com' -param2 'AD\sys-LyncProATSC' -param3 'Z0185-XAP0007-S.test.COMPANY.com'
If It is in the remote system, then make sure you are mentioning the remote path properly in the invoke-command.
When using the below code to create printer on Windows 2008 servers to create the printers
`function CreatePrinterPort {
$server = $args[0]
$port = ([WMICLASS]“\\$server\ROOT\cimv2:Win32_TCPIPPrinterPort").createInstance()
$port.Name= $args[1]
$port.SNMPEnabled=$false
$port.Protocol=2
$port.HostAddress= $args[2]
$port.Put()
}
function CreatePrinter {
$server = $args[0]
$print = ([WMICLASS]“\\$server\ROOT\cimv2:Win32_Printer”).createInstance()
$print.Drivername = $args[1]
$print.PortName = $args[2]
$print.Shared = $true
$print.Published = $false
$print.Sharename = $args[3]
$print.Location = $args[4]
$print.Comment = $args[5]
$print.DeviceID=$args[2]
$print.Put()
}
$printers = Import-Csv “C:\printers.csv”
foreach ($printer in $printers) {
CreatePrinterPort $printer.Printserver $printer.Portname $printer.IPAddress
CreatePrinter $printer.Printserver $printer.Driver $printer.Portname $printer.Sharename $printer.Location $printer.Comment $printer.Printername
}'
I am getting the following error. The port creation function is working.
"IsSingleton : False
Exception calling "Put" with "0" argument(s): "Generic failure "
At line:23 char:1
+ $print.Put()
+ ~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException"
I have importing all the details from a CSV file and it contains all the information.
Any suggestions?
I'm pretty sure you're both doing this the hard way, and didn't read the MSDN page for the Win32_Printer class. It says in the remarks that you have to enable the SeDriverUpdate privilege before you can issue the Put method, so I have a feeling that's where you're getting your error from.
Next, use the Set-WMIInstance cmdlet, or the newer 'New-CIMInstance` if you can. Calling the classes directly is possible I'm sure, but if the server is local it won't enable the privileges that you need to create a printer.
Lastly, you could make your function better if you made it an advanced function, and allowed piped data. Check this out:
function CreatePrinter {
[cmdletbinding()]
Param(
[Parameter(ValueFromPipelineByPropertyName=$true)]
[Alias('Printserver')]
$Server,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[Alias('Driver')]
$Drivername,
[Parameter(ValueFromPipelineByPropertyName=$true)]
$PortName,
[Parameter(ValueFromPipelineByPropertyName=$true)]
$Sharename,
[Parameter(ValueFromPipelineByPropertyName=$true)]
$Location,
[Parameter(ValueFromPipelineByPropertyName=$true)]
$Comment,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[Alias('IPAddress')]
$HostAddress,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[Alias('PrinterName')]
$Name
)
PROCESS{
$PortArgs = #{
Name = $PortName
SNMPEnabled = $false
Protocol = 2
HostAddress = $HostAddress
}
Set-WmiInstance -Class Win32_TCPIPPrinterPort -Arguments $PortArgs -ComputerName $Server -PutType UpdateOrCreate -EnableAllPrivileges
$PrinterArgs = #{
Drivername = $Drivername
PortName = $PortName
Shared = $true
Published = $false
Sharename = $Sharename
Location = $Location
Comment = $Comment
DeviceID = $PortName
Name = $Name
}
Set-WmiInstance -Class Win32_Printer -Arguments $CmdArgs -ComputerName $Server -PutType UpdateOrCreate -EnableAllPrivileges
}
}
That creates the port, and then the printer. I suppose you could split it into two functions, but do you really see needing one without the other? Plus you can pipe your CSV data directly into it like this:
Import-CSV "C:\printers.csv" | CreatePrinter
That's it, that will create ports and printers for all records in the CSV. Plus I used the UpdateOrCreate enum, so if something isn't right you could correct the CSV and just run it again to refresh the settings to the correct settings without having to worry about deleting old copies and making new copies of things.
How is it possible to use the parameters collected in a hash table for use with ArgumentList on Invoke-Command?
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
Structure = 'yyyy-MM-dd'
}
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock ${Function:Copy-FilesHC} -ArgumentList #CopyParams
Whatever I try, it's always complaining about the 'Source':
Cannot validate argument on parameter 'Source'. The "Test-Path $_" validation script for the argument with
value "System.Collections.Hashtable" did not return true. Determine why the validation script failed
This blog talks about a similar problem, but I can't get it to work.
The same is true for a simple Copy-Item within Invoke-Command, example:
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock {Copy-Item} -ArgumentList #CopyParams
Invoke-Command : Missing an argument for parameter 'ArgumentList'. Specify a parameter of type 'System.Obj
ect[]' and try again.
At line:11 char:89
+ ... ck {Copy-Item} -ArgumentList #CopyParams
Thank you for your help.
One-liner, to convert a remote script to accept named parameters from a hash.
Given a scriptblock which you wish to call like this:
$Options = #{
Parameter1 = "foo"
Parameter2 = "bar"
}
Invoke-Command -ComputerName REMOTESERVER -ArgumentList $Options -ScriptBlock {
param(
$Parameter1,
$Parameter2
)
#Script goes here, this is just a sample
"ComputerName: $ENV:COMPUTERNAME"
"Parameter1: $Parameter1"
"Parameter2: $Parameter2"
}
You can convert it like so
Invoke-Command -Computername REMOTESERVER -ArgumentList $Options -ScriptBlock {param($Options)&{
param(
$Parameter1,
$Parameter2
)
#Script goes here, this is just a sample
"ComputerName: $ENV:COMPUTERNAME"
"Parameter1: $Parameter1"
"Parameter2: $Parameter2"
} #Options}
What's going on? Essentially we've wrapped the original script block like so:
{param($Options)& <# Original script block (including {} braces)#> #options }
This makes the original script block an anonymous function, and creates the outer script block which has a parameter $Options, which does nothing but call the inner script block, passing #options to splat the hash.
Here's one way to approach passing named parameters:
function Copy-FilesHC
{
param ($Source,$Destination,$Structure)
"Source is $Source"
"Desintation is $Destination"
"Structure is $Structure"
}
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
Destination = "'E:\DEPARTMENTS\CBR\SHARE\Target 2'" #Nested quotes required due to embedded space in value.
Structure = 'yyyy-MM-dd'
}
$SB = [scriptblock]::Create(".{${Function:Copy-FilesHC}} $(&{$args}#CopyParams)")
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock $SB
Basically, you create a new script block from your invoked script, with the parameters splatted to that from the hash table. Everything is already in the script block with the values expanded, so there's no argument list to pass.
I found a workaround, but you have to make sure that your Advanced function which is located in your module file is loaded up front in the local session. So it can be used in the remote session. I wrote a small helper function for this.
Function Add-FunctionHC {
[CmdletBinding(SupportsShouldProcess=$True)]
Param(
[String]$Name
)
Process {
Try {
$Module = (Get-Command $Name -EA Stop).ModuleName
}
Catch {
Write-Error "Add-FunctionHC: Function '$Name' doesn't exist in any module"
$Global:Error.RemoveAt('1')
Break
}
if (-not (Get-Module -Name $Module)) {
Import-Module -Name $Module
}
}
}
# Load funtion for remoting
Add-FunctionHC -Name 'Copy-FilesHC'
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target\De file.txt'
Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
}
$RemoteFunctions = "function Copy-FilesHC {${function:Copy-FilesHC}}" #';' seperated to add more
Invoke-Command -ArgumentList $RemoteFunctions -ComputerName 'SERVER' -Credential $Cred -ScriptBlock {
Param (
$RemoteFunctions
)
. ([ScriptBlock]::Create($RemoteFunctions))
$CopyParams = $using:CopyParams
Copy-FilesHC #CopyParams
}
The big advantage is that you don't need to copy your complete function in the script and it can stay in the module. So when you change something in the module to the function it will also be available in the remote session, without the need to update your script.
I recently experienced a similar problem and solved it by building the hash (or rebuilding the hash) inside the invoke by leveraging the $using variable scope (more on that here)
it looks something like this:
$Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
$Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
$Structure = 'yyyy-MM-dd'
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock {
$CopyParms= #{
'Source'=$Using:Source
'Destination'=$Using:Destination
'Structure'=$Using:Structure
}
Function:Copy-FilesHC #CopyParms
}
This is what works for me:
$hash = #{
PARAM1="meaning of life"
PARAM2=42
PARAM3=$true
}
$params = foreach($x in $hash.GetEnumerator()) {"$($x.Name)=""$($x.Value)"""}
I know this is late, but I ran into the same problem and found a solution that worked for me. Assigning it to a variable within the scriptblock and then using that variable to splat didn't show any problems.
Here's an example:
$param=#{"parameter","value"}
invoke-command -asjob -session $session -ScriptBlock {$a=$args[0];cmdlet #a } -ArgumentList $param