How to exit shell when the script contains restarting a new instance? - powershell

At the beginning of my script, I add a piece of code to make sure the shell run with full admin privileges:
# Running with full admin privileges
param([switch]$Elevated)
function Test-Admin {
$currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
$currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
if ((Test-Admin) -eq $false) {
if ($elevated) {
# tried to elevate, did not work, aborting
} else {
Start-Process pwsh -Verb RunAs -ArgumentList ('-noprofile -noexit -file "{0}" -elevated' -f ($myinvocation.MyCommand.Definition))
}
exit
}
# Other stuff below...
After that is a normal script (the Other stuff below section). The problem is, I want to automatically close the shell window after the script is done. Before I add the full privilege code, putting exit in the bottom of the script works fine, but not now anymore. I don't understand why, because everything else in this section works fine.

Normally scripts auto exit at the end.
Try to omit -noexit parameter from argumentlist and delete exit at the bottom of the script.
For me, there is no sense to call a script with -noexit and put exit at the end of it. Without these all will automagically work.
If you want to examine what is going on during script writing, you can put eg. a timeout /t 90 command at the end, so you get 90 seconds to examine output before it auto closes.

Related

Application doesn't get triggered from Powershell script, when invoking inside Do...Until

I am trying to build custom Windows System Utility script which offers some tasks with relevant keypress choices.
For cleanup task, I am trying to invoke CCleaner64.exe from this script, with it's correct switches as mentioned here. And the script I built so far is below:
$ScriptDir = Split-Path $MyInvocation.MyCommand.Path
if (-Not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) {
if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
$CommandLine = "-File `"" + $MyInvocation.MyCommand.Path + "`" " + $MyInvocation.UnboundArguments
Start-Process -FilePath PowerShell.exe -Verb Runas -ArgumentList $CommandLine
Exit
}
}
Set-Location $ScriptDir; Echo 'Current Directory: ' + (Get-Location | Out-String)
function SysUtilMenu {
param (
[string]$Title = 'Windows System Utility'
)
Clear-Host
Write-Host "============ $Title ==========="
Write-Host "1: Do task 1 here."
Write-Host "2: Cache/Temp Files Cleanup."
Write-Host "Q: Exit this Application."
}
Do
{
SysUtilMenu
$selection = Read-Host "Press key to run given task..."
switch ($selection)
{
'1' {
## Do task 1 here...
} '2' {
$CclnrApp64 = "$Env:ProgramFiles\CCleaner\CCleaner64.exe"
Start-Process $CclnrApp64 -ArgumentList '/Clean'
Start-Process $CclnrApp64 -ArgumentList '/Registry'
}
}
}
Until($selection -eq 'q')
But when I press '2', it doesn't invoke CCleaner64.exe in the taskbar, which I checked.
I don't get, why the same Start-Process line doesn't work in that script, but if I open the Powershell terminal separately and run below commands one-by-one, it works perfectly ?
$CclnrApp64 = "$Env:ProgramFiles\CCleaner\CCleaner64.exe"
Start-Process $CclnrApp64 -ArgumentList '/Clean'
Is it due to Script's Self-Elevation, I have taken care of setting the location of the script instead of C:\Windows\System32.
Suggestion with detailed explanation is greatly appreciated...
From the link you have added, the documentation under Command-line parameters for CCleaner operation pane focus, it says the switch should be /CLEANER, not /Clean, and since your code also shows the switch /Registry, I thought this is what you were after (to open the app on a particular tab page).
My initial thoughts were:
it is possible you need to add the -Wait switch so PowerShell will ony start the second command after the first one has completed.
so the ful command would be Start-Process -FilePath "$CclnrApp64" -ArgumentList '/Cleaner' -Wait
to try and use the call operator & instead of Start-Process like & "$CclnrApp64" "/CLEANER"
Both above have the paths in variable $CclnrApp64 in between quotes because $env:programfiles will usually expand to C:\Program Files which has a space in the path.
Under Command-line parameters for CCleaner Business and Technician Edition, there is also a switch called /Clean
If you have that version, the switch should clean up using whatever rules are rules defined in ccleaner.ini and optionally puts the results in log_filename.txt
However, on that same CCleaner page, there is also a listing of other parameters, especially for use in a commandline and as you have experimented using the /AUTO switch, it appears this is what you were after:
CCleaner runs silently and automatically, using the current set of saved options to clean the PC. CCleaner then exits.
A note about the /AUTO switch though:
When you run CCleaner.exe using the /AUTO parameter, CCleaner does not run the Registry cleaner. You cannot currently run the Registry cleaner through a command-line parameter
All this means there are several switches you can use with CCleaner, but they all serve a different purpose.
/CLEANER, /REGISTRY, /TOOLS and /OPTIONS are for opening the application at a certain pane
/AUTO (with optional /SHUTDOWN), /EXPORT and /DELETE (with optional /METHOD) are to have the application perform cleaning/delete actions
and for the Business and Technician Edition there is also
/analyze, /clean and /update

How to prevent exit of host and return exit code?

Ansgar Wiechers' answer works well whenever starting a new PowerShell process. https://stackoverflow.com/a/50202663/447901 This works in both cmd.exe and powershell.exe.
C:>type .\exit1.ps1
function ExitWithCode($exitcode) {
$host.SetShouldExit($exitcode)
exit $exitcode
}
ExitWithCode 23
In a cmd.exe interactive shell.
C:>powershell -NoProfile -Command .\exit1.ps1
C:>echo %ERRORLEVEL%
23
C:>powershell -NoProfile -File .\exit1.ps1
C:>echo %ERRORLEVEL%
23
In a PowerShell interactive shell.
PS C:>powershell -NoProfile -Command .\exit1.ps1
PS C:>$LASTEXITCODE
23
PS C:>powershell -NoProfile -File .\exit1.ps1
PS C:>$LASTEXITCODE
23
HOWEVER... Running the .ps1 script inside an existing interactive PowerShell host will exit the host completely.
PS C:>.\exit1.ps1
<<<poof! gone! outahere!>>>
How can I prevent it from exiting the host shell?
Do not use $host.SetShouldExit(): it is not meant to be called by user code.
Instead, it is used internally by PowerShell in response to an exit statement in user code.
Simply use exit 23 directly in your exit1.ps1 script, which will do what you want:
When run inside a PowerShell session, the script will set exit code 23 without exiting the PowerShell process as a whole; use $LASTEXITCODE to query it afterwards.
.\exit.ps1; $LASTEXITCODE # -> 23
When run via the PowerShell CLI:
with -File, the exit code set by the script automatically becomes the PowerShell process' exit code, which the caller can examine; when called from cmd.exe, %ERRORLEVEL% reflects that exit code.
powershell -File .\exit.ps1
:: This outputs 23
echo %ERRORLEVEL%
with -Command, additional work is needed, because PowerShell then simply maps any nonzero exit code to 1, which causes the specific exit code to be lost; to compensate for that, simply execute exit $LASTEXITCODE as the last statement:
powershell -Command '.\exit.ps1; exit $LASTEXITCODE'
:: This outputs 23
echo %ERRORLEVEL%
For more information about how PowerShell sets exit codes, see this answer.
If:
you do not control how your script is invoked via the CLI, yet must ensure that the correct exit code is reported even when the script is invoked via -Command,
and you're willing to assume the risk of using $host.SetShouldExit(), even though it isn't designed for direct use,
you can try the following:
function ExitWithCode($exitcode) {
if ([Environment]::CommandLine -match ( # Called via the CLI? (-File or -Command)
' .*?\b' +
[regex]::Escape([IO.Path]::GetFileNameWithoutExtension($PSCommandPath)) +
'(?:\.ps1\b| |$)')
) {
# CAVEAT: While this sets the exit code as desired even with -Command,
# the process terminates instantly.
$host.SetShouldExit($exitcode)
}
else {
# Exit normally, which in interactive session exits the script only.
exit $exitcode
}
}
ExitWithCode 23
The function looks for the file name of the executing script on the process command line to detect whether the enclosing script is being invoked directly via the CLI, via the automatic $PSCommandPath variable, which contains the script's full path.
If so, the $host.SetShouldExit() call is applied to ensure that the exit code is set as intended even in the case of invocation via -Command.
Note that this amounts to a repurposing of the effectively internal .SetShouldExit() method.
Surprisingly, this repurposing works even if additional commands come after the script call inside the -Command string, but note that this invariably means that the success status of the truly last command - if it isn't the script call - is then effectively ignored.
This approach isn't foolproof[1],but probably works well enough in practice.
[1]
There could be false positives, given that only the file name is looked for, without extension (because -Command allows omitting the .ps1 extension of scripts being called).
There could be false negatives, if the script is being called via another script or via an alias.
How can I prevent it from exiting the host shell?
You can check if the currently running PowerShell process is a child of another PowerShell parent process, and only call $host.SetShouldExit() when that condition is true. For example:
function ExitWithCode($exitcode) {
# Only exit this host process if it's a child of another PowerShell parent process...
$parentPID = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$PID" | Select-Object -Property ParentProcessId).ParentProcessId
$parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$parentPID" | Select-Object -Property Name).Name
if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) }
exit $exitcode
}
ExitWithCode 23
Hope this helps.

How to run exe with/without elevated privileges from PowerShell

I would like an easy way to run a process with different privileges from the same user without asking or knowing his/her password. A dialog is okay if necessary. I would prefer not to launch a PowerShell sub-process to accomplish this.
Scenario 1:
PowerShell script is running in admin-mode. I want to launch a script or an .exe without admin privileges but on the same user.
Scenario 2:
PowerShell script is running in normal mode. I want to launch a script or an .exe with admin privileges on the same user.
Let's split this into three parts.
First determine if current session is running with admin privileges:
$CurrentID = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$CurrentPrincipal = new-object System.Security.Principal.WindowsPrincipal($CurrentID)
$adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator
# Check to see if session is currently with admin privileges
if ($CurrentPrincipal.IsInRole($adminRole)) {
write-host "Yes we are running elevated."
}else{
write-host "No this is a normal user session."
}
Now, if we are running with or without elevation, you can start a new process with elevated privileges like this:
$newProc = new-object System.Diagnostics.ProcessStartInfo "PowerShell"
# Specify what to run
$newProc.Arguments = "powershell.exe"
# If you set this, process will be elevated
$newProc.Verb = "runas"
[System.Diagnostics.Process]::Start($newProc)
And lastly, if we have elevated privileges, but would like to start a new process without...
I have no idea. Will have to try to find the answer to this, but as it is not a common scenario, I had no luck so far.
EDIT: I have now seen a couple of “solutions” for this scenario. There is no native way to do this in .NET/PowerShell. Some are quite complicated (Calls to some 12 COM objects). This vista-7-uac-how-to-lower-process-privileges is a good reference.
The one that seems most elegant to me, is exploiting a “bug” in explorer.exe.
Just launch you .exe using explorer.exe and the resulting process runs without privilege elevation again.
$newProc = new-object System.Diagnostics.ProcessStartInfo "PowerShell"
# Specify what to run, you need the full path after explorer.exe
$newProc.Arguments = "explorer.exe C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
[System.Diagnostics.Process]::Start($newProc)
EDIT #2: Another way I have just found to start a new non-elevated process from an already elevated environment is to use the runas.exe with the 0x20000 (Basic User) trust level:
C:\> runas /showtrustlevels
The following trust levels are available on your system:
0x20000 (Basic User)
C:\> runas /trustlevel:0x20000 devenv
I use this as first command in all scripts that requires elevated mode, it transfer the script to another elevated process if I forgot to start up as Admin. You have to confirm so it's not suitable for automated tasks
If (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
$arguments = "& '" + $myinvocation.mycommand.definition + "'"
Start-Process powershell -Verb runAs -ArgumentList $arguments
Break }

Custom powershell prompt is clearing $lastexitcode

My powershell profile has a custom powershell prompt that unfortunately causes $lastexitcode values to be lost. For instance, given a powershell script "fail.ps1" with contents "exit 123", when I run the script, $? is $false while $lastexitcode is 0. If I instead run powershell without loading my profile with the custom prompt, after running fail.ps1 then $lastexitcode is 123.
Has anyone seen this problem before? Is there a way to preserve $lastexitcode as the prompt is generated?
I ran into this when using Posh-git, https://github.com/dahlbyk/posh-git, a nice powershell prompt for git.
Issue can be resolved by capturing $LASTEXITCODE at the start of the prompt and restoring it at the end:
function prompt {
$realLASTEXITCODE = $LASTEXITCODE
# ...
$LASTEXITCODE = $realLASTEXITCODE
}
You need to do this to make it work:
function prompt {
$realLASTEXITCODE = $global:LASTEXITCODE
# ...
$global:LASTEXITCODE = $realLASTEXITCODE
# cleanup
Remove-Variable realLASTEXITCODE
}

How to reload user profile from script file in PowerShell

I want to reload my user profile from a script file. I thought that dot sourcing it from within the script file would do the trick, but it doesn't work:
# file.ps1
. $PROFILE
However, it does work if I dot source it from PowerShell's interpreter.
Why do I want to do this?
I run this script every time I update my profile and want to test it, so I'd like to avoid having to restart PowerShell to refresh the environment.
If you want to globally refresh your profile from a script, you will have to run that script "dot-sourced".
When you run your script, all the profile script runs in a "script" scope and will not modify your "global" scope.
In order for a script to modify your global scope, it needs to be "dot-source" or preceded with a period.
. ./yourrestartscript.ps1
where you have your profile script "dot-sourced" inside of "yourrestartscript.ps1". What you are actually doing is telling "yourrestartscript" to run in the current scope and inside that script, you are telling the $profile script to run in the script's scope. Since the script's scope is the global scope, any variables set or commands in your profile will happen in the global scope.
That doesn't buy you much advantage over running
. $profile
So, the approach that you marked as the answer may work inside the Powershell command prompt, but it doesn't work inside PowerShell ISE (which, to me, provides a superior PowerShell session) and probably won't work right in other PowerShell environments.
Here's a script that I have been using for a while, and it has worked very well for me in every environment. I simply put this function into my Profile.ps1 at ~\Documents\WindowsPowerShell, and whenever I want to reload my profile, I dot-source the function, i.e.
. Reload-Profile
Here's the function:
function Reload-Profile {
#(
$Profile.AllUsersAllHosts,
$Profile.AllUsersCurrentHost,
$Profile.CurrentUserAllHosts,
$Profile.CurrentUserCurrentHost
) | % {
if(Test-Path $_){
Write-Verbose "Running $_"
. $_
}
}
}
& $profile
works to reload the profile.
If your profile sets aliases or executes imports which fail then you will see errors because they were already set in the previous loading of the profile.
Why are you trying to do this?
Because it is likely to create duplicates (appends to $env:path) and problems with setting constant/readonly objects causing errors.
There was a thread on this topic recently on microsoft.public.windows.powershell.
If you are trying to reset the state of the session there is no way to do this, even using an inner scope ($host.EnterNestedPrompt()) because of the ability to set variables/aliases/... at "all scope".
I found this workaround:
#some-script.ps1
#restart profile (open new powershell session)
cmd.exe /c start powershell.exe -c { Set-Location $PWD } -NoExit
Stop-Process -Id $PID
A more elaborated version:
#publish.ps1
# Copy profile files to PowerShell user profile folder and restart PowerShell
# to reflect changes. Try to start from .lnk in the Start Menu or
# fallback to cmd.exe.
# We try the .lnk first because it can have environmental data attached
# to it like fonts, colors, etc.
[System.Reflection.Assembly]::LoadWithPartialName("System.Diagnostics")
$dest = Split-Path $PROFILE -Parent
Copy-Item "*.ps1" $dest -Confirm -Exclude "publish.ps1"
# 1) Get .lnk to PowerShell
# Locale's Start Menu name?...
$SM = [System.Environment+SpecialFolder]::StartMenu
$CurrentUserStartMenuPath = $([System.Environment]::GetFolderPath($SM))
$StartMenuName = Split-Path $CurrentUserStartMenuPath -Leaf
# Common Start Menu path?...
$CAD = [System.Environment+SpecialFolder]::CommonApplicationData
$allUsersPath = Split-Path $([System.Environment]::GetFolderPath($CAD)) -Parent
$AllUsersStartMenuPath = Join-Path $allUsersPath $StartMenuName
$PSLnkPath = #(Get-ChildItem $AllUsersStartMenuPath, $CurrentUserStartMenuPath `
-Recurse -Include "Windows PowerShell.lnk")
# 2) Restart...
# Is PowerShell available in PATH?
if ( Get-Command "powershell.exe" -ErrorAction SilentlyContinue ) {
if ($PSLnkPath) {
$pi = New-Object "System.Diagnostics.ProcessStartInfo"
$pi.FileName = $PSLnkPath[0]
$pi.UseShellExecute = $true
# See "powershell -help" for info on -Command
$pi.Arguments = "-NoExit -Command Set-Location $PWD"
[System.Diagnostics.Process]::Start($pi)
}
else {
# See "powershell -help" for info on -Command
cmd.exe /c start powershell.exe -Command { Set-Location $PWD } -NoExit
}
}
else {
Write-Host -ForegroundColor RED "Powershell not available in PATH."
}
# Let's clean up after ourselves...
Stop-Process -Id $PID
This is only a refinement of the two line script in guillermooo's answer above, which did not get the new PowerShell window into the correct directory for me. I believe this is because $PWD is evaluated in the new PowerShell window's context, which is not the value we want set-location to process.
function Restart-Ps {
$cline = "`"/c start powershell.exe -noexit -c `"Set-Location '{0}'" -f $PWD.path
cmd $cline
Stop-Process -Id $PID
}
By rights it shouldn't work, as the command line it spits out is malformed, but it seems to do the job and that's good enough for me.
since I stumbled onto this several years later, I thought to add that you can use the invocation operator: & to load your profile with the default variable to your profile: $profile.
so, if your session somehow fails to load your profile (happens to me with cmder/conemu) just type:
& $profile
I used this to troubleshoot what profile was taking forever to load.
Start Run:
powershell_ise -noprofile
Then i ran this:
function Reload-Profile {
#(
$Profile.AllUsersAllHosts,
$Profile.AllUsersCurrentHost,
$Profile.CurrentUserAllHosts,
$Profile.CurrentUserCurrentHost
) | % {
if(Test-Path $_){
Write-Verbose "Running $_"
$measure = Measure-Command {. $_}
"$($measure.TotalSeconds) for $_"
}
}
}
. Reload-Profile
Thank you #Winston Fassett for getting me closer to finding my issue.
Pseudo Alias (simulate keys)
If you just want a function to work like an alias in the console, just simulate the key presses to get around having to use the dot source.
# when "reload" is typed in the terminal, the profile is reloaded
# use sendkeys to send the enter key to the terminal
function reload {
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait(". $")
[System.Windows.Forms.SendKeys]::SendWait("PROFILE")
[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")
}
screenshot of it working