How to get the PID of my Powershell Script? - powershell

I would like to get the PID of my powershell script. I'am able to do that in bash like that :
#!/bin/bash
VARIABLE=$$
echo "This is a test"
echo $VARIABLE
The output is :
root#DESKTOP-TURGKNS:~# ./test.sh
THIS IS A VARIABLE
218
And if I execute the script again, the PID change every time.
In powershell, if I try that :
$PID
Write-Output "THIS IS A TEST"
The output is :
PS C:\Windows\system32> $PID
Write-Output "THIS IS A TEST"
5520
THIS IS A TEST
PS C:\Windows\system32> $PID
Write-Output "THIS IS A TEST"
5520
THIS IS A TEST
PS C:\Windows\system32> $PID
Write-Output "THIS IS A TEST"
5520
THIS IS A TEST
I think that $$ and $PID don't work in the same way.
There is someone to show me how to do that ?

Unlike shell scripts written for POSIX-compatible shells such as bash, PowerShell scripts (*.ps1 files) run in-process.
Therefore, all invocations of a given script (more generallly, all scripts) in a given PowerShell session (process) report the same value in the automatic $PID variable, namely the current process' ID.
To run a .ps1 script out-of-process, you'll have to call the PowerShell CLI (powershell.exe for Windows PowerShell, pwsh for PowerShell (Core) 7+), which creates a PowerShell child process; e.g.:
# Note: Passing a command via { ... } only works from *inside* PowerShell.
pwsh -NoProfile { ./some.ps1 }
# With arguments
pwsh -NoProfile { ./some.ps1 #args } -args foo, $PID
However:
PowerShell's startup cost is significant, so you pay a noticeable performance penalty.
Behind the scenes, XML-based serialization and deserialization is involved for communicating data types, and the type fidelity has limits, just as in PowerShell remoting. That is, if complex objects are passed to or received from the child process, you may only get emulations of these objects - see this answer for background information.
Note that if you're calling from outside PowerShell, use the CLI's -File parameter to invoke a script (in which case only text in- and output is supported); e.g.:
pwsh -NoProfile -File ./some.ps1 foo $PID
For a comprehensive description of the PowerShell CLI, see this answer.
I think that $$ [in bash] and $PID [in PowerShell] don't work in the same way.
They do: both report the current process' ID; the difference in observed behavior is solely due to the difference between execution in a child process vs. in-process execution.
As an aside: PowerShell too has an automatic $$ variable, but it serves an entirely different purpose than in bash (where it is the equivalent of PowerShell's $PID): It contains the last token of the most recently submitted command line and is intended for interactive editing convenience (e.g., after submitting Get-ChildItem someReallyLongDirectoryName, you can refer to someReallyLongDirectoryName with $$ at the next prompt).
As such, it is the equivalent of bash's built-in $_ variable.

Related

Make Start-Process respect Verbose preference of parent script

Here are my scripts
Parent.ps1
[CmdletBinding(SupportsShouldProcess=$true)]
Param()
Write-Verbose 'Triggering Child Process...'
Start-Process PowerShell.exe '.\Child.ps1'
Child.ps1
[CmdletBinding(SupportsShouldProcess=$true)]
Param()
Write-Verbose 'Child Process Triggered.' # I want output from this line to be displayed
Write-Output 'Child Process Triggered.'
Start-Sleep 10
I'm calling the parent script as below
powershell Parent.ps1 -Verbose
Actual outpt:
VERBOSE: Triggering Child Process...
Child Process Triggered.
Desired Output:
VERBOSE: Triggering Child Process...
VERBOSE: Child Process Triggered.
Child Process Triggered.
If you really want to run .\Child.ps1 via another PowerShell instance, in a new window, asynchronously:
Start-Process PowerShell.exe "-c .\Child.ps1 -Verbose:`$$($VerbosePreference -eq 'Continue')"
Note the use of -c (-Command) to signal what PowerShell CLI parameter the command string is passed to, to distinguish it from -f (-File). While not strictly necessary, because -c is the default in Windows PowerShell (powershell.exe), it helps to clarify, especially given that PowerShell (Core) 7+ (pwsh) now defaults to -f.
When you invoke your Parent.ps1 script with -Verbose (-vb), PowerShell translates this switch to a script-scoped $VerbosePreference variable with value Continue.
To propagate a switch value - on or off - programmatically, you can follow the switch name with : and a Boolean, e.g. -Verbose:$true.
Caveat: While something like -Verbose:$false is typically the same as not passing the switch at all, there are exceptions, and this is one of them: -Verbose:$false explicitly overrides a caller's $VerbosePreference preference variable to deactivate verbose output - see this answer.
That said, this isn't a concern in your case, given that you're launching a new PowerShell instance, and there's no session-internal caller.
The above uses an expandable string to translate the value of $VerbosePreference into the appropriate Boolean; note that the subexpression ($(...)) is prefixed with `$, i.e. an escaped $ character to be retained verbatim, because stringifying a Boolean results in either True or False, so the $ prefix is needed to turn it back into a Boolean the way it needs to be represented as a literal in source code.
Note that if you were to invoke .\Child.ps1 directly from your parent script, it would automatically "inherit" the parent's $VerbosePreference value (it would see the same value by default, due to PowerShell's dynamic scoping).
A note on -c (-Command) vs. -f (-File) and PowerShell (Core) 7+:
For invoking a PowerShell script file (.ps1) via the CLI, it is generally sufficient and preferable for robust passing of verbatim arguments to use the -f (-File) parameter; -c (-Command) is only needed if you need the command string to be evaluated as PowerShell code.
In Windows PowerShell (powershell.exe), the -f parameter doesn't recognize Boolean argument values, unfortunately, which is why -c is used in the solution above. This limitation has been fixed in PowerShell (Core) 7+ (pwsh.exe).
See also:
Guidance on when to use -c (-Command) vs. -f (-File)
An overview of PowerShell's CLI; covers both editions.

Why PowerShell.exe There is no way to dot source a script?

The help document says that the script will be executed in 'dot-sourced' mode, but it doesn't. why?
I read the full text of the help document and couldn't find the reason, so I came for help.
PS> PowerShell.exe -File '.\dot-source-test.ps1'
PS> $theValue
PS> . '.\dot-source-test.ps1'
PS> $theValue
theValue
PS>
The content of 'dot-source-test.ps1' is $theValue = 'theValue'.
If the value of File is a file path, the script runs in the local scope ("dot-sourced"), so that the functions and variables that the script creates are available in the current session.
about_PowerShell_exe - PowerShell | Microsoft Docs
To prevent conceptual confusion:
In order to dot-source a script, i.e. execute it directly in the caller's scope (as opposed to a child scope, which is the default), so that the script's variables, function definitions, ... are seen by the caller:
In the current PowerShell session, just use ., the dot-sourcing operator, directly:
# Dot-source in the caller's scope.
# When executed at the prompt in an interactive PowerShell session,
# the script's definitions become globally available.
. '.\dot-source-test.ps1'
Via powershell.exe, the Windows PowerShell CLI[1]:
Note: Whatever dot-sourcing you perform this way is limited to the child process in which powershell.exe runs and its PowerShell session; it has no impact on the caller's session.
Dot-sourcing via the CLI makes sense only in two scenarios:
Scenario A: You're passing commands via the (possibly positionally implied) -Command (-c) parameter that relies on definitions that must first be dot-sourced from a script file, and you want the session to exit automatically when the commands have finished executing.
Scenario B: You're entering a (possibly nested) interactive PowerShell session into which you want to dot-source (pre-load) definitions from a script file; as any interactive session, you will need to exit it manually, typically with exit.
Scenario A: Pre-load definitions, execute commands that rely on them, then exit:
The following starts a (new) PowerShell session as follows:
Script file .\dot-source-test.ps1 is dot-sourced, which defines variable $theValue in the caller's (here: the global) scope.
The value of $theValue is output.
The new session is automatically exited on completing the commands.
PS> powershell -c '. .\dot-source-test.ps1; $theValue'
theValue
Scenario B: Enter a (new) interactive session with pre-loaded definitions:
Simply add the -noexit switch in order to enter an interactive session in which script file .\dot-source-test.ps1 has been dot-sourced:
powershell -noexit -c '. .\dot-source-test.ps1'
# You're now in a (new) interactive session in which $theValue is defined,
# and which you must eventually exit manually.
Note:
If neither -File nor a command (via explicit or implied -Command / -c) are specified, -noexit is implied.
Because -c is needed here for dot-sourcing, -noexit must be specified to keep the session open.
While using -File for dot-sourcing instead - powershell -noexit -File '.\dot-source-test.ps1' - works too, I suggest avoiding it for conceptual reasons:
While it is technically true that a script passed to -File is dot-sourced in the new session, that is (a) unexpected, given that scripts executed from inside a session are not (they run in a child scope) and (b) by far the most typical use case for -File is to execute a given script and then exit - in which case the aspect of dot-sourcing is irrelevant.
As such, it is better to think of this behavior as an implementation detail, and it is unfortunate that the CLI help mentions it so prominently - causing the confusion that prompted this question.
[1] The same applies analogously to the PowerShell [Core] 7+ CLI, pwsh, except that it defaults to -File rather than -Command.
It's about the way the path to the file is passed through the command line. See the below example when used in Command Prompt. (test.ps1 contains the line $theValue = 'theValue')
Without using the "-File" toggle, it's treated differently, as an argument to be passed to the PowerShell process being triggered.
Seeing the same thing when calling in PowerShell.
The specific part you reference is under the " -File" toggle, which needs to be used.
If the value of File is "-", the command text is read from standard input. Running powershell -File - without redirected standard input starts a regular session. This is the same as not specifying the File parameter at all.
If the value of File is a file path, the script runs in the local scope ("dot-sourced"), so that the functions and variables that the script creates are available in the current session.
(source: Microsoft Docs > About PowerShell.exe)

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.

Open a powershell window from an existing powershell window, and keep it open after running code

I can open a powershell window from an existing one and provide it code to run here
But I can't get the second window to stay open once the code has run
Here's what I want to run in the first powershell window, and I would like the powershell window that opens to stay open after running the code (currently, it closes immediately)
start powershell { ECHO "hi" }
Note
I tried some suggestions here but not having any luck
Also, I got a fix (of sorts) using something like start powershell { ECHO "hi"; TIMEOUT 20 } but that's not going to keep the window permanently open
PowerShell -Command {Write-Host "Test" } -NoExit
from about Powershell. To start the Powershell in a new window you can use:
start-process powershell.exe -ArgumentList "-noExit", "-command","Write-host 'TEST'; Write-Host 'Test2'"
Important -Command must be the last parameter (about Powershell):
When the value of Command is a string, Command must be the last parameter specified because any characters typed after the command are interpreted as the command arguments.
Generally:
If you pass a command (-Command) or script file (-File) to execute to PowerShell's CLI (powershell.exe in Windows PowerShell, pwsh.exe in PowerShell [Core] v6+), PowerShell by default executes the command / script and then exits.
With a command or script specified for execution, you need to add the -NoExit switch if you want the new session to remain open.
Caveat: (Unless you call directly from within PowerShell with a script block), a positional argument - i.e., one neither preceded by -Command nor -File - is implicitly bound to:
-Command in Windows PowerShell
-File in PowerShell [Core] v6+.
Therefore, it's advisable to use the target parameter name explicitly.
With Start-Process (whose built-in alias on Windows - but not on Unix - is start):
Note: The primary use of Start-Process is to launch an independent process asynchronously in a new window. However, the latter isn't supported on Unix-like platforms, where Start-Process's utility is therefore limited.
start powershell { ECHO "hi" } happens to work from PowerShell (except that the window closes right after executing the command), but it's important to note that you cannot actually pass script blocks to Start-Process, only strings.
Start-Process accepts an executable name (implied -FilePath parameter), and an array of string arguments (implied -ArgumentList / -Args parameter).
If you pass a script block ({ ... }), it is automatically stringified, meaning that its literal string contents are used (stripped of the { and }) as the (only) -ArgumentList string value.
Thus, bypassing the unnecessary script-block creation, your command - with -NoExit applied as desired - should be (with explicitly named parameters; note that -Command and its argument must come last):
Start-Process -FilePath powershell -ArgumentList '-NoExit -Command ECHO "hi"'
Note:
While passing arguments individually, as an array to -ArgumentList is arguably conceptually cleaner, it is actually better to pass all arguments as a single string, using embedded quoting as necessary, due to a longstanding bug - see GitHub issue #5576.
Trying to open an interactive shell in a new window as a different user, via the -Credential parameter, is broken up to at least PowerShell 7.1, resulting in keyboard input getting blocked both in the new window and in the caller's window - see this answer for a workaround with runas.exe and GitHub issue #12129.

-s when using Start-Job in powershell

I am trying to call a Start-Job in powershell. When I do, it spawns a background powershell with the following arguments:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Version 5.0 -s -NoLogo -NoProfile -EncodedCommand [encoded command I want to run in base64]
However, the powershell command never seems to complete, whatever the command I send it.
I tried spawning a powershell instance like this:
powershell.exe -s
And this also seems to create an instance that seems frozen, not executing or doing anything. Looking online, I cannot seem to find any reference to the -s argument.
Does anybody know what it's for or how to get rid of it so that my start-jobs work properly?
Edit: It's possible that -s is the shorthand for -sta, but my command does not freeze using -sta, but it does using -s.
Edit2: I have since found out that -s is a shorthand for -ServerMode, apparently a Legacy Powershell 2.0 option. I have no idea why that is added when using Start-Job.
Edit3: The command I use is:
$deploymentsJobs += Start-Job -InitializationScript { SomeSmallFunction } (AnotherFunction) -ArgumentList $arg1, $arg2, $arg3}
tl;dr:
The -s option is an expected part of the command line used to launch a background job via a new PowerShell process - it puts the new process in server mode, which is required to communicate with the calling process for background job management.
It is not a legacy option, but it isn't documented either, because it is only meant to be used internally by PowerShell.
Given that everything you describe is as expected, the problem is likely with the specific commands you run via -InitializationScript and in the main script block (the implied -ScriptBlock argument).
As you've discovered, a Start-Job call spawns a powershell -s -NoLogo -NoProfile call behind the scenes (discoverable via Task Manager).
That is, a new PowerShell process is created to run the commands in the background.
An -EncodedCommand parameter with a Base64-encoded command string is only present if you invoked Start-Process with the -Initialization parameter - the main script block (the (implied) -ScriptBlock argument) is not passed via the command line (see below).
-s is used PowerShell-internally - always - to invoke background jobs, and -s, as you've also discovered, is an alias for the -servermode switch. (Given that only -STA is documented, one would expect -s to be short for -STA, but it is not).
-s / -servermode is an implementation detail, used only by PowerShell itself, which is why it isn't documented.
This location in the PowerShell Core source code on GitHub shows you how the command line for the background process is constructed.
Server mode is the mode the background process must be in in order to communicate with the calling process via its standard streams (stdin, stdout, stderr): That is, the command to execute in the background is sent to the background process through its stdin stream, and the background process reports its output via its stdout and stderr streams.[1]
Note that XML-based serialization / deserialization happens during this inter-process communication, using the same infrastructure as PowerShell remoting - see this answer for more information.
[1] Ohad Schneider points out that it is possible to accidentally disrupt this communication if the main script block contains commands such as Start-Process -NoNewWindow with a console program that directly write to the background process' stdout stream - see this answer.