Powershell script Stop process and proceed - powershell

I have a script where i run the shutdown.exe command on a list of computers. The script works fine until it hangs for some reason.
Is there a way that i can "ctrl + c" the shutdown command and then proceed to the next PC.
here is what im using.
buttonRestartWorkstations_Click={
#TODO: Place custom script here
$online = $checkedlistbox1.CheckedItems | where { Test-Connection -
ComputerName $_ -Count 1 -Quiet }
$computercount = $online.Items.Count
$progressbar1.Maximum = $online.Count
$progressbar1.Step = 1
$progressbar1.Value = 0
foreach ($computer in $online)
{
$progressbar1.PerformStep()
shutdown -r -t $textbox3.Text -m $computer
Start-Sleep -s 1
}
$label2.Visible = $true
$label2.Text = "Selected Servers will reboot on the " + $textbox1.text

The Restart-Computer cmdlet would give you the ability to target multiple computers in parallel, without a problem on one computer affecting execution on others.
As you state, Restart-Computer is not an option for you because you want to have a delay before the restart is initiated on a given computer (which is what shutdown -r -t <secs> gives you; note that while Restart-Computer does have a -Delay parameter, its purpose is different).
If:
your target computers are set up for PowerShell remoting
and you can run your script elevated (with administrative privileges)
you can use Invoke-Command to target the computers in parallel and then locally run shutdown.exe on them (PSv3+ syntax):
$delay = $textbox3.Text
Invoke-Command -ComputerName $online {
shutdown -r -t $using:delay
"$(('FAILED to initiate', 'Successfully initiated')[$LASTEXITCODE -eq 0]) reboot on $env:COMPUTERNAME."
} | ForEach-Object { $progressbar1.PerformStep() }
Just like with your original code, execution on each target computer will return once the restart has been initiated, though execution will happen in parallel, and the responses received from the target computers are not guaranteed to be in input order.
If you wanted to verify and wait for successful restarts, more work would be needed.
Any errors are printed to the console in red and can later be examined in the $Error collection.
Note the primary purpose of "$(('FAILED to initiate', 'Successfully initiated')[$LASTEXITCODE -eq 0]) reboot on $env:COMPUTERNAME." is to unconditionally produce some (non-error) output on each computer, so that the ForEach-Object script block is invoked for each (shutdown produces no stdout output by default, and stderr output is not acted on by ForEach-Object).

Related

PowerShell Remoting, Eventing, Runspace - sticky Event Subscriber

I am currently stuck at a problem involving a WMI Event subscriber on a remote session running in a background runspace.
Below is the representative code that reproduces the issue (the real code is too long to share here) but, essentially, through a PS script I remotely install advertised WSUS updates with reboots when necessary. It takes about 90 minutes end to end.
The issue I am trying to solve at the moment is that during the course of patching, the support staff inadvertently log in to the the server being remotely patched and do their admin activities. To remind them, I want to display a pop up message as soon as the user logs in when my remote script is running. I am trying to do it using a background runspace plugged into the main patching script. It makes use of WMI eventing on the target server (which is being patched) to monitor user logons and display message as soon as it detects one. The below code is working as I expect. It even survives target server reboots.
$RemoteServerName = 'Server1.contoso.com'
$UserLogonAlertScriptBlock = {
param ($SyncedHashTable)
$RemoteServerName = $SyncedHashTable.target
try {
$Session = New-PSSession -ComputerName $RemoteServerName -ErrorAction Stop
} catch {}
while($true){
if($Session.State -eq 'Opened') {
$RemoteMonitoringJob = Invoke-Command -Session $Session -AsJob -ScriptBlock {
$null = Register-WMIEvent -Query "SELECT * FROM __InstanceCreationEvent WITHIN 3 WHERE TargetInstance ISA 'Win32_LogonSession'" -SourceIdentifier 'User.Logon'
Wait-Event -SourceIdentifier "User.Logon" -Timeout 7200 | ForEach-Object {
msg * /TIME:7200 /V "User logon detected" | Out-Null
$_ | Remove-Event
}
}
while($RemoteMonitoringJob.State -in #('NotStarted','Running')) {
Start-Sleep -Seconds 1
}
} else {
while($true) {
try {
$Session = New-PSSession -ComputerName $RemoteServerName -ErrorAction Stop
} catch {}
if($Session.State -eq 'Opened') {
break
}
Start-Sleep -Seconds 1
}
}
}
}
$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.runspace = $Runspace
$SyncedHashTable = [hashtable]::Synchronized(#{})
$SyncedHashTable.host = $host
$SyncedHashTable.target = $RemoteServerName
$Runspace.Open()
$handle = $PowerShell.AddScript($UserLogonAlertScriptBlock).AddArgument($SyncedHashTable).BeginInvoke()
Write-Host '$(Get-Date): Long running script execution targeting $RemoteServerName has started'
Start-Sleep -Seconds 120 # it usually runs for upto 90 minutes, with remote reboots of $RemoteServerName
Write-Host "$(Get-Date): The script execution has completed"
### The code that cleans up the sticky event subscriber on the target server needs to be added here
The part I am stuck at is after the script completes its execution. The wsmanprovhost.exe running on the target server continues to stick around and shows alert messages when new users log on. I think it's because of the WMI event listener still being active on the box, not releasing the remote PS session.
In the above code, I need help close that remote listener so wsmanprovhost.exe disappears.
Could you please help?
PS. I have referred to #mklement0 's response in the following post but still no joy: The RunSpace and its closure
Update:
I have managed to address the challenge by adding a Boolean flag into the SyncedHashtable which is passed to the background runspace. When I want to stop the remote logon monitoring, in the main script I flip the flag. Since it's inside a synced hashtable, I can monitor that inside the runspace and terminate the remote invoke command job in the run space. But I still had to forcibly kill the remote wsmprovhost.exe as it refuses to go. I could do it by getting the pid of the remote PS session in advance. Not the most elegant way to close a remote PS session but it does the job for me. It's just that since the remote session is continuously monitoring for user logon event, there does not appear to be a way to run a piece of code in that session to unsubscribe the WMI event source. Will do more testing to see if there is any side effect.

How to prevent multiple instances of the same PowerShell 7 script?

Context
On a build server, a PowerShell 7 script script.ps1 will be started and will be running in the background in the remote computer.
What I want
A safenet to ensure that at most 1 instance of the script.ps1 script is running at once on the build server or remote computer, at all times.
What I tried:
I tried meddling with PowerShell 7 background jobs (by executing the script.ps1 as a job inside a wrapper script wrapper.ps1), however that didn't solve the problem as jobs do not carry over (and can't be accessed) in other PowerShell sessions.
What I tried looks like this:
# inside wrapper.ps1
$running_jobs = $(Get-Job -State Running) | Where-Object {$_.Name -eq "ImportantJob"}
if ($running_jobs.count -eq 0) {
Start-Job .\script.ps1 -Name "ImportantJob" -ArgumentList #($some_variables)
} else {
Write-Warning "Could not start new job; Existing job detected must be terminated beforehand."
}
To reiterate, the problem with that is that $running_jobs only returns the jobs running in the current session, so this code only limits one job per session, allowing for multiple instances to be ran if multiple sessions were mistakenly opened.
What I also tried:
I tried to look into Get-CimInstance:
$processes = Get-CimInstance -ClassName Win32_Process | Where-Object {$_.Name -eq "pwsh.exe"}
While this does return the current running PowerShell instances, these elements carry no information on the script that is being executed, as shown after I run:
foreach ($p in $processes) {
$p | Format-List *
}
I'm therefore lost and I feel like I'm missing something.
I appreciate any help or suggestions.
I like to define a config path in the $env:ProgramData location using a CompanyName\ProjectName scheme so I can put "per system" configuration.
You could use a similar scheme with a defined location to store a lock file created when the script run and deleted at the end of it (as suggested already within the comments).
Then, it is up to you to add additional checks if needed (What happen if the script exit prematurely while the lock is still present ?)
Example
# Define default path (Not user specific)
$ConfigLocation = "$Env:ProgramData\CompanyName\ProjectName"
# Create path if it does not exist
New-Item -ItemType Directory -Path $ConfigLocation -EA 0 | Out-Null
$LockFilePath = "$ConfigLocation\Instance.Lock"
$Locked = $null -ne (Get-Item -Path $LockFilePath -EA 0)
if ($Locked) {Exit}
# Lock
New-Item -Path $LockFilePath
# Do stuff
# Remove lock
Remove-Item -Path $LockFilePath
Alternatively, on Windows, you could also use a scheduled task without a schedule and with the setting "If the task is already running, then the following rule applies: Do not start a new instance". From there, instead of calling the original script, you call a proxy script that just launch the scheduled task.

Powershell Script cycles through machines but hangs if one loses network temporarily

I have a powershell script that parses a txt file which is full of machine names, then one by one, it creates a session to the system, runs a few commands, and moves to the next system. The script usually take about 10-30 seconds to run on each system depending on the case encountered in the script.
Once in a while the system that is currently being checked will lose the network connection for some various reason. When this happens the console starts writing yellow warning messages about attempting to reconnect for 4 minutes and then disconnects the session when it cannot reconnect.
Even if it establishes the connection again within the 4 minutes, it doesn't do anything after that, it's like the script just freezes. It won't move on to the next system and it doesn't stop the script, I have to manually stop it, or if i manually run the script, i can hit control+c to break out of the current loop, and it then moves on to the next machine in the list.
Is there any way to break out of the current loop if a warning is encountered so it can move on to the next machine? That would be my ideal solution. thanks!
Script is simple..
foreach($server in Get-Content .\machines.txt) {
if($server -match $regex){
invoke-command $server -ErrorAction SilentlyContinue -ScriptBlock{
command1
command2
command3
}
}
this is what happens
PS C:\temp> .\script.ps1
machine1
machine2
machine3
machine4
machine5
WARNING: The network connection to machine5 has been interrupted. Attempting to reconnect for up to 4 minutes...
WARNING: Attempting to reconnect to machine5 ...
WARNING: Attempting to reconnect to machine5 ...
WARNING: Attempting to reconnect to machine5 ...
WARNING: The network connection to machine5 has been restored.
But it never goes on to machine6
When i work remotely with multiple machines i usually start the processes on the machines in parallel. So i have less impact when single machines are timing out. I use powershell 7 ForEach-Object -Parallel Feature for this https://devblogs.microsoft.com/powershell/powershell-foreach-object-parallel-
feature/
Try something like this:
$Credential=Get-Credential
#all Necessary parameters must be in the Object i give to ForEach Object
$myHosts = #(
#Hosts i want to connect to with values i want to use in the loop
#{Name="probook";cred=$Credential;param1="one_1";param2="two_1"}
#{Name="probook";cred=$Credential;param1="one_2";param2="two_2"}
)
$var1="one"
$var2="two"
$myHosts | ForEach-Object -Parallel {
#Variables outside of this "Parallel" Loop are not available. Because this is startet as separate SubProcess
#All Values come from the Object i piped in the ForEach-Object
$myHost=$_
#This is written to your local Shell
Write-Host ("Computer: "+ $env:Computername)
Write-Host $myHost.param1
Write-Host $myHost.param2
Write-Host $myHost.cred.UserName
Invoke-Command -ComputerName $myHost.Name -Credential $myHost.cred -ArgumentList #($myHost.param1,$myHost.param2) -ScriptBlock {
#Variables outside of of this Invoke Command Script Block are not available because this is a new Remote-Shell on the remote Host
#Parameters in Ordner of -Argument List
param($param1,$param2)
#Do your things on the Remote-Host here
#This is not Visbible -> it is only written on the "remote Shell"
Write-Host $env:Computername
#Here you get Back Values from the remote Shell
$env:Computername
$param1
$param2
}
} -ThrottleLimit 5
Hmm his is indeed a Problem.
You could experiment with:
Start-Job
(https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/start-job?view=powershell-7.1)
Get-Job (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-job?view=powershell-7.1)
Receive-Job (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/receive-job?view=powershell-7.1)
So you have more control what the processes do.
You start Background Jobs with Start-Job. Start-Job deliveres Job Objects Back -> save them in a array or variables
With Get-Job you see all Jobs currently Running
with Receive-Job you get back the output from a Job so far. You can use receive-Job to get back all PSObjects a Background Job has written.
Cannot explain in Detail, but this woul be another try i would do for this job.

Do threads still execute using -asjob with wait-job?

Hello all and good afternoon!
I had a quick question regarding -asjob running with invoke-command.
If I run 2 Invoke-Command's using -asjob, does it run simultaneously when I try to receive the ouput? Does this mean wait-job waits till the first job specified is finished running to get the next results?
Write-Host "Searching for PST and OST files. Please be patient!" -BackgroundColor White -ForegroundColor DarkBlue
$pSTlocation = Invoke-Command -ComputerName localhost -ScriptBlock {Get-Childitem "C:\" -Recurse -Filter "*.pst" -ErrorAction SilentlyContinue | % {Write-Host $_.FullName,$_.lastwritetime}} -AsJob
$OSTlocation = Invoke-Command -ComputerName localhost -ScriptBlock {Get-Childitem "C:\Users\me\APpdata" -Recurse -Filter "*.ost" -ErrorAction SilentlyContinue | % {Write-Host $_.FullName,$_.lastwritetime} } -AsJob
$pSTlocation | Wait-Job | Receive-Job
$OSTlocation | Wait-Job | Receive-Job
Also, another question: can i save the output of the jobs to a variable without it showing to the console? Im trying to make it where it checks if theres any return, and if there is output it, but if theres not do something else.
I tried:
$job1 = $pSTlocation | Wait-Job | Receive-Job
if(!$job1){write-host "PST Found: $job1"} else{ "No PST Found"}
$job2 = $OSTlocation | Wait-Job | Receive-Job
if(!$job2){write-host "OST Found: $job2"} else{ "No OST Found"}
No luck, it outputs the following:
Note: This answer does not directly answer the question - see the other answer for that; instead, it shows a reusable idiom for a waiting for multiple jobs to finish in a non-blocking fashion.
The following sample code uses the child-process-based Start-Job cmdlet to create local jobs, but the solution equally works with local thread-based jobs created by Start-ThreadJob as well as jobs based on remotely executing Invoke-Command -ComputerName ... -AsJob commands, as used in the question.
It shows a reusable idiom for a waiting for multiple jobs to finish in a non-blocking fashion that allows for other activity while waiting, along with collecting per-job output in an array.
Here, the output is only collected after each job completes, but note that collecting it piecemeal, as it becomes available, is also an option, using (potentially multiple) Receive-Job calls even before a job finishes.
# Start two jobs, which run in parallel, and store the objects
# representing them in array $jobs.
# Replace the Start-Job calls with your
# Invoke-Command -ComputerName ... -AsJob
# calls.
$jobs = (Start-Job { Get-Date; sleep 1 }),
(Start-Job { Get-Date '1970-01-01'; sleep 2 })
# Initialize a helper array to keep track of which jobs haven't finished yet.
$remainingJobs = $jobs
# Wait iteratively *without blocking* until any job finishes and receive and
# output its output, until all jobs have finished.
# Collect all results in $jobResults.
$jobResults =
while ($remainingJobs) {
# Check if at least 1 job has terminated.
if ($finishedJob = $remainingJobs | Where State -in Completed, Failed, Stopped, Disconnected | Select -First 1) {
# Output the just-finished job's results as part of custom object
# that also contains the original command and the
# specific termination state.
[pscustomobject] #{
Job = $finishedJob.Command
State = $finishedJob.State
Result = $finishedJob | Receive-Job
}
# Remove the just-finished job from the array of remaining ones...
$remainingJobs = #($remainingJobs) -ne $finishedJob
# ... and also as a job managed by PowerShell.
Remove-Job $finishedJob
} else {
# Do other things...
Write-Host . -NoNewline
Start-Sleep -Milliseconds 500
}
}
# Output the jobs' results
$jobResults
Note:
It's tempting to try $remainingJobs | Wait-Job -Any -Timeout 0 to momentarily check for termination of any one job without blocking execution, but as of PowerShell 7.1 this doesn't work as expected: even already completed jobs are never returned - this appears to be bug, discussed in GitHub issue #14675.
If I run 2 Invoke-Command's using -asjob, does it run simultaneously when I try to receive the output?
Yes, PowerShell jobs always run in parallel, whether they're executing remotely, as in your case (with Invoke-Command -AsJob, assuming that localhost in the question is just a placeholder for the actual name of a different computer), or locally (using Start-Job or Start-ThreadJob).
However, by using (separate) Wait-Job calls, you are synchronously waiting for each jobs to finish (in a fixed sequence, too). That is, each Wait-Job calls blocks further execution until the target job terminates.[1]
Note, however, that both jobs continue to execute while you're waiting for the first one to finish.
If, instead of waiting in a blocking fashion, you want to perform other operations while you wait for both jobs to finish, you need a different approach, detailed in the the other answer.
can i save the output of the jobs to a variable without it showing to the console?
Yes, but the problem is that in your remotely executing script block ({ ... }) you're mistakenly using Write-Host in an attempt to output data.
Write-Host is typically the wrong tool to use, unless the intent is to write to the display only, bypassing the success output stream and with it the ability to send output to other commands, capture it in a variable, or redirect it to a file. To output a value, use it by itself; e.g., $value instead of Write-Host $value (or use Write-Output $value, though that is rarely needed); see this answer.
Therefore, your attempt to collect the job's output in a variable failed, because the Write-Host output bypassed the success output stream that variable assignments capture and went straight to the host (console):
# Because the job's script block uses Write-Host, its output goes to the *console*,
# and nothing is captured in $job1
$job1 = $pSTlocation | Wait-Job | Receive-Job
(Incidentally, the command could be simplified to
$job1 = $pSTlocation | Receive-Job -Wait).
[1] Note that Wait-Job has an optional -Timeout parameter, which allows you to limit waiting to at most a given number of seconds and return without output if the target job hasn't finished yet. However, as of PowerShell 7.1, -Timeout 0 for non-blocking polling for whether jobs have finished does not work - see GitHub issue #14675.

