I have a PS script to monitor a logging file for a specific list of servers in the network. If the script logic finds the issue I'm monitoring for, I want to interrupt the script process with a launch and wait of windows explorer for the related server network folder path.
Windows explorer does show the requested folder, however PS doesn't wait for it to close.
This is the script I'm testing with:
# Sample networkfolderpath
$networkfolderpath = '\\server\d$\parent\child'
Start-Process explorer.exe -ArgumentList $networkfolderpath -Wait
FYI: I have a RDP function that is setup the same way and it does wait as expected.
Start-Process mstsc /v:$computername -Wait
I'm presuming at this point that windows explorer just behaves differently than some other exe.
What am I missing?
As mentioned in the comments, the example with mstsc only works because there's a 1-to-1 relationship between closing the main window it produces and exiting the process.
This relationship does not exist with explorer - it'll detect on launch that the desktop session already has a shell, notify it of the request, and then exit immediately.
Instead, use Read-Host to block further execution of the script until the user hits enter:
# launch folder window
Invoke-Item $networkfolderpath
# block the runtime from doing anything further by prompting the user (then discard the input)
$null = Read-Host "Edit the files, then hit enter to continue..."
Here is some proof-of-concept code that opens the given folder and waits until the new shell (Explorer) window has been closed.
This is made possible by the Shell.Application COM object. Its Windows() method returns a list of currently open shell windows. We can query the LocationURL property of each shell window to find the window for a given folder path. Since there could already be a shell window that shows the folder we want to open, I check the number of shell windows to be sure a new window has been opened. Alternatively you could choose to bring an existing shell window to front. Then just loop until the Visible property of the shell window equals $False to wait until the Window has been closed.
Function Start-Explorer {
[CmdletBinding()]
param (
[Parameter(Mandatory)] [String] $Path,
[Parameter()] [Switch] $Wait
)
$shell = New-Object -ComObject Shell.Application
$shellWndCountBefore = $shell.Windows().Count
Invoke-Item $Path
if( $wait ) {
Write-Verbose 'Waiting for new shell window...'
$pathUri = ([System.Uri] (Convert-Path $Path)).AbsoluteUri
$explorerWnd = $null
# Loop until the new shell window is found or timeout (30 s) is exceeded.
foreach( $i in 0..300 ) {
if( $shell.Windows().Count -gt $shellWndCountBefore ) {
if( $explorerWnd = $shell.Windows() | Where-Object { $_.Visible -and $_.LocationURL -eq $pathUri }) {
break
}
}
Start-Sleep -Milliseconds 100
}
if( -not $explorerWnd ) {
$PSCmdlet.WriteError( [Management.Automation.ErrorRecord]::new(
[Exception]::new( 'Could not find shell window' ), 'Start-Explorer', [Management.Automation.ErrorCategory]::OperationTimeout, $Path ) )
return
}
Write-Verbose "Found $($explorerWnd.Count) matching explorer window(s)"
#Write-Verbose ($explorerWnd | Out-String)
Write-Verbose 'Waiting for user to close explorer window(s)...'
try {
while( $explorerWnd.Visible -eq $true ) {
Start-Sleep -Milliseconds 100
}
}
catch {
# There might be an exception when the COM object dies before we see Visible = false
Write-Verbose "Catched exception: $($_.Exception.Message)"
}
Write-Verbose 'Explorer window(s) closed'
}
}
Start-Explorer 'c:\' -Wait -Verbose -EA Stop
Related
I am writing a powershell script that is running on Linux. The purpose of this script is to run the MS SQL Server and show its output, but when the user presses Ctrl+C or some error happens, the script takes a copy of the data folder and then after that exits.
[CmdletBinding()]
PARAM (
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Command,
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Args
)
BEGIN {
Write-Output "started $PSScriptRoot"
$currentErrorLevel = $ErrorActionPreference
# [console]::TreatControlCAsInput = $true
}
PROCESS {
try {
$ErrorActionPreference = 'SilentlyContinue';
$IsSqlRunning = Get-Process -name "sqlservr";
if ($null -eq $IsSqlRunning) {
start-process "/opt/mssql/bin/sqlservr" -wait -NoNewWindow
}
}
catch {
$ErrorActionPreference = $currentErrorLevel;
Write-Error $_.Exception
}
}
End {
$ErrorActionPreference = $currentErrorLevel;
#do backup
Create-Backup "/opt/mssql/bin/data"
Write-Output "finishd!"
}
I got a couple of problems with this script:
When I press Ctrl+C it breaks the main script and it never reaches to the Create-Backup section at the bottom of the script.
If I remove -Wait then the script won't show the sql log output
So my prefered solution is to run the sql with -Wait parameter and prevent the powershell to exit the code after I press Ctrl+C, but instead Ctrl+C close the sql instance
So I'm looking for a way to achieve both.
For simplicity I'll assume that your function only needs to support a single input object, so I'm using a simple function body without begin, process and end blocks, which is equivalent to having just an end block:
[CmdletBinding()]
PARAM (
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Command,
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Args
)
Write-Output "started $PSScriptRoot"
# No need to save the current value, because the modified
# value is local to the function and goes out of scope on
# exiting the function.
$ErrorActionPreference = 'SilentlyContinue';
try {
$IsSqlRunning = Get-Process -name "sqlservr";
if ($null -eq $IsSqlRunning) {
start-process "/opt/mssql/bin/sqlservr" -wait -NoNewWindow
}
}
catch {
Write-Error $_.Exception
}
finally {
# This block is *always* called - even if Ctrl-C was used.
Create-Backup "/opt/mssql/bin/data"
# CAVEAT: If Ctrl-C was used to terminate the run,
# you can no longer produce *pipeline* input at this point
# (it will be quietly ignored).
# However, you can still output to the *host*.
Write-Host "finished!"
}
If you really need to support multiple input objects, it gets more complicated.
I am not quite sure how to explain my problem, but I have a function that installs Office, imagine the person that runs this script does not have internet connection or does not have enough space on her hard drive. I have the XML file set to hide the setup interface so the user can't see the installation process. Just to be clear all my code works fine, just want add this feature so that if something goes wrong while the user runs the script I know where the error was.
This is my function:
Function Install-Office365OfficeProducts{
Write-Host ""
Start-Sleep -Seconds 5
Write-Host "Installing Office 365 ProPlus..."
# Installing Office 365 ProPlus
Install-Office365Product -path "$PSScriptRoot\setup.exe" -xmlPath "$PSScriptRoot\InstallO365.xml"
This is what I have tried:
if (Install-Office365OfficeProducts -eq 0) {
Write-Host "FAILED"}
I am very confused, I thought that a function that runs with no error returns 1 and when it runs with errors returns 0.
Also have tried to put the code like this:
try {
Install-Office365Product -path "$PSScriptRoot\setup.exe" -xmlPath "$PSScriptRoot\InstallO365.xml"
} catch {
Write-Host "Failed!"
}
EDIT:
Basically i want to be shown an error if the Office setup is not finished...
#Thomas
Function Install-Office365Product{
Param (
[string]$path,
[string]$xmlPath
)
$arguments = "/configure `"$xmlPath`""
try{
Start-Process -FilePath "$path" -ArgumentList "$arguments" -Wait -NoNewWindow -ErrorAction Stop
}catch{
Write-Host "It was not possible to install the product!"
}
}
Your try/catch-block inside Install-Office365OfficeProducts is useless, because Install-Office365Product will not throw anything, except you pass wrong arguments. The try/catch-block inside Install-Office365Product will most likely also not catch anything. But you can of course evaluate the return code of your installer called with Start-Process:
function Install-Office365Product {
Param (
[string]$path,
[string]$xmlPath
)
$arguments = "/configure `"$xmlPath`""
$process = Start-Process -FilePath "$path" -ArgumentList "$arguments" -Wait -PassThru -NoNewWindow
if ($process.ExitCode -eq 0) {
Write-Host "Installation successful"
} else {
Write-Host "Installation failed"
}
}
Instead of writing to stdout, you can of course also throw an exception and handle it later in a higher function.
I have a program which needs to be installed silently. But it doesn't stop the process at the end (because of window - need to press OK). So I need to:
Check if the folder exists
Start the installation of the program and wait for ~15 seconds
Then I need to check one of the files in this folder of the program
If it exists I should kill the process of installation (if not go to 1.)
So I have two script blocks. How can I realize it to one script block or maybe function?
if (-not(Test-Path $folder)) {
Start-Process $program /S
} else {
Write-Output "Program is already installed"
}
if (Test-Path $file) {
Stop-Process -Name "Install program*"
} else {
Write-Output "Program is already installed"
}
Nest the second conditional in the "then" branch of the first one, assign the installer process object to a variable, add a sleep after starting the process, and kill the process via the process object.
if (-not(Test-Path $folder)) {
$p = Start-Process $program /S -PassThru
Start-Sleep -Seconds 15
if (Test-Path $file) {
$p.Kill()
} else {
Write-Output "Program is already installed"
}
} else {
Write-Output "Program is already installed"
}
I'm trying to setup Powershell ISE to manage few VMs hosted under Hyper-V and using Powershell Direct.
What I'm trying to do is:
collect in one script all relevant "settings" under variables whose name starts with an appropriate prefix, for example "vms";
at the end of this script open a PowerShellTab for each VM to manage (if not already opened);
copy in the runspace of this new Tab all the relevant settings variables (I think this is necessary for the next step);
start a remote PSSession with the VM (if not already started) and copy or update the settings variables also in this remote PSSession;
enter interactively this PSSession to be able to launch my commands (F8) after selecting portions of sample/template scripts.
My script is this one.
$vms = 'VMName1','VMName2'
$vmsCredentials = #{
VMName1 = [pscredential]::new('User1', (ConvertTo-SecureString 'pw1' -AsPlainText -force))
VMName2 = [pscredential]::new('User2', (ConvertTo-SecureString 'pw2' -AsPlainText -force))
}
$vmsInfo1 = 'blah,blah,blah'
$vmsInfo2 = #{}
$vmsInfo3 = #(1,2,3)
$tabPrevious = $psISE.CurrentPowerShellTab
$vms |% {
Write-Information $_
$tab = $psISE.PowerShellTabs |? DisplayName -eq $_
if ($tab -eq $null)
{
# opens new PS ISE Tab
$tab = $psISE.PowerShellTabs.Add()
$psISE.PowerShellTabs.SetSelectedPowerShellTab($tabPrevious)
$tab.DisplayName = $_
$tab.ExpandedScript = $true
$tab.Files.Clear()
# open all files in new tab
$tabPrevious.Files |% FullPath |? { Test-Path $_ } |% {
[void]$tab.Files.Add($_)
}
}
else
{
# exit remote interactive session, if any
if ($tab.Prompt -match "^\[$_\]: ") {
while (!$tab.CanInvoke) { sleep -Milliseconds 100 }
$tab.Invoke('exit')
}
}
# export/update variables to tab
while (!$tab.CanInvoke) { sleep -Milliseconds 100 }
$rs = ($tab.InvokeSynchronous({ $ExecutionContext.Host.Runspace }, $false))[0]
if ($null -ne $rs) {
Get-Variable "vms*" |% { $rs.SessionStateProxy.SetVariable($_.Name, $_.Value) }
}
# start a new remote PSSession for the tab, if not already done
while (!$tab.CanInvoke) { sleep -Milliseconds 100 }
$tab.Invoke("if (`$null -eq `$pchPSSession) { `$pchPSSession = New-PSSession -VMName '$_' -Credential `$vmsCredentials['$_'] }")
# export/update variables to the remote PSSession and enter interactively
while (!$tab.CanInvoke) { sleep -Milliseconds 100 }
$tab.Invoke("Invoke-Command `$pchPSSession { `$args |% { Set-Variable `$_.Name `$_.Value } } -ArgumentList (Get-Variable 'vms*'); Enter-PSSession -Session `$pchPSSession")
# uncomment this line and no error occurs
#[void]$psISE.PowerShellTabs.Remove($tab)
}
Unfortunately this script works well:
the very first time I run the script in a fresh ISE,
or when only one new Tab need to be opened,
or if I close immediately the just added Tab (uncomment last line),
or under Debugger
otherwise the two last PowerShellTab.Invoke fails (Null Reference).
Any idea to solve this error?
Any way to do it better?
I'm trying to create a popup window that stays open until the script finishes.
I have the following code to create a popup box
$wshell = New-Object -ComObject Wscript.Shell
$wshell.Popup("Operation Completed",0,"Done",0x1)
$wshell.quit
I figured $wshell.quit would close the window, but it doesn't. Is there a way to close this dialog box from within the script without user interaction?
The use of $wsheel.quit won't work here because in PowerShell when you execute $wshell.Popup(..) the session will wait untill the form is closed.
You won't be able to run any other command untill the window will be closed.
What you can do is to create the popup window in different session and by that you can run you code and when your code finish, search for the job and kill it.
Solution #1:
function killJobAndItChilds($jobPid){
$childs = Get-WmiObject win32_process | where {$_.ParentProcessId -eq $jobPid}
foreach($child in $childs){
kill $child.ProcessId
}
}
function Kill-PopUp($parentPid){
killJobAndItChilds $parentPid
Get-Job | Stop-Job
Get-Job | Remove-Job
}
function Execute-PopUp(){
$popupTitle = "Done"
$popupScriptBlock = {
param([string]$title)
$wshell = New-Object -ComObject Wscript.Shell
$wshell.Popup("Operation Completed",0,$title,0x1)
}
$job = Start-Job -ScriptBlock $popupScriptBlock -ArgumentList $popupTitle
# Waiting till the popup will be up.
# Can cause bugs if you have other window with the same title, so beaware for the title to be unique
Do{
$windowsTitle = Get-Process | where {$_.mainWindowTitle -eq $popupTitle }
}while($windowsTitle -eq $null)
}
Execute-PopUp
#[YOUR SCRIPT STARTS HERE]
Write-Host "Your code"
Start-Sleep 3
#[YOUR SCRIPT ENDs HERE]
Kill-PopUp $pid
It creates your pop-up and only when the window is up (Verifying by the title. Notice that it can cause colissions if there is another process with the same window's title) your code will start run.
When your code will finish it will kill the job.
Notice that I didn't use Stop-Job to stop the job.
I guess it because when the job created the pop-up it can't receive any commands untill the popup will be close.
To overcome it I kill the job's process.
Solution #2 (using events):
function Kill-PopUp(){
kill (Get-Event -SourceIdentifier ChildPID).Messagedata
Get-Job | Stop-Job
Get-Job | Remove-Job
}
function Execute-PopUp(){
$popupTitle = "Done"
$popupScriptBlock = {
param([string]$title)
Register-EngineEvent -SourceIdentifier ChildPID -Forward
New-Event -SourceIdentifier ChildPID -MessageData $pid > $null
$wshell = New-Object -ComObject Wscript.Shell
$wshell.Popup("Operation Completed",0,$title,0x1)
}
$job = Start-Job -ScriptBlock $popupScriptBlock -ArgumentList $popupTitle
# Waiting till the popup will be up.
# Can cause bugs if you have other window with the same title, so beaware for the title to be unique
Do{
$windowsTitle = Get-Process | where {$_.mainWindowTitle -eq $popupTitle }
}while($windowsTitle -eq $null)
}
Execute-PopUp
#[YOUR SCRIPT STARTS HERE]
Write-Host "Your code"
Start-Sleep 3
#[YOUR SCRIPT ENDs HERE]
Kill-PopUp
You can, from within power shell, open internet explorer, displaying a local html page (aka a splash screen) then, when done, close it
$IE=new-object -com internetexplorer.application
$IE.navigate2("www.microsoft.com")
$IE.visible=$true
...
$IE.Quit()
Some reading Here https://social.technet.microsoft.com/Forums/ie/en-US/e54555bd-00bb-4ef9-9cb0-177644ba19e2/how-to-open-url-through-powershell?forum=winserverpowershell
Some more reading Here How to properly close Internet Explorer when launched from PowerShell?
See https://msdn.microsoft.com/en-us/library/x83z1d9f(v=vs.84).aspx for the parameters.
The one you need is [nSecondsToWait], if the value is 0 the script waits indeffinetly, use a value for the seconds and it wil close by itself.
intButton = wShell.Popup(strText,[nSecondsToWait],[strTitle],[nType])
Another way would be sending a keystoke to the dialog with wshShell.SendKeys "%N"
Using the first method here an example of what you could do.
I'm using vbscript here, no experience with powershell but it's almost the same solution.
Set wShell = WScript.CreateObject("WScript.Shell")
count = 1
result = -1
Do while result = -1
result = wShell.Popup("Operation Completed", 1, "Done", 0)
count = count + 1
Wscript.echo count
if count = 10 then Exit Do ' or whatever condition
Loop