Help with calling Powershell from another powershell

Hello, I am new to Powershell and need help with calling a powershell script. I have created an appication with AppDeployToolkit. It is deployed with SCCM and on initial install it saves my two powershell scripts Script1.ps1 and Script2.ps1 here: $envProgramData\company\checkpendingreboot.

Every morning at 10:00 task manager calls Script1.ps1 from that location. This script checks for pending reboot, pending file rename and uptime. It returns False if no pending reboot, pending file rename or up time greater than 5 days. If found it returns True and should call Script2.ps1. (Script2.ps1 should open a window notifying the user a reboot is required.) This is where I have a mistake in my powershell script. Here is what the code in Script1.ps1 looks like when it should be calling Script2.ps1.

#IF ($output.IsPendingReboot -eq ‘True’)
IF ($rebootpending -eq ‘True’ -or $Uptime -ge ‘5’)
{
$mainExitCode = 3010
“reboot pending detected” > “C:\ProgramData\company\Logs\Reboot_Pending_detected.log”
powershell -File “C:\ProgramData\company\Check_PendingReboot\Script2.ps1”
}

I am still learning so maybe my approach is totally incorrect. I would appreciate help.

Idontknow,
Welcome to the forum. :wave:t4:

Not a very wise choice of your nickname. At some time in the future you will be knowing and then it’s misleading. :wink: :smiley:

Maybe not incorrect but unnecessarily cumbersome.

First - why 2 scripts?
Second - when you want to call another *.ps1 script from an existing PowerShell session you don’t need to call PowerShell again. It’s enough to provide the script name/path.
Next - you may share the rest of your code. I could imagine there’s more room for improvement.
For example

You should use the proper automatic PowerShell variables $true or $false instead of strings as content for your variable $rebootpending. Then you don’t need the actual comparison. Instead you could use the variable $rebootpending directly.
And if you want to compare against [int] typed values you cannot use quotes. They turn your integer to a string.
Next - why do you only compare two conditions? You mentioned you have 3 … “PendingReboot”, “PendingFileRename” and “Uptime greater than 4 days”.

And last but not least:
When you post code, sample data, console output or error messages please format it as code using the preformatted text button ( </> ). Simply place your cursor on an empty line, click the button and paste your code.

Thanks in advance

How to format code in PowerShell.org <---- Click :point_up_2:t4: :wink:

I’ll make it my goal to change my username to Idontknowmuchwithoutgoogle :wink:
I have cobbled together many scripts that I have found online. I did try to combine the two scripts but I was getting a Task Schedule result of 0x41301 and I assumed it was because my Script2.ps1 was still running somehow without the window showing.
As to why, I only compare two things I thought I had covered pendingfilerename in the tests for pendingreboot. Perhaps I didn’ though.
I really want to learn so I will provide my scripts that I have cobbled together for your review. Thank you for taking the time to even review my first posting!

Script1.ps1