Is there a way to disable and enable network connection when internet is lost

this is my first post so hopefully I am doing it right.
I am looking for a way to automate the process of disabling my Ethernet network connection and then reenable it.
I am on a camp internet connection (work away for weeks at a time) and the internet connection is fine besides it dropping out every half an hour or so and requiring me to refresh the connection. Usually I just disconnect the Ethernet plug for a second and plug it back in but I am wanting to automate the process so I can remote connect to the pc when I am away.
Any help on how to accomplish this would be appreciated.
Edit: I left a vital part out of my description. I am wanting the script to detect when the internet connection is lost and then refresh the connection. Once the connection is lost it never regains it until I refresh the connection.
If you are using DHCP
Then Dollars to donuts you are just needing to renew your DHCP lease.
That is something you can do in CMD easily (I'm sure there is likely as easy a method in PS, but I don't know it offhand)
This will release the currently assigned DHCP Address wait a few seconds and then renew it.
IPConfig /Release & timeout 5 & IPConfig /Renew
At the CLI or in a batch script you could set up a do-while loop to loop until you return an expected up address if you wanted.
Example of a costume loop in cmd cli.
For /L %L (1,1,2147483648) DO (
IPConfig /Release & timeout 3 & IPConfig /Renew &timeout 3 &(
Ping -n 2 4.2.2.2 | find /I "Reply From" | find /I " 4.2.2.2" &&(
Exit /b 0
)
)
)
Disabling and enabling your NIC can be done through CMD or Powershell.
Here Powershell is easier.
Get only currently enabled adapters, use that to disable them wait a few seconds and enable them again
$NIC_Original_State = get-NetAdapter | ? {$_.Status -eq "Enabled"}
$NIC_Original_State | Disable-NetAdapter -name $_.name -confirm:$false
Sleep 5
$NIC_Original_State | Enable-NetAdapter -name $_.name -confirm:$false
Combine with checking the ping to the internet (as in cmd example) to generate a loop until we have a good connection.
$NIC_Original_State = get-NetAdapter | ? {$_.Status -eq "Enabled"}
DO {
$NIC_Original_State | Disable-NetAdapter -name $_.name -confirm:$false
Sleep 5
$NIC_Original_State | Enable-NetAdapter -name $_.name -confirm:$false
Sleep 5
} While ( ! $(test-connection -TargetName 4.2.2.2 -Quiet ) )
Now, PowerShell will need to be instantiated from a CMD prompt, I usually write a .cmd script wrapper for the .ps1 script to be called from in task scedualer or by hand.
Sometimes I will write a combo .cmd script with the ps code embedded and just create a hardline. To the original .cmd script as a. .ps1 to execute the ps code.
Although for something this simple it might be simplest to use PowerShell.exe to run the script as an in-line command.
But all roads lead to Rome, you do you on executing PowerShell, but in order to run it periodically, you will need to schedule the script to run, and by default there is no way to run ps scripts directly, so pick a method that you like that works for running the script and then use windows scheduler to schedule the task to run every x interval you like (say hourly)
Make surw you select a new task (not a basic task) and select run with highest priviledges" checkbox, and select the newest version of windows available in the compatibility list-box.
Make sure to provide a useename and password of an administrator, and select to save the password/run whether the user is logged on or not.
Set your triggers and put in the method you chose to execute the script and run some tests to make sure it works as expected.
Edit:
The original method is int he form DO { Action } WHILE ( Condition To Check Returns True) Since the While is at the end it guarantees one iteration.
Since you don't want to disable and enable the NIC even once if the internet is pingable, you can use a WHILE ( Condition To Check Returns True) { Action }
$NIC_Original_State = get-NetAdapter | ? {$_.Status -eq "Enabled"}
While ( ! $( test-connection -ComputerName 4.2.2.2 -Quiet ) ) {
$NIC_Original_State | Disable-NetAdapter -name $_.name -confirm:$false
Sleep 5
$NIC_Original_State | Enable-NetAdapter -name $_.name -confirm:$false
Sleep 5
}
You absolutely need to have a scheduled task to run this regularly.
Even if you want to only start it once and leave it in the background, and havit checking always (which we could do) you'll need to set up a scheduled task to kill it if still running and start it again at some interval as it's possible to have a process become unresponsive or be killed or have the computer restart etc and you forget to start the script etc.
This version will just loop every one minute to check, and only executes when the internet is not reachable.
while ($true) {
$NIC_Original_State = get-NetAdapter | ? {$_.Status -eq "Enabled"}
While ( ! $( test-connection -ComputerName 4.2.2.2 -Quiet ) ) {
$NIC_Original_State | Disable-NetAdapter -name $_.name -confirm:$false
Sleep 5
$NIC_Original_State | Enable-NetAdapter -name $_.name -confirm:$false
Sleep 5
}
sleep 60
}
Again, you ca just run this in powershell, sure, btu then if it gets stopped for any reason it won;t be started again.
Use task scheduler ad set up an action to run this o Startup and once an hour, killing any previously running copy of the script.
Usually for running a CMD script I just write a simple cmd script so if I want to edit any part of anything I only change the script never the task.
<# ## & REM Script Name:
#(
SETLOCAL ENABLEDELAYEDEXPANSION
ECHO OFF
(
NET SESSION 2>&1 >NUL
) || (
powershell.exe -Command "Start-Process cmd \"/k %~dpnx0\" -Verb RunAs"
pause
GOTO :EOF
)
IF EXIST "%~dpn0.ps1" (
DEL /Q /F %~dpn0.ps1"
)
MKLINK /H "%~dpn0.ps1" "%~f0"
)
Powershell.exe -ExecutionPolicy Bypass -File "%~dpn0.ps1"
(
ENDLOCAL
EXIT /B %_eLvl%
)
#>
## Script:
## Powershell Portion Begins
while ($true) {
$NIC_Original_State = get-NetAdapter | ? {$_.Status -eq "Enabled"}
While ( ! $( test-connection -ComputerName 4.2.2.2 -Quiet ) ) {
$NIC_Original_State | Disable-NetAdapter -name $_.name -confirm:$false
Sleep 5
$NIC_Original_State | Enable-NetAdapter -name $_.name -confirm:$false
Sleep 5
}
sleep 60
}
You can handle this using Get-NetAdapter , Enable-NetAdapter and Disable-NetAdapter commands.