I am trying to test if two PC's are connected by using the following script
$array ='PC1','PC2'
for ($i=0; $i -lt $array.length; $i++) {
Start-Job –Name TestConnection$i –Scriptblock {
if(test-connection $array[$i] -count 1 -quiet){
write-host Success
}
else { write-host No connection
}
}
}
When I try to do Receive-Job for either one I get "Cannot index into a null array".
What am I doing wrong?
You need to pass in the PC name as an argument, as the array does not exist in the context of the script block, like this:
$array ='PC1','PC2'
for ($i=0; $i -lt $array.Length; $i++) {
Start-Job –Name TestConnection –Scriptblock {
param($pcName)
if(Test-Connection $pcName -Count 1 -Quiet) {
Write-Host Success
} else {
Write-Host No connection
}
} -ArgumentList $array[$i]
}
You have to pass $i (and any other variables) via -ArgumentList through the Start-Job Cmdlet since your script block is running in an entirely different powershell host and doesn't have access to anything inside the shell that started the job.
Even though your script block exists inside the original code, Powershell does not expand any variables in it until it's executing the code in the other host. You can define param() at the beginning of your script block to use the variable you pass via -ArgumentList
Related
Occasionally I forget to log off from a server or am disconnected through an error and I don't remember the name of the server. And my domain account starts getting periodically locked out, so I have to access logs on DC to find out which server(s) keep locking my account and log off from it/them. So I wanted to write to script in powershell that would log me off from all servers in a domain (with the exception of the server where I run the script on of course) without me needing to search which to log off from. This is what I have:
$ErrorActionPreference = "Silentlycontinue"
$Servers = (Get-ADComputer -Filter *).Name
$ScriptBlock = {
$Sessions = quser | ?{$_ -match $env:USERNAME}
if (($Sessions).Count -ge 1)
{
$SessionIDs = ($Sessions -split ' +')[2]
Write-Host "Found $(($SessionIDs).Count) user login(s) on $Server."
$SessionIDs | ForEach-Object
{
Write-Host "Logging off session [$($_)]..."
logoff $_
}
}
}
foreach ($Server in $Servers)
{
if ($Server -isnot $env:COMPUTERNAME)
{
Invoke-Command -ComputerName $Server -ScriptBlock {$ScriptBlock}
}
}
But when I launch the script, nothing happens. The script doesn't return any errors but doesn't log me off from any server, nor does it write any of the messages from Write-Host cmdlet, obviously. I noticed the $SessionIDs variable definition only returns ID of the first session. Usually this shouldn't be a problem, since it's unlikely I will have more than one session on a server, but I'd like to have this insurance. Can anyone tell me what's wrong in the script?
I notice a few things...
"First, I don't think quser | Where-Object {$_ -match $env:USERNAME} will ever return anything. The output of quser will not contain the hostname."
Try this for getting logon sessions:
$Sessions = (query user /server:$Env:ComputerName) -split "\n" -replace '\s\s+', ';' |
ConvertFrom-Csv -Delimiter ';'
Next, when you reference the $Server variable on the remote machine in your script block, it is out of scope. You would need to use $Using:Server in the script block.
Lastly, the -isnot operator doesn't compare value, it compares type. So in your last foreach, the if statement evaluates to "if type string is not type string" and will not run. Try -ne or -notlike instead.
Working with objects is much easier if you can just parse the output of QUser.exe. Given your scenario, here's my take on it:
$servers = (Get-ADComputer -Filter '*').Name.Where{$_ -ne $env:COMPUTERNAME}
foreach ($server in $servers)
{
if (-not ($quser = ((QUser.exe /server:$server) -replace '\s{20,39}',',,' -replace '\s{2,}',',' 2>&1) | Where-Object -FilterScript { $_ -match $env:USERNAME })) {
Continue
}
Write-Verbose -Message "$($quser.Count) session(s) found on $server." -Verbose
($quser.Trim() | ConvertFrom-Csv -Header 'USERNAME','SESSIONNAME','ID','STATE','IDLE TIME','LOGON TIME').foreach{
Write-Verbose -Message "Logging user [$($_.UserName)] off." -Verbose
LogOff.exe $_.ID /server:$server
}
}
Filtering should always happen before hand meaning, filter out your computer name on your first call to Get-ADComputer. Since you're using QUser.exe and LogOff.exe to begin with, I'd recommend the use of it all the way through since LogOff accepts an ID value that QUser outputs.
Next, placing the call to quser inside your if statement does two things in this case.
Filters for all users matching $ENV:UserName
Returns $true if anything is found, and $false if not found.
So, switching the results using -not will turn $false into $true allowing the execution of the code block which will just continue to the next server.
This in turn doesn't bother with the rest of the code and continues onto the next computer if no matching names were found.
The use of $quser inside the if statement is so you can save the results to it if more than one name is found; (..) allows this as it turns the variable assignment into an expression having the output pass through onto the pipeline where it is either empty, or not.
Finally, referencing the $quser variable we can convert the strings into objects piping to ConvertFrom-Csv. Only step left to do is iterate through each row and passing it over to LogOff to perform the actual logoff.
If you've noticed, the headers are manually-specified because it is filtered out by the Where-Object cmdlet. This is a better approach seeing as there could be "more than one" RDP Session, now you're just left with those sessions matching the name which can be saved to $quser, so no extra filtering is needed down the line.
So I modified the script this way and it works, sort of. It logs off account from servers, which is the main goal. There are still some glitches, like the message it sends from the first Write-Host doesn't give server's name, the message from second one gives a different value than it should (it gives [1] value after -split instead of [2] for some reason; but those are not really that important things, even though I will try to make at least the first message right) and $SessionIDs still gives only the first value, but usually you shouldn't have more than one RDP session per server. I've seen more sessions of one user, but that is very rare. But I'd also like to fix this if possible. Nevertheless, the script basically does the most important thing. But if someone has a suggestion how to fix the glitches I mentioned I would be grateful.
$ErrorActionPreference = "Silentlycontinue"
$Servers = (Get-ADComputer -Filter *).Name
$ScriptBlock = {
$Sessions = quser | ?{$_ -match $env:USERNAME}
if (($Sessions).Count -ge 1)
{
$SessionIDs = , ($Sessions -split ' +')[2]
Write-Host "Found $(($SessionIDs).Count) user login(s) on $Server."
Foreach ($SessionID in $SessionIDs)
{
Write-Host "Logging off session $SessionID..."
logoff $SessionID
}
}
}
foreach ($Server in $Servers)
{
if ($Server -ne $env:COMPUTERNAME)
{
Invoke-Command -ComputerName $Server -ScriptBlock $ScriptBlock
}
}
So I have an SSM association that would run a AWS-RunRemoteScript document to call a Powershell script that would run commands for domain join.
Its successfully doing the domain join but after I try to do a exit 3010, which return a 255 exit status and my script fails.
My AWS-RunRemoteScript document accepts some params. Below is the snippet in my powershell script.
try {
$ansible_token = (Get-SECSecretValue -SecretId $ansibletoken_secretname -Select SecretString | ConvertFrom-Json | Select -ExpandProperty token)
$instanceid = Invoke-RestMethod -Uri "http://169.254.169.254/latest/meta-data/instance-id"
$limit = $instanceid + ",127.0.0.1"
If((gwmi win32_computersystem).partofdomain -ne $True) {
Write-Host "Instance not joined to domain. Running domain join ansible template.."
$domain_join_success = Run-AnsibleJob $domain_join_template $extra_vars_param_domain_join $limit $ansible_token $tower_url
if ($domain_join_success -eq $True)
{
Write-Host "Shutting Down Instance $($instanceid).." --> This is printed and then I get a 255.
exit 3010
}
}
If((gwmi win32_computersystem).partofdomain -eq $True)
{
Write-Output "The instance is domain joined, running node setup ansible template.."
$node_initialize_success = Run-AnsibleJob $initialize_node_template $extra_vars_param_initialize_node $limit $ansible_token $tower_url
If($node_initialize_success -eq $True)
{
exit 0
}
}
}
catch {
Write-Host "--------------Error Occured----------------"
Write-Host $_
}
exit 1
Run-AnsibleJob is a function which calls an ansible job and returns tru if it is successful, and false if it fails.
What am doing wrong?
My current script checks if a specific folder on some clients exists.
I'd like to check if the client is online or offline before checking if the folder exists.
My current script looks like this:
$CDS = Get-content C:\Users\XY\Desktop\Clientliste.txt
Foreach($c in $CDS) {
IF (Test-Connection -BufferSize 32 -Count 1 -ComputerName $c -Quiet) {
Foreach ($c in $CDS) {
$Test = Test-Path -path "\\$c\c$\apps\perl"
Start-Sleep -s 0.25
If ($Test -eq $True) {
Write-Host "Path exists on $c."
}
Else {
Write-Host "Path NOT exist on $c."
}
}
}
Else {
Write-Host "The remote computer " $c " is Offline"
}
}
I don't know how to link the foreach loops so that they work together.
Because when I run my script now, it goes after the first if request in the 2nd foreach loop and it does leave it first, when it finishes the 2nd foreach loop.
I don't want that. I want that if the client is online, it checks if the paths exists and then goes to the next client and checks again if it is online and then...
Maybe you can help me :)
Why do another identical loop over the items from the text file is you have already tested the machine is reachable?
Just remove that second loop and do:
$CDS = Get-Content -Path 'C:\Users\XY\Desktop\Clientliste.txt'
foreach($computer in $CDS) {
if (Test-Connection -BufferSize 32 -Count 1 -ComputerName $computer -Quiet) {
if (Test-Path -Path "\\$computer\C$\apps\perl" -PathType Container) {
Write-Host "Path exists on computer '$computer'."
}
else {
Write-Host "Path NOT exist on computer '$computer'."
}
}
else {
Write-Host "The remote computer '$computer' is Offline"
}
}
Let's say I have a script:
write-host "Message.Status: Test Message Status";
I managed to run it in a separate process by doing:
powershell.exe -Command
{ write-host "Message.Status: Test Message Status"; }
The problem is I want to pass parameters to the script so that I can achieve something like this:
write-host "I am in main process"
powershell.exe -Command -ArgumentList "I","am","here"
{
write-host "I am in another process"
write-host "Message.Status: $($one) $($two) $($three)";
}
However -ArgumentList doesn't work here
I get:
powershell.exe : -ArgumentList : The term '-ArgumentList' is not recognized as the name of a cmdlet, function, script file, or operable
I need to run some part of PowerShell script file in a different process and I cannot use another file due to the fact that PowerShell script is uploaded to external system.
The -Command parameter is expecting a scriptblock in which you can define your parameters using a Param() block. Then use the -args parameter to pass in the arguments. Your only mistake was to put the -args after -command before you defined the scriptblock.
So this is how it works:
write-host "I am in main process $($pid)"
powershell.exe -Command {
Param(
$one,
$two,
$three
)
write-host "I am in process $($pid)"
write-host "Message.Status: $($one) $($two) $($three)";
} -args "I", "am", "here" | Out-Null
Output:
I am in main process 17900
I am in process 10284
Message.Status: I am here
You can use the -File parameter and follow it by the path to script. Any unnamed arguments which follows will be passed as script parameters. Something like below should do
powershell -File "C:\ScriptFolder\ScriptwithParameters.ps1" "ParameterOneValu" "valuetwo"
Ok so if you need another process entirely but not another file then your best bet is probably .NET runspaces. Basically wrap your code in a scriptblock
$SB = {
*Your Code*
}
Then set up a runspace like below, making sure to use the "UseNewThread" as the thread option. Note that $arg is whatever your argument to be passed to the script is
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "UseNewThread"
$newRunspace.Open()
$psCmd = [PowerShell]::Create().AddScript($SB).AddArgument($arg)
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()
You'll likely need to tweak this if you need to get any data back from the runspace once it is complete but there are a few ways to do that(leave a comment if you need assistance). If you need synchronous execution rather than async then change .BeginInvoke() to .Invoke()
So should get you started, But it will require a few moving parts.
First we define a new function:
function Run-InNewProcess{
param([String] $code)
$code = "function Run{ $code }; Run $args"
$encoded = [Convert]::ToBase64String( [Text.Encoding]::Unicode.GetBytes($code))
start-process PowerShell.exe -argumentlist '-noExit','-encodedCommand',$encoded
}
This function will be what starts the new process. It uses the start-process cmdlet, The -Argumentlist is our arguments applied to the powershell.exe You can remove -noExit to make the new process close on completion or add other powershell flags, and flags on Start-Process to get the windows and behaviours tweaked to your requirements.
Next we define our script block:
$script = {
[CmdletBinding()]
Param (
[Parameter(Position=0)]
[string]$Arg1,
[Parameter(Position=1)]
[string]$Arg2)
write-host "I am in another process"
write-host "Message.Status: $($Arg1) $($Arg2)";
}
Here we define some parameters in the opening part of the block, They have a position and name, so for example any argument in position 0 will be in the variable $arg1 The rest of the code in the block is all executed in the new process.
Now we have defined the script block and the function to run it in a new process, All we have to do is call it:
Run-InNewProcess $script -Arg1 '"WHAT WHAT"' -Arg2 '"In the But"'
Copy past this code all in to your ISE and you will see it in action.
Start-Job will create a process for its scriptblock, and it's straightforward to pass arguments to it.
Write-Host "Process count before starting job: $((Get-Process |? { $_.ProcessName -imatch "powershell" }).Count)"
$job = Start-Job `
-ArgumentList "My argument!" `
-ScriptBlock {
param($arg)
Start-Sleep -Seconds 5;
Write-Host "All Done! Argument: $arg"
}
while ($job.State -ne "Completed")
{
Write-Host "Process count during job: $((Get-Process |? { $_.ProcessName -imatch "powershell" }).Count)"
Start-Sleep -Seconds 1
}
Receive-Job $job -AutoRemoveJob -Wait
Write-Host "Process count after job: $((Get-Process |? { $_.ProcessName -imatch "powershell" }).Count)"
I'm working on a large script where I run a Foreach loop, define variables in that loop and afterwards check if the $Server variable is pingable and if it is remotely accessible.
For this I use the following functions coming from the PowerShell help:
# Function to check if $Server is online
Function CanPing ($Server) {
$error.clear()
$tmp = Test-Connection $Server -ErrorAction SilentlyContinue
if ($?) {
Write-Host "Ping succeeded: $Server"; Return $true
}
else {
Write-Host "Ping failed: $Server."; Return $false
}
}
# Function to check if $Server is remotely accessible
Function CanRemote ($Server) {
$s = New-PSSession $Server -Authentication Credssp -Credential $Credentials -Name "Test" -ErrorAction SilentlyContinue
if ($s -is [System.Management.Automation.Runspaces.PSSession]) {
Enter-PSSession -Session $s
Exit-PSSession
Write-Host "Remote test succeeded: $Server."; Return $true
}
else {
Write-Host "Remote test failed: $Server."; Return $false
}
}
# Execute functions to check $Server
if ($Server -ne "UNC") {
if (CanPing $Server) {
if (-Not (CanRemote $Server)) {
Write-Host "Exit loop REMOTE" -ForegroundColor Yellow
continue
}
}
else {
Write-Host "Exit loop PING" -ForegroundColor Yellow
continue # 'continue' to the next object and don't execute the rest of the code, 'break' exits the foreach loop completely
}
}
Every time when I run this code, there is a process created on the remote server called wsmprovhost.exe. This process represents the PowerShell session, if the info I found on the web is correct. However, when doing Get-PSSession $Server there are no open sessions displayed in the PowerShell ISE, even though the processes are visible on the remote server and can only be killed with the Task Manager.
When I run this code often the limit of open sessions is reached because every time a new process wsmprovhost.exe is added to the $Server and the command errors out. I've tried to solve this by adding Exit-PSSessionto the code, but it doesn't close the session.
Any help or ideas are more than welcome.
The problem is that Enter-PSSession. Enter-PSSession can only be used interactively, you can't use it in a script. I'd suggest something more like this:
# Function to check if $Server is remotely accessible
Function CanRemote ($Server) {
Try {
$s = New-PSSession $Server -Authentication Credssp -Credential $Credentials -Name "Test" -ErrorAction Stop
Write-Host "Remote test succeeded: $Server."
$true
Remove-PSSession $s
}
Catch {
"Remote test failed: $Server."
$false
}
}
If I have understood correctly, Your remote ps-session are not getting closed.
To my understaning, Get-PSSession will show the session till your local session
is alive (I mean the session you created the remote ps-session) but once your local session
ends Get-PSSession will not show them cause they are no more live on your computer
rather on the remote system (or) they are no more in local session scope.
You can get the session using the command
Get-PSSession -ComputerName server_name
If you want to remove them you can do like
Get-PSSession -ComputerName server_name | Remove-PSSession
Even After executing the below command also, if you are not able to create session
Get-PSSession -ComputerName server_name | Remove-PSSession
Please, Restart the service Windows Remote Management (WS-Management) in the target machine.