if ($company_UseDialogs){
			## Only use for longer installations (Installation duration approx. >3 minutes)
			#Show-InstallationProgress -WindowLocation 'BottomRight'
		}

		$ErrorActionPreference = 'Stop'

		$scriptBlock = {
			if ($null -ne $using) {
				# $using is only available if this is being called with a remote session
				$VerbosePreference = $using:VerbosePreference
			}

			function Test-RegistryKey {
				[OutputType('bool')]
				[CmdletBinding()]
				param
				(
					[Parameter(Mandatory)]
					[ValidateNotNullOrEmpty()]
					[string]$Key
				)
			
				$ErrorActionPreference = 'Stop'

				if (Get-Item -Path $Key -ErrorAction Ignore) {
					$true
				}
			}

			function Test-RegistryValue {
				[OutputType('bool')]
				[CmdletBinding()]
				param
				(
					[Parameter(Mandatory)]
					[ValidateNotNullOrEmpty()]
					[string]$Key,

					[Parameter(Mandatory)]
					[ValidateNotNullOrEmpty()]
					[string]$Value
				)
			
				$ErrorActionPreference = 'Stop'

				if (Get-ItemProperty -Path $Key -Name $Value -ErrorAction Ignore) {
					$true
				}
			}

			function Test-RegistryValueNotNull {
				[OutputType('bool')]
				[CmdletBinding()]
				param
				(
					[Parameter(Mandatory)]
					[ValidateNotNullOrEmpty()]
					[string]$Key,

					[Parameter(Mandatory)]
					[ValidateNotNullOrEmpty()]
					[string]$Value
				)
			
				$ErrorActionPreference = 'Stop'

				if (($regVal = Get-ItemProperty -Path $Key -Name $Value -ErrorAction Ignore) -and $regVal.($Value)) {
					$true
				}
			}

			# Added "test-path" to each test that did not leverage a custom function from above since
			# an exception is thrown when Get-ItemProperty or Get-ChildItem are passed a nonexistant key path
			$tests = @(
				{ Test-RegistryKey -Key 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' }
				{ Test-RegistryKey -Key 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootInProgress' }
				{ Test-RegistryKey -Key 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' }
				{ Test-RegistryKey -Key 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\PackagesPending' }
				{ Test-RegistryKey -Key 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting' }
				{ Test-RegistryValueNotNull -Key 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Value 'PendingFileRenameOperations' }
				{ Test-RegistryValueNotNull -Key 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Value 'PendingFileRenameOperations2' }
				{ 
					# Added test to check first if key exists, using "ErrorAction ignore" will incorrectly return $true
					'HKLM:\SOFTWARE\Microsoft\Updates' | Where-Object { test-path $_ -PathType Container } | ForEach-Object {
						if(Test-Path "$_\UpdateExeVolatile" ){ 
							(Get-ItemProperty -Path $_ -Name 'UpdateExeVolatile' | Select-Object -ExpandProperty UpdateExeVolatile) -ne 0 
						}else{ 
							$false 
						}
					}
				}
				{ Test-RegistryValue -Key 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' -Value 'DVDRebootSignal' }
				{ Test-RegistryKey -Key 'HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttempts' }
				{ Test-RegistryValue -Key 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon' -Value 'JoinDomain' }
				{ Test-RegistryValue -Key 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon' -Value 'AvoidSpnSet' }
				{
					# Added test to check first if keys exists, if not each group will return $Null
					# May need to evaluate what it means if one or both of these keys do not exist
					( 'HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName' | Where-Object { test-path $_ } | % { (Get-ItemProperty -Path $_ ).ComputerName } ) -ne 
					( 'HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName' | Where-Object { Test-Path $_ } | % { (Get-ItemProperty -Path $_ ).ComputerName } )
				}
				{
					# Added test to check first if key exists
					'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Services\Pending' | Where-Object { 
						(Test-Path $_) -and (Get-ChildItem -Path $_) } | ForEach-Object { $true }
				}
			)

			foreach ($test in $tests) {
				Write-Verbose "Running scriptblock: [$($test.ToString())]"
				if (& $test) {
					$true
					break
				}
			}
		}

		# if ComputerName was not specified, then use localhost 
		# to ensure that we don't create a Session.
		if ($null -eq $ComputerName) {
			$ComputerName = "localhost"
		}

		foreach ($computer in $ComputerName) {
			try {
				$connParams = @{
					'ComputerName' = $computer
				}
				if ($PSBoundParameters.ContainsKey('Credential')) {
					$connParams.Credential = $Credential
				}

				$output = @{
					ComputerName    = $computer
					IsPendingReboot = $false
				}

				if ($computer -in ".", "localhost", $env:COMPUTERNAME ) {        
					if (-not ($output.IsPendingReboot = Invoke-Command -ScriptBlock $scriptBlock)) {
						$output.IsPendingReboot = $false
					}
				}
				else {
					$psRemotingSession = New-PSSession @connParams
				
					if (-not ($output.IsPendingReboot = Invoke-Command -Session $psRemotingSession -ScriptBlock $scriptBlock)) {
						$output.IsPendingReboot = $false
					}
				}
				[pscustomobject]$output
			} catch {
				Write-Error -Message $_.Exception.Message
			} finally {
				if (Get-Variable -Name 'psRemotingSession' -ErrorAction Ignore) {
					$psRemotingSession | Remove-PSSession
				}
			}
		}
		$rebootpending = $output.IsPendingReboot

		##checking for System Uptime
		$LastBootUpTime=Get-WinEvent -ProviderName eventlog | Where-Object {$_.Id -eq 6005} | Select-Object TimeCreated -First 1 
		$timenow=Get-Date 
		$Uptime = New-TimeSpan -Start $LastBootUpTime.TimeCreated.Date -End $timenow 
		$Uptime = $Uptime.Days

		#IF ($output.IsPendingReboot -eq 'True')
		IF ($rebootpending -eq 'True' -or $Uptime -ge '5')  
		{
			$mainExitCode = 3010
			"reboot pending detected" > "C:\ProgramData\Company\Logs\Reboot_Pending_detected.log"
			powershell -File "C:\ProgramData\Company\Check_PendingReboot\Schedule_Reboot_or_Reboot_Now.ps1" 
		}
		ELSE 
		{	$mainExitCode = 0
			"no reboot pending detected" > "C:\ProgramData\Company\Logs\No_Reboot_Pending_detected.log"
		}

Script2.ps1 (Schedule_Reboot_or_Reboot_Now.ps1)

[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | out-null
[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") | out-null
$TimeStart = Get-Date
$TimeEnd = $timeStart.addminutes(360)
Do
{
	$TimeNow = Get-Date
	if ($TimeNow -ge $TimeEnd)
	{
		
		Unregister-Event -SourceIdentifier click_event -ErrorAction SilentlyContinue
		Remove-Event click_event -ErrorAction SilentlyContinue
		[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
		[void][System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
		Exit
	}
	else
	{
		$Balloon = new-object System.Windows.Forms.NotifyIcon
		$Balloon.Icon = [System.Drawing.SystemIcons]::Information
		$Balloon.BalloonTipText = "Aufgrund der Softwareinstallation muss der Computer neu gestartet werden."
		$Balloon.BalloonTipTitle = "Neustart erforderlich"
		$Balloon.BalloonTipIcon = "Warning"
		$Balloon.Visible = $true;
		$Balloon.ShowBalloonTip(20000);
		$Balloon_MouseOver = [System.Windows.Forms.MouseEventHandler]{ $Balloon.ShowBalloonTip(20000) }
		$Balloon.add_MouseClick($Balloon_MouseOver)
		Unregister-Event -SourceIdentifier click_event -ErrorAction SilentlyContinue
		Register-ObjectEvent $Balloon BalloonTipClicked -sourceIdentifier click_event -Action {
			Add-Type -AssemblyName Microsoft.VisualBasic
			
			If ([Microsoft.VisualBasic.Interaction]::MsgBox('Wollen Sie Ihren Computer jetzt neu starten?', 'YesNo,MsgBoxSetForeground,Question', 'System Maintenance') -eq "NO")
			{ }
			else
			{
				shutdown -r -f
			}
			
		} | Out-Null
		
		Wait-Event -timeout 600 -sourceIdentifier click_event > $null
		Unregister-Event -SourceIdentifier click_event -ErrorAction SilentlyContinue
		$Balloon.Dispose()
	}

}
Until ($TimeNow -ge $TimeEnd)

So, I combined the two scripts. In Powershell ISE it runs perfectly. The problems start when I run it through task manager. The popup window stating a reboot is required never shows. Is this because task manager is being run at System and hidden and won’t display to the user?

That’s what I actually already was about to answer. I was just not finished yet looking at your code. :wink:

Yes … sessions in a Windows systems are isolated. But since you only read registry settings you could run your script in the context of the logged on user. But that would show at least the taskbar icon of the running PowerShell instance. :man_shrugging:t4:

Yes, I used $env:username instead of System and the notification did in fact show up. However, you are correct, the Powershell screen did as well. :frowning: I know this won’t be deemed acceptable. I thought about using a Configuration Item with Remediation but I don’t know how this will work.
Thanks again for helping!

Look at the available parameters for powershell.exe and try adjusting your scheduled task, might be able to hide the window.

powershell.exe /?
...
-WindowStyle
    Sets the window style to Normal, Minimized, Maximized or Hidden.
...

Unfortunately this will not work either. There is a workaround with a VBS script as wrapper for the PowerShell call though. You will find it when you search for something like “Hide PowerShell Console window”. Or there are workarounds available out there let you at least minimize the console window if that’s enough.

I’m note sure if that works but you could try to set up an SCCM application calling your script, actually hide the PowerShell window but allow the “User Interaction”.

Yes, I tried this and it still flashed very quickly. Thanks for the suggestion though.

I combined the two scripts I posted above. This was much cleaner than calling one from the other.
Now on to the creating a scheduled task:
I wanted my scheduled task to not pop up a powershell window to the logged on user. I had to create a .vbs file which would be called by task scheduler. Here is the code in the vbs file.

CreateObject("Wscript.Shell").Run """%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe"" -NoProfile -ExecutionPolicy Bypass -File ""C:\Script.ps1""",0,True

I used this vbs script to launch my powershell script for the scheduled task.
One last requirement:
Since my powershell script displays a notification to the user I needed to make sure the scheduled task was run as the logged on user. If it was run by SYSTEM my logged on user wouldn’t see the notification window. I did this by using -GroupID Users in my powershell script to create a scheduled task.

Maybe this will help someone else.