Folder Picker not loading outside ISE - powershell

I have a script that starts with a folder picker dialog, however I understand that POSH can't execute scripts outside of ISE like that (STA vs. MTA), so I have a separate script to dot-source it.
I have error handling in the first script in case the user presses Cancel:
if ($Show -eq "OK") {
return $objForm.SelectedPath
} else {
Write-Error "Operation cancelled by user."
exit
}
Now I need for the 2nd script (the one calling the first script) to detect the same Cancellation.
This is what I've got so far:
"Choose a folder containing the items to be moved..."
""
try {
powershell -STA -File "C:\Test\Script.ps1"
""
"Operation completed. An event log has been created:"
(Resolve-Path .\).Path +"\log.txt"
""
"Press any key to continue..."
""
$x = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
} catch {
if ($LastExitCode -ne 0) { exit $LastExitCode }
Write-Host "User cancelled the operation."
""
"Press any key to continue..."
""
$x = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
That gives me a nasty looking multi-line Write Error Exception in red text.
At C:\Test\Script.ps1:27 char:30
+ $folder = Select-FolderDialog <<<< #contains user's selected folder
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Select-FolderDialog
I'm not sure why it's generating an error message referencing the other script, since the other script runs fine (from ISE of course).
Desired Output:
If user cancels folder picker, I just want a nice clean error message to display:
User cancelled the operation.
Press any key to continue.
Edit
Here is the folder picker script I have. It works fine in ISE but when you Right Click and choose Run with Powershell it just launches a blank prompt window. To prevent the end user from accidentally editing the script I would like for it to run from outside ISE. BTW, I am using POSH 2.
# Function for folder picker dialog
Function Select-FolderDialog
{
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
$objForm = New-Object System.Windows.Forms.FolderBrowserDialog
# Default location is script's location
$objForm.SelectedPath = (Resolve-Path .\).Path
$objForm.Description = "Select Folder"
$Show = $objForm.ShowDialog()
If ($Show -eq "OK")
{Return $objForm.SelectedPath}
Else
{
Write-Error "Operation cancelled by user."
Exit
}
}
$folder = Select-FolderDialog #contains user's selected folder

Keep just your function in your 2nd script, dot source the file to load the function, then place the $folder = Select-FolderDialog call in your main script, a la:
"Choose a folder containing the items to be moved..."
""
try {
. "C:\Test\Script.ps1" # this dot sources the function from the second file (no need to call a separate Powershell process to do this, as data from that process won't be returned to the primary process calling it)
$folder = Select-FolderDialog # this runs the function in your original script, storing the result in the $folder variable
""
"Operation completed. An event log has been created:"
(Resolve-Path .\).Path +"\log.txt"
""
"Press any key to continue..."
""
$x = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
} catch {
if ($LastExitCode -ne 0) { exit $LastExitCode }
Write-Host "User cancelled the operation."
""
"Press any key to continue..."
""
$x = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}

Related

Comparing a String Variable via PowerShell

I'm trying to create an if and else statement within PowerShell that exits my code if the $value variable has a 0 or 1.
The below is my code which is executed remotely:
$MsgBox = {
Add-Type -AssemblyName PresentationFramework
$ButtonType = [System.Windows.MessageBoxButton]::OKCancel
$MesssageIcon = [System.Windows.MessageBoxImage]::Warning
$MessageBody = "Please save anything open in your current Web Browser, this includes Google Chrome, Microsoft Edge, and Mozilla FireFox and close each one respectively. Click Ok when done saving and closing browsers. IT Will be deleting your Cache and Cookies.
-IT"
$MessageTitle = "Read Me"
$Result = [System.Windows.MessageBox]::Show($MessageBody, $MessageTitle,$ButtonType,$MesssageIcon)
Switch ($Result){
'Ok' {
return "1"
}
'Cancel' {
return "0"
}
}
}
$value = invoke-ascurrentuser -scriptblock $MsgBox -CaptureOutput
$exit = "0"
Write-Host $value
if ($value.Equals($exit)){
Write-Host "Script Exiting"
exit
}
else{
Write-Host "Continuing Script"
}
Am I comparing the wrong way in PowerShell?
I tested the code with Write-Host and my scriptblock returns 0 or 1 as planned.
When it gets to the If and Else statement it doesn't seem to compare value and exit, and will instead go straight to the else statement and Write "Continuing Script"
I've also tried with $value -eq 0 and $value -eq "0".
Invoke-AsCurrentUser, from the third-party RunAsUser module, when used with -CaptureOutput invariably outputs text, as it would appear in the console, given that a call to the PowerShell CLI, is performed behind the scenes (powershell.exe, by default).
The captured text includes a trailing newline; to trim (remove) it, call .TrimEnd() before comparing values:
# .TrimEnd() removes the trailing newline.
$value = (Invoke-AsCurrentuser -scriptblock $MsgBox -CaptureOutput).TrimEnd()
if ($value -eq '0') {
Write-Host "Script Exiting"
exit
}
else {
Write-Host "Continuing Script"
}

Powershell start-process running windows explorer does not wait

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

Not entering Function

I'm trying to write a script to remotely rename multiple computers. Here's what I have (I know the Verify function works so that can be skipped over. The issue is occurring with the GetComputers function)
function main{
$DomainCredential = Verify
$computers = GetComputers
#Rename -computers $computers -DomainCredential $DomainCredential
}
function Verify{
# Prompt for Credentials and verify them using the DirectoryServices.AccountManagement assembly.
Write-Host "Please provide your credentials so the script can continue."
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
# Extract the current user's domain and also pre-format the user name to be used in the credential prompt.
$UserDomain = $env:USERDOMAIN
$UserName = "$UserDomain\$env:USERNAME"
# Define the starting number (always #1) and the desired maximum number of attempts, and the initial credential prompt message to use.
$Attempt = 1
$MaxAttempts = 5
$CredentialPrompt = "Enter your Domain account password (attempt #$Attempt out of $MaxAttempts):"
# Set ValidAccount to false so it can be used to exit the loop when a valid account is found (and the value is changed to $True).
$ValidAccount = $False
# Loop through prompting for and validating credentials, until the credentials are confirmed, or the maximum number of attempts is reached.
Do {
# Blank any previous failure messages and then prompt for credentials with the custom message and the pre-populated domain\user name.
$FailureMessage = $Null
$Credentials = Get-Credential -UserName $UserName -Message $CredentialPrompt
# Verify the credentials prompt wasn't bypassed.
If ($Credentials) {
# If the user name was changed, then switch to using it for this and future credential prompt validations.
If ($Credentials.UserName -ne $UserName) {
$UserName = $Credentials.UserName
}
# Test the user name (even if it was changed in the credential prompt) and password.
$ContextType = [System.DirectoryServices.AccountManagement.ContextType]::Domain
Try {
$PrincipalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext $ContextType,$UserDomain
} Catch {
If ($_.Exception.InnerException -like "*The server could not be contacted*") {
$FailureMessage = "Could not contact a server for the specified domain on attempt #$Attempt out of $MaxAttempts."
} Else {
$FailureMessage = "Unpredicted failure: `"$($_.Exception.Message)`" on attempt #$Attempt out of $MaxAttempts."
}
}
# If there wasn't a failure talking to the domain test the validation of the credentials, and if it fails record a failure message.
If (-not($FailureMessage)) {
$ValidAccount = $PrincipalContext.ValidateCredentials($UserName,$Credentials.GetNetworkCredential().Password)
If (-not($ValidAccount)) {
$FailureMessage = "Bad user name or password used on credential prompt attempt #$Attempt out of $MaxAttempts."
}
}
# Otherwise the credential prompt was (most likely accidentally) bypassed so record a failure message.
} Else {
$FailureMessage = "Credential prompt closed/skipped on attempt #$Attempt out of $MaxAttempts."
}
# If there was a failure message recorded above, display it, and update credential prompt message.
If ($FailureMessage) {
Write-Warning "$FailureMessage"
$Attempt++
If ($Attempt -lt $MaxAttempts) {
$CredentialPrompt = "Authentication error. Please try again (attempt #$Attempt out of $MaxAttempts):"
} ElseIf ($Attempt -eq $MaxAttempts) {
$CredentialPrompt = "Authentication error. THIS IS YOUR LAST CHANCE (attempt #$Attempt out of $MaxAttempts):"
}
}
} Until (($ValidAccount) -or ($Attempt -gt $MaxAttempts))
# If the credentials weren't successfully verified, then exit the script.
If (-not($ValidAccount)) {
Write-Host -ForegroundColor Red "You failed $MaxAttempts attempts at providing a valid user credentials. Exiting the script now... "
EXIT
} Else {
Write-Host "Credntials authenticated"
return $Credentials
}
}
function GetComputers{
$oldnames = New-Object System.Collections.ArrayList
Write-Output "Enter the PC numbers to be named. Do not include 'PC' only type the following numbers. Type 'end' when finished"
$userinput = Read-Host
while($userinput -ne "end"){
$userinput = "$('PC')$($userinput)"
[void]$oldnames.Add($userinput)
$userinput = Read-Host
}
return $oldnames
}
workflow Rename($computers, $DomainCredential){
foreach -parallel ($computer in $computers){
$newname = "$($computer)$('MK')"
Rename-Computer -PSComputerName $computer -NewName $newname -DomainCredential $DomainCredential
}
}
main
The Verify function works perfectly, but then it just hangs and nothing else happens. I added a debug line between the call of the Verify and the call of the GetComputer functions, and that also printed out. Im new to powershell and am out of ideas
Are you certain that it is hanging, or is Read-Host just showing a blank input location? Calling that function without any arguments will just give you a blinking cursor in command line. Try adding some form of prompt to read-host, like below:
PS C:\Users\mbolton> $var=read-host
"string"
PS C:\Users\mbolton> $var
"string"
PS C:\Users\mbolton> $var=read-host "type something in"
type something in: "different string"
PS C:\Users\mbolton> $var
"different string"
PS C:\Users\mbolton>

Using Read-host with Input validation and TRAP

I'm just a typical admin trying to make a simple script for some IT assistants in remote offices, to make domain joins easier while minimizing potential errors. The script's end game is to run the one-liner command Add-Computer -DomainName $DomainToJoin -OUPath $LocDN -NewName $WS_NewName -Restart.
But the whole script is supposed to include input validation for the computer's new name as well as for the target OU for the two-letter code for the remote office.
Googling for code snippets for days, esp. from sites like yours, was very helpful. But the problem I have now is I couldn't find the right codes to combine Read-Host , input length validation, and TRAP to work together without losing the value for my variables.
Pardon my coding as obviously I'm no real PS scripter, and I know the wrong portions of it are very basic. I would want to spend more time if I had the luxury, but I would really so much appreciate it if you could point me in the right direction.
Thank you so much in advance.
Please see my code below:
# START: Display name and purpose of invoked script
$path = $MyInvocation.MyCommand.Definition
Clear-Host
Write-Host $path
Write-Host " "
Write-Host "This script will allow you to do the following in a single-step process:"
Write-Host "(1) RENAME this computer"
Write-Host "(2) JOIN it to MYDOMAIN"
Write-Host "(3) MOVE it to a target OU"
Write-Host "(4) REBOOT"
Write-Host " "
Pause
# Function: PAUSE
Function Pause ($Message = "Press any key to continue . . . ") {
if ((Test-Path variable:psISE) -and $psISE) {
$Shell = New-Object -ComObject "WScript.Shell"
$Button = $Shell.Popup("Click OK to continue.", 0, "Script Paused", 0)
}
else {
Write-Host -NoNewline $Message
[void][System.Console]::ReadKey($true)
Write-Host
}
Write-Host " "
}
# Function: Define the parameters
Function Define-Parameters {
# Specify new computer name, with validation and TRAP
$WS_NewName = $null
while ($null -eq $WS_NewName) {
[ValidateLength(8,15)]$WS_NewName = [string](Read-Host -Prompt "NEW NAME of computer (8-15 chars.)" )
TRAP {"" ;continue}
}
Write-Host " "
# Domain to join.
$DomainToJoin = 'mydomain.net'
# Specify the target OU, with validation and trap
$baseOU='OU=Offices OU,DC=mydomain,DC=net'
$OU2 = $null
while ($null -eq $OU2) {
[ValidateLength(2,2)]$OU2 = [string](Read-Host -Prompt 'Target OU (TWO-LETTER code for your office)' )
TRAP {"" ;continue}
}
Write-Host " "
$LocDN = "OU=$OU2,$baseOU"
}
# Function: Summary and confirmation screen for defined parameters.
Function Confirm-Parameters {
Write-Host "==========================================================================="
Write-Host "Please confirm that you are joining this computer to
$DomainToJoin (MYDOMAIN)"
Write-Host "with the following parameters:"
Write-Host ""
Write-Host ""
Write-Host "Computer's NEW NAME: $WS_NewName"
# Write-Host "Domain to Join: $DomainToJoin"
Write-Host "TARGET mission OU: $OU2"
}
# Call Define-Parameters Function
Define-Parameters
# Call Confirm-Parameters Function
Confirm-Parameters
<#
Some more code here
#>
# FINAL COMMAND if all else works: Join the computer to the domain, rename it, and restart it.
# Add-Computer -DomainName $DomainToJoin -OUPath $LocDN -NewName $WS_NewName -Restart
In your code, you have a lot of things defined very strangely. Your functions create a new scope and the variables you're trying to define therein will disappear after calling them unless you change the variable scope (in this case, to $script: or $global:). Also, to use functions, you need to define them first (your Pause doesn't actually do anything)
Here's something you can do with your Define-Parameters function (I suggest looking at Get-Verb)
# Domain to join.
$DomainToJoin = 'mydomain.net'
# Function: Define the parameters
Function Get-Parameters {
do {
$global:WS_NewName = Read-Host -Prompt 'NEW NAME of computer (8-15 chars)'
} until ($WS_NewName.Length -gt 7 -and $WS_NewName.Length -lt 16)
''
do {
$global:OU2 = Read-Host -Prompt 'Target OU (TWO-LETTER code for your office)'
} until ($OU2 -match '^[a-z]{2}$')
''
$OU2 = "OU=$global:OU2,OU=Offices OU,DC=mydomain,DC=net"
}
I'd strongly recommend moving away from the ISE to do your testing and test in an actual powershell console.
Perhaps Try/Catch block instead of trap?
Try {
[ValidatePattern('^\w{8,15}$')]$compname=read-host 'enter comp name' -ErrorAction Stop
[ValidatePattern('^\w{2}$')]$OU=read-host 'enter OU name' -ErrorAction Stop
}
Catch {
$ErrorMessage = $_.Exception.Message
$ErrorLineNumber = $_.InvocationInfo.ScriptLineNumber
$ErrorCommandName = $_.InvocationInfo.InvocationName
Write-Error -Message "The error message was: <$ErrorMessage>, script line: <$ErrorLineNumber>, command name: <$ErrorCommandName>"
exit 255
}

How to detect if Azure Add-AzureAccount login fails or is canceled?

In an Azure PowerShell script I use Add-AzureAccount to login the user to Azure. But how do I detect if the user didn't finish the login successfully so that I can abort the script?
Another way to do this is to use a try and catch block.
try {
Add-AzureAccount -ErrorAction Stop
}
catch {
Write-Error $_.Exception.Message
}
#Write the remaining script here
#Control won't come here unless the Add-AzureAccount was successful
Write-Verbose 'User logged in'
However, any Microsoft account would be able to login even if they don't have any subscriptions associated. So, here is a bit modified code.
try {
$a = Add-AzureAccount -ErrorAction Stop
if ($a.Subscriptions) {
Write-Verbose 'User Logged in'
} else {
throw 'User logged in with no subscriptions'
}
}
catch {
Write-Error $_.Exception.Message
}
#Write the remaining script here
#Control won't come here unless the Add-AzureAccount was successful
Not really a PowerShell expert (and hoping we would get better answers), but can't we do something like the following:
$a = Add-AzureAccount
If ($a)
{
Write-Verbose "User logged in"
}
Else
{
Write-Verbose "User not logged in"
}
I use following function, and the key is using the -warningVariable which will capture if the user canceled the login screen on purpose or if the logged in user doesn't have any subscriptions attached. Just in case something is not captured I added an errorAction stop so the exceptions are also taken care of.
The script below also offers the chance to re-enter credentials in case the user made a mistake.
function LoginAzure
{
try
{
$a = Add-AzureAccount -ErrorAction Stop -WarningVariable warningAzure -ErrorVariable errorAzure
if ($warningAzure -ne "")
{
$continue = Read-Host "Following warning occured: " $warningAzure " Press 'R' to re-enter credentials or any other key to stop"
if ($continue -eq "R")
{
LoginAzure
}
else
{
exit 1
}
}
}
catch
{
$continue = Read-Host "Following error occured: " $errorAzure " Press 'R' to re-enter credentials or any other key to stop"
if ($continue -eq "R")
{
LoginAzure
}
else
{
exit 1
}
}
     
}
Import-Module "C:\Program Files (x86)\Microsoft SDKs\Azure\PowerShell\ServiceManagement\Azure\Azure.psd1"
LoginAzure