Can't assign value to a variable inside of Invoke-Command - powershell

It seems to be strange but I can't assign a value to variable inside of Invoke-Command. Here is the code below but when print out $targetComputerPath it's simply empty. What's wrong?
foreach ($item in $computersPath){
$computername = $item.Name
$username = $item.UserID
Write-Host computer $computername and user $username
if (Test-Connection -ComputerName $computername -Count 1 -ErrorAction SilentlyContinue)
{
if ($((Get-Service WinRM -ComputerName $computername).Status) -eq "stopped")
{
(Get-Service WinRM -ComputerName $computername).Start()
}
Invoke-Command -ComputerName $computername -ScriptBlock {
if ($((Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion").ReleaseId) -eq "1903" )
{
$targetComputerPath = "\\"+$computername+"\c$\Users\"+$username+"\Desktop\"
write-host "1903"
}
else
{
$targetComputerPath = "\\"+$computername+"\c$\Users\"+$username+"\Desktop\"
write-host "something else"
}
}
}
write-host $targetComputerPath
}

The point of WinRM is that you take a script block, and execute it on a different machine.
None of the variables you define in the host script will be available on the remote machine.
This becomes more apparent when you separate the "task", a.k.a the script block, from the Invoke-Command, like this:
$task = {
$version = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
if ($version.ReleaseId -eq "1903") {
# note that `$username` cannot be available here, it's never been defined!
return "\\$env:COMPUTERNAME\c$\Users\$username\Desktop"
} else {
return "\\$env:COMPUTERNAME\c$\Users\$username\Desktop"
}
}
foreach ($item in $computersPath) {
$computername = $item.Name
$username = $item.UserID
Write-Host computer $computername and user $username
if (Test-Connection -ComputerName $computername -Count 1 -ErrorAction SilentlyContinue) {
$winrm = Get-Service WinRM -ComputerName $computername
if ($winrm.Status -eq "stopped") { $winrm.Start() }
$targetComputerPath = Invoke-Command -ComputerName $computername -ScriptBlock $task
Write-Host "The machine returned: $targetComputerPath"
}
}
As you can see, you can return values from the script block and they will be available as the return value of Invoke-Command.
If you want to pass arguments to your script block, this thread talks about that: How do I pass named parameters with Invoke-Command?

Related

PowerShell Script Issues with Variable Values

I am trying to write this script to restart computers only if they are Offline. The script for getting user infomration works but I cannot get the variable values for the restart portion at the bottom of the script. Does anyone have a suggestion? I am somewhat new to Powershell, but writing code. Example of my script follows:
Function Get-LoggedOnUser
{
Param
(
$ComputerName = $env:COMPUTERNAME,
$Credential
)
Function Test-RemoteRegistry
{
Param
(
[Parameter(Mandatory = $False)]
[switch]$Enable
,
[Parameter(Mandatory = $False)]
[switch]$Disable
,
[Parameter(ValueFromPipeline=$True)]
[String[]]$ComputerName = $env:COMPUTERNAME
)
Begin
{
$PipelineInput = (-not $PSBOUNDPARAMETERS.ContainsKey("ComputerName")) -and (-not $ComputerName)
Function Test ($Computer)
{
Try
{
[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $Computer) | Out-Null
#20ms faster than Get-Service per computer! Not sure how to handle/check things like the firewall though...
#If we hit here without error Remote Reg is enabled.
If ($Disable)
{
Try
{
Get-Service -Name RemoteRegistry -ComputerName $Computer | Set-Service -Status Stopped -ErrorAction Stop
Return $False
#If we hit here without error Remote Reg is now disabled.
}
Catch
{
Return $True
#If we hit here, we couldn't stop remote registry.
}
}
Else
{
Return $True
}
}
Catch
{
If ($Enable)
{
Try
{
Get-Service -Name RemoteRegistry -ComputerName $Computer | Set-Service -Status Running -ErrorAction Stop
Return $True
#If we hit here without error Remote Reg is now enabled.
}
Catch
{
Return $False
#If we hit here, we couldn't start remote registry.
}
}
Else
{
Return $False
#If we hit here remote registry is disabled.
}
}
}
}
Process
{
If ($PipelineInput)
{
Test $_
}
Else
{
$ComputerName | ForEach-Object {
Test $_
}
}
}
}
Foreach ($Computer in $Computername)
{
$Online = $False
$User = $False
$Locked = $False
If (Test-Connection $Computer -Count 2 -Quiet)
{
$Online = $True
If ($Credential)
{
$User = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $Computer -Credential $Credential | Select-Object -ExpandProperty UserName -ErrorAction Stop
}
Else
{
$User = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $Computer | Select-Object -ExpandProperty UserName -ErrorAction Stop
}
If (Test-RemoteRegistry -Enable -ComputerName $Computer)
{
If ((Get-Process logonui -ComputerName $Computer -ErrorAction SilentlyContinue) -and ($user))
{
$Locked = $True
}
}
}
$Output = New-Object PSObject
$Output | Add-Member noteproperty ComputerName $Computer
$Output | Add-Member noteproperty Online $Online
$Output | Add-Member noteproperty Username $User
$Output | Add-Member noteproperty Locked $Locked
$Output
}
}
Get-LoggedOnUser
If (($Online) -eq $False)
{Shutdown /r t 0 /m \\$Computername}
ELSE
{Write-host 'HELLO $Online $Computername'}
I just want this for a single user as I am using PDQ Inventory to roll out the script. The variables at the end of the script are $null?
Variables defined in a child scope - in which functions run by default - are never seen by the calling scope. See the conceptual about_Scopes help topic
It's best for functions to communicate values to the caller via their output ("return value"), which you're function is already doing: it outputs objects whose properties contain the values of interest.
Therefore:
Get-LoggedOnUser |
ForEach-Object { # Loop over all output objects
# Refer to the object at hand via the automatic $_ variable.
# Note the use of "..." (expandable strings) so as to support
# expansion (string interpolation).
if (-not $_.Online) { Shutdown /r t 0 /m "\\$($_.ComputerName)" }
else { "HELLO $($_.Online) $($_.ComputerName)" }
}

Powershell Error Handling not working in Invoke-command block

I want to print error message when service is not found or machine is offline
$csv = Import-Csv "C:\Users\user\Desktop\computers.csv" -delimiter ","
foreach ($cs in $csv){
$computers = $cs.DNSHostname
foreach ($computer in $computers){
Try{
Invoke-Command -ComputerName $computer -ScriptBlock {
$ProcessCheck = Get-Process -Name agentid-service -ErrorAction Stop
if ($null -eq $ProcessCheck) {
Write-output "agentid-service IS not running $env:computername"
}
else {
Write-output "agentid-service IS running $env:computername"
}
}
}
catch{
Write-Warning $Error[0]
}
}
}
Instead, when service name is not found, i'm getting error:
Cannot find a process with the name "agentid-service". Verify the process name and call the cmdlet again.
And if connection to computer is not possible then getting:
[vm.domain.local] Connecting to remote server vm.domain.local failed with the following error message : The WinRM client cannot process the request because the server name cannot be resolved.
And script continue execution (as it should).
How can i get "Catch" block to be executed ?
You can do this in one foreach loop.
Try
$computers = (Import-Csv "C:\Users\user\Desktop\computers.csv").DNSHostname
foreach ($computer in $computers) {
if (!(Test-Connection -ComputerName $computer -Count 1 -Quiet)) {
Write-Warning "Computer $computer cannot be reached"
continue # skip this one and proceed with the next computer
}
try{
Invoke-Command -ComputerName $computer -ScriptBlock {
$ProcessCheck = Get-Process -Name 'agentid-service' -ErrorAction Stop
if (!$ProcessCheck.Responding) {
"agentid-service is NOT not running on computer '$env:computername'"
}
else {
"agentid-service is running on computer '$env:computername'"
}
}
}
catch{
Write-Warning $_.Exception.Message
}
}
Thanks to #Theo's answer, managed to fix it:
$csv = Import-Csv "C:\Users\user\Desktop\computers.csv" -delimiter ","
foreach ($cs in $csv){
$error.Clear()
$computers = $cs.DNSHostname
foreach ($computer in $computers){
Try{
Invoke-Command -ComputerName $computer -ScriptBlock {
$ProcessCheck = Get-Process -Name agentid-service -ErrorAction Stop
if ($null -ne $ProcessCheck) {
Write-output "agentid-service IS running $env:computername"
}
} -ErrorAction Stop
}
catch{
if ($null -eq $ProcessCheck){
Write-warning "agentid-service IS not running $env:computername"
}
Write-Warning $Error[0]
}
}
}

How do I remote into multiple remote computers via PS to discover one specific app and determine the version number on each remote device? No output

FYI: I'm very new to PS and I'm using this as a learning opportunity. Again, I'm trying to find a
specific application on a list of multiple remote devices and determine the version number of the
application on their corresponding host system. I attempted this via a registry query (found this to
be challenging) and then I used Get-WMIObject. As of now, I'm working with this as my script. It's
not producing any output; instead, it returns to the command prompt with no errors or messages.
Script to find specific application and version in multiple remote devices:
$Servers = Get-Content -Path C:\\files\Serverlist.txt
$CIMSession = New-CIMSession -ComputerName $Servers Get-Credentials
$Vendor = "App Name"
foreach($Serv in $Servers) {
If(Test-Connection -ComputerName $Serv -Count 1 -Quiet) {
$Status = Get-Ciminstance Win32_Product -Computername $Serv | Where-object {$_.Version -contains
$Vendor}
if($Status) {
Out-file -Filepath C:\\files\AppVerResults.txt
}
}
}
I also tried adjusting the following section of the script as shown below but it presented me with the error "Get-CimInstance : Access is denied." Is this error message due to group policy or so? I am able to remote into the device corresponding to the message via RDP.
if($Status) {
$Servers + " - "
$Status | Out-file -Filepath C:\\files\AppVerResults.txt
}
}
}
Should I go about it via invoke-command or registry query? I'm slowly picking things up so I'll continue my research but I was hoping to get some advice in the meantime.
I still believe searching the registry is the easier way to go unless you have the specific file path for the .exe.
Use this function to find software on a remote, or local PC. Theres a filter option by specifying -SoftwareName (to look for).
Find-Software -ComputerName Remote_ComputerName -SoftwareName 'SQL'
Also accepts pipeline input, as well as multiple computer names to query for.
Find-Software -ComputerName ComputerOne, ComputerTwo, ComputerThree -SoftwareName 'SQL'
'ComputerOne','ComputerTwo' | Find-Software -SoftwareName 'SQL'
Exporting is also allowed by piping to an Export-* cmdlet.
Heres the code:
Function Find-Software {
[cmdletBinding()]
Param(
[Parameter(Mandatory=$false,
ValueFromPipeLine=$true,
ValueFromPipeLineByPropertyName=$true)]
[Alias('cn','name')]
[string[]]$ComputerName = $env:COMPUTERNAME,
[Parameter(Mandatory=$false)]
[String]$SoftwareName
)
Begin{
#Get Computer Names to check software version for
$Server_List = Get-Content -Path "C:\files\Serverlist.txt"
#Get Credentials for Script Scope once.
$Credentials = Get-Credential
}
Process{
if($PSBoundParameters.ContainsKey('SoftwareName')){
foreach($Computer in $ComputerName){
Try{
$PSSession = New-PSSession -ComputerName $Computer -Credential $Credentials -EnableNetworkAccess -ErrorAction Stop
$Software_List = Invoke-Command -ScriptBlock {
Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" } -Session $PSSession
$Software_List = $Software_List | Where-Object -FilterScript {$_.DisplayName -match $SoftwareName} | Sort-Object -Property DisplayName
foreach($Software in $Software_List){
if($Software){
[PSCustomObject]#{
"Computer Name" = $Computer
"Software Name" = $Software.DisplayName
" Version " = $Software.DisplayVersion
}
} else {
[PSCustomObject]#{
"Computer Name" = $Computer
"Software Name" = "Not found"
" Version " = $null
}
}
}
} Catch {
"Unable to connect to PC: $Computer"
"Error: $($Error[0].Message.Split('.')[1].Trim())"
}
}
} else {
foreach($Computer in $ComputerName){
Try{
$PSSession = New-PSSession -ComputerName $Computer -Credential $Credentials -EnableNetworkAccess -ErrorAction Stop
$Software_List = Invoke-Command -ScriptBlock {
Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" } -Session $PSSession
$Software_List = $Software_List | Sort-Object -Property DisplayName
foreach($Software in $Software_List){
[PSCustomObject]#{
"Computer Name" = $Computer
"Software Name" = $Software.DisplayName
" Version " = $Software.DisplayVersion
}
}
} Catch {
"Unable to connect to PC: $Computer"
"Error: $($Error[0].Message.Split('.')[1].Trim())"
}
}
} #end ELSE statement
} #end PROCESS block
End {
if(Get-PSSession){
Get-PSSession | Remove-PSSession
}
} #end END block - Perform Session Clean Up
} #end FUNCTION
Simply modify it to fit your needs :)

Unable to pass variable to Invoke-command

This is my first time using the workflow, could anyone explain me what is wrong with the code ?
Powershell version is 5.1
$Script = {
return(Get-Service WINRM).Status
}
workflow pushupdate{
##Select OUs
$OUs=
"OU=Workstations,DC=contoso,DC=com",
"OU=Notebooks,DC=contoso,DC=com"
foreach -parallel ($computer in ($Ous | foreach { Get-ADComputer -Filter {enabled -eq $true} -SearchBase $_} | Select Name)) {
if ((Test-Connection $computer.name -Quiet) -eq "True") {
Write-Output "Running update on:" $computer.name
InlineScript {
Invoke-Command -ComputerName $computer.name -Script $Script -Verbose
}
}
else{
Write-Output $computer.name "unreachable!"
}
}
}
pushupdate
I keep getting the error:
Invoke-Command : Cannot validate argument on parameter 'ScriptBlock'. The argument is null. Provide a valid value for
the argument, and then try running the command again.
At pushupdate:245 char:245
Variables defined outside the InlineScript block are unknown to the Invoke-Command cmdlet unless you use them as $using:<varname>.
It seems however that you cannot do that with a variable which is actually a scriptblock. That needs to be defined inside the InlineScript itself:
workflow pushupdate{
# Select OUs
$OUs = "OU=Workstations,DC=contoso,DC=com", "OU=Notebooks,DC=contoso,DC=com"
# get a string array of computerNames
$computers = ( $Ous | ForEach-Object { Get-ADComputer -Filter "Enabled -eq 'True'" -SearchBase $_ } ).Name
foreach -parallel ($computer in $computers) {
if (Test-Connection -ComputerName $computer -Quiet -Count 1) {
Write-Output "Running update on:" $computer
InlineScript {
# define the scriptblock here
$script = {(Get-Service WINRM).Status}
Invoke-Command -ComputerName $using:computer -ScriptBlock $Script -Verbose
}
}
else{
Write-Output "$computer unreachable!"
}
}
}
pushupdate

Powershell - Passing multiple arraylists to Invoke-Command block

I am trying to write a powershell script that will tell me if a computer in my network is on or off, and if it is on, if there is anyone logged in. Currently I have:
# Create some empty arraylists
$availablecomputers = New-Object System.Collections.ArrayList
$unavailablecomputers = New-Object System.Collections.ArrayList
$usersloggedon = New-Object System.Collections.ArrayList
#Check connectivity for each machine via Test-WSMan
foreach ($computer in $restartcomputerlist)
{
try
{
Test-WSMan -ComputerName $computer -ErrorAction Stop |out-null
Invoke-Command `
-ComputerName $computer `
-ScriptBlock `
{
if
((Get-WmiObject win32_computersystem).username -like "AD\*")
{
$args[0] += $computer
}
else
{
$args[1] += $computer
}
} `
-ArgumentList (,$usersloggedon), (,$availablecomputers)
}
catch
{
$unavailablecomputers += $computer
}
}
So far, if the computer is not on, it works correctly. However, if it is on, $computer won't be added to $usersloggedon or $availablecomputers. Any help would be appreciated.
#Mathias is correct; variables you pass into the scriptblock are passed by value (serialized), not by reference, so you can't update them and change the original object.
To return values from the scriptblock, use Write-Object or just simply "use" the value (Write-Object $env:COMPUTERNAME is the same as just doing $env:COMPUTERNAME).
For your specific situation, consider returning an object that contains the information you want:
$computers = #()
#Check connectivity for each machine via Test-WSMan
foreach ($computer in $restartcomputerlist)
{
try
{
Test-WSMan -ComputerName $computer -ErrorAction Stop |out-null
$computers += Invoke-Command -ComputerName $computer -ScriptBlock {
$props = #{
Name = $env:COMPUTERNAME
Available = $true
UsersLoggedOn = ((Get-WmiObject win32_computersystem).username -like "AD\*")
}
New-Object PSObject -Property $props
}
}
catch
{
$props = #{
Name = $computer
Available = $false
UsersLoggedOn = $false
}
$computers += New-Object PSObject -Property $props
}
}
$computers # You can now use this with Select-Object, Sort-Object, Format-* etc.