Returning value from Start-Process argument - powershell

I have a PowerShell function (Add-EventLogSource) that checks if an event log source exists. If it does not exist and the shell is not elevated, I start a new, elevated shell and call the function again.
I can't seem to get the return values correct. If the event log source does not exist, and I call Add-EventLogSource, I am not getting the return value all the way back to instance that originally called Add-EventLogSource. Can anyone see the problem? The code looks like this:
Function Add-EventLogSource {
Param (
[Parameter(Mandatory=$True)]
$EventLogSource
)
# Check if $EventLogSource exists as a source. If the shell is not elevated and the check fails to access the Security log, assume the source does not exist.
Try {
$sourceExists = [System.Diagnostics.EventLog]::SourceExists("$EventLogSource")
}
Catch {
$sourceExists = $False
}
If ((([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] “Administrator”)) -AND ($sourceExists -eq $False)) { # Shell is elevated...
Try {
New-EventLog –LogName Application –Source $EventLogSource -ErrorAction Stop
}
Catch {
Return "Error"
}
Return "Created"
}
ElseIf ($sourceExists -eq $False) {
$return = Start-Process PowerShell –Verb RunAs -ArgumentList "Add-EventLogSource -EventLogSource $EventLogSource; start-sleep 5" -Wait
Return $return
}
Else {
Return "Exists"
}
}
Thanks.

Related

How to run a module within a Scriptblock in PowerShell?

I am currently trying to import a .psm1 file dynamically into a script block to execute it.
I am using parallelisation along with jobs as I need to trigger several modules simultaneously as different users.
This is the code:
$tasksToRun | ForEach-Object -Parallel {
$ScriptBlock = {
param ($scriptName, $Logger, $GlobalConfig, $scriptsRootFolder )
Write-Output ("hello $($scriptsRootFolder)\tasks\$($scriptName)")
Import-Module ("$($scriptsRootFolder)\tasks\$($scriptName)")
& $scriptName -Logger $Logger -GlobalConfig $GlobalConfig
}
$job = Start-Job -scriptblock $ScriptBlock `
-credential $Cred -Name $_ `
-ArgumentList ($_, $using:Logger, $using:globalConfig, $using:scriptsRootFolder) `
Write-Host ("Running task $_")
$job | Wait-job -Timeout $using:timeout
if ($job.State -eq 'Running') {
# Job is still running, stop it
$job.StopJob()
Write-Host "Stopped $($job.Name) task as it took too long"
}
else {
# Job completed normally, get the results
$job | Receive-Job
Write-Host "Finished task $($job.Name)"
}
}
The logger variable is a hashtable as defined here:
$Logger = #{
generalLog = $function:Logger
certificateLog = $function:LoggerCertificate
alertLog = $function:LoggerAlert
endpointServiceLog = $function:LoggerEndpointService
}
Currently, it is erroring with the following:
ObjectNotFound: The term
' blah blah blah, this is the code straight from the logger function '
is not recognized as the name of a cmdlet, function, script file, or operable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
The logger function servers the purpose of logging to a file in a specific way, it is generalised to that it can be used across many tasks.
A cut down example of a logger (probably won't compile, just deleted a bunch of lines to give you the general idea):
function LoggerEndpointService {
param (
# The full service name.
[string]$ServiceFullName,
# The unique identifier of the service assigned by the operating system.
[string]$ServiceId,
# The description of the service.
[string]$Description,
# The friendly service name.
[string]$ServiceFriendlyName,
# The start mode for the service. (disabled, manual, auto)
[string]$StartMode,
# The status of the service. (critical, started, stopped, warning)
[string]$Status,
# The user account associated with the service.
[string]$User,
# The vendor and product name of the Endpoint solution that reported the event, such as Carbon Black Cb Response.
[string]$VendorProduct
)
$ServiceFullName = If ([string]::IsNullOrEmpty($ServiceFullName)) { "" } Else { $ServiceFullName }
$ServiceId = If ([string]::IsNullOrEmpty($ServiceId)) { "" } Else { $ServiceId }
$ServiceFriendlyName = If ([string]::IsNullOrEmpty($ServiceFriendlyName)) { "" } Else { $ServServiceFriendlyNameiceName }
$StartMode = If ([string]::IsNullOrEmpty($StartMode)) { "" } Else { $StartMode }
$Status = If ([string]::IsNullOrEmpty($Status)) { "" } Else { $Status }
$User = If ([string]::IsNullOrEmpty($User)) { "" } Else { $User }
$Description = If ([string]::IsNullOrEmpty($Description)) { "" } Else { $Description }
$VendorProduct = If ([string]::IsNullOrEmpty($VendorProduct)) { "" } Else { $VendorProduct }
$EventTimeStamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ssK"
$Delay = 100
For ($i = 0; $i -lt 30; $i++) {
try {
$logLine = "{{timestamp=""{0}"" dest=""{1}"" description=""{2}"" service=""{3}"" service_id=""{4}""" `
+ "service_name=""{5}"" start_mode=""{6}"" vendor_product=""{7}"" user=""{8}"" status=""{9}""}}"
$logLine -f $EventTimeStamp, $env:ComputerName, $Description, $ServiceFullName, $ServiceId, $ServiceFriendlyName, $StartMode, $VendorProduct, $User, $Status | Add-Content $LogFile -ErrorAction Stop
break;
}
catch {
Start-Sleep -Milliseconds $Delay
}
if ($i -eq 29) {
Write-Error "Alert logger failed to log, likely due to Splunk holding the file, check eventlog for details." -ErrorAction Continue
if ([System.Diagnostics.EventLog]::SourceExists("SDOLiveScripts") -eq $False) {
Write-Host "Doesn't exist"
New-EventLog -LogName Application -Source "SDOLiveScripts"
}
Write-EventLog -LogName "Application" -Source "SDOLiveScripts" `
-EventID 1337 `
-EntryType Error `
-Message "Failed to log to file $_.Exception.InnerException.Message" `
-ErrorAction Continue
}
}
}
Export-ModuleMember -Function LoggerEndpointService
If anyone could help that'd be great, thank you!
As mentioned in the comments, PowerShell Jobs execute in separate processes and you can't share live objects across process boundaries.
By the time the job executes, $Logger.generalLog is no longer a reference to the scriptblock registered as the Logger function in the calling process - it's just a string, containing the definition of the source function.
You can re-create it from the source code:
$actualLogger = [scriptblock]::Create($Logger.generalLog)
or, in your case, to recreate all of them:
#($Logger.Keys) |ForEach-Object { $Logger[$_] = [scriptblock]::Create($Logger[$_]) }
This will only work if the logging functions are completely independent of their environment - any references to variables in the calling scope or belonging to the source module will fail to resolve!

Get fail message from a function on powershell script

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.

Best way to terminate a PowerShell function based on parameters

I have a few functions that get called either from Jenkins as part of a pipeline, they also get called from a pester test or lastly they can get called from the powershell console. The issue I have really stems from Jenkins not seeming to handle write-output in the way I think it should.
So what I am doing is creating a Boolean param that will allow my to choose if I terminate my function with a exit code or a return message. The exit code will be used by my pipeline logic and the return message for the rest ?
Is there a alternate approach I should be using this seems to be a bit of a hack.
function Get-ServerPowerState
{
[CmdletBinding()]
param
(
[string[]]$ilo_ip,
[ValidateSet('ON', 'OFF')]
[string]$Status,
[boolean]$fail
)
BEGIN
{
$here = Split-Path -Parent $Script:MyInvocation.MyCommand.Path
$Credentials = IMPORT-CLIXML "$($here)\Lib\iLOCred.xml"
}
PROCESS
{
foreach ($ip in $ilo_ip)
{
New-LogEntry -Message ("Getting current powerstate " + $ip)
If (Test-Connection -ComputerName $ip.ToString() -Count 1 -Quiet)
{
$hostPower = Get-HPiLOhostpower -Server $ip -Credential
$Credentials -DisableCertificateAuthentication
}
}
}
END
{
If($fail){
New-LogEntry -Message "Script been set to fail with exit code" -Log Verbose
New-LogEntry -Message "The host is powered - $($HostPower.Host_Power)" -Log Verbose
If($hostPower.HOST_POWER -match $Status)
{
Exit 0
}
else {
Exit 1
}
}
else {
New-LogEntry -Message "Script been set to NOT fail with exit code" -Log Verbose
New-LogEntry -Message "The host is powered - $($HostPower.Host_Power)" -Log Verbose
If($hostPower.HOST_POWER -match $Status)
{
return 0
}
else {
return 1
}
}
}
}
Like this
function Get-Output {
param ([switch]$asint)
if ($asint) {
return 1
}
else {
write-output 'one'
}
}
Get-Output
Get-Output -asint
If you intend to use the output in the pipeline then use Write-Output. If you intend to only send it to the host process then use Write-Host. I typically use the return keyword if I want to assign a return value to a variable.
[int]$result = Get-Output -asint

Powershell to display Regsvr32 result in console instead of dialog

I've searched but did not find any answer.
The task is register one dll using Powershell ps1, followed by other lines of scripts. I don't want to be interrupted by the dialog, so added the /s parameter. But now the result information is ignored, no matter succeed or fail.
I want the result displayed in console. But how?
Launch regsvr32.exe /s with Start-Process -PassThru and inspect the ExitCode property:
$regsvrp = Start-Process regsvr32.exe -ArgumentList "/s C:\path\to\your.dll" -PassThru
$regsvrp.WaitForExit(5000) # Wait (up to) 5 seconds
if($regsvrp.ExitCode -ne 0)
{
Write-Warning "regsvr32 exited with error $($regsvrp.ExitCode)"
}
Here is a more complete full powershell cmdlet with pipeline support.
function Register-Dll
{
<#
.SYNOPSIS
A function that uses the utility regsvr32.exe utility to register a file
.PARAMETER Path
The file path
.PARAMETER Unregister
when specified, unregisters instead of registers
#>
[CmdletBinding()]
param (
[ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
[Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipeLineByPropertyName=$true)]
[Alias("FullName")]
[string[]]$Path,
[Alias("u")]
[switch]$Unregister
)
begin {
if ($Unregister)
{
$regflag = "-u "
}
else
{
$regflag = ""
}
[int]$NumFailed=0
$RegExitCodes = #{
0="SUCCESS";
1="FAIL_ARGS - Invalid Argument";
2="FAIL_OLE - OleInitialize Failed";
3="FAIL_LOAD - LoadLibrary Failed";
4="FAIL_ENTRY - GetProcAddress failed";
5="FAIL_REG - DllRegisterServer or DllUnregisterServer failed.";
}
}
process {
foreach ($p in $path)
{
try
{
$regsvrp = Start-Process regsvr32.exe -ArgumentList "/s $regflag <code>$p</code>" -Wait -NoNewWindow -PassThru
if($regsvrp.ExitCode -ne 0)
{
$NumFailed++
Write-Error "regsvr32 $regflag for $p exited with error $($regsvrp.ExitCode) - $($RegExitCodes[$regsvrp.ExitCode])"
}
} catch {
$NumFailed++
Write-Error $_.Exception.Message
}
}
}
end {
if ($NumFailed -gt 0)
{
if ($Unregister)
{
$mode = "unregister"
}
else
{
$mode = "register"
}
Write-Error "Failed to $mode $NumFailed dll's, see previous errors for detail"
}
}
}
Usage:
function Register-MyAppDll
{
param(
[Parameter(Mandatory=$true,ParameterSetName="Both")]
[switch]$ReRegister,
[Parameter(Mandatory=$true,ParameterSetName="UnregisterOnly")]
[Alias("u")]
[switch]$UnRegister,
[Parameter(Mandatory=$true,ParameterSetName="RegisterOnly")]
[Alias("r")]
[switch]$Register
)
$RegOptions = #()
if ($UnRegister -or $ReRegister) { $RegOptions += #{Unregister=$true} }
if ($Register -or $ReRegister) { $RegOptions += #{} }
$dlltoregister = Get-ChildItem "C:\MyApp\bin" -Filter *.dll | where {$_ -notmatch '^interop'}
foreach ($RegOpt in $RegOptions)
{
$dlltoregister | Register-Dll #RegOpt
}
}
Register-MyAppDll -UnRegister
Register-MyAppDll -Register
Register-MyAppDll -ReRegister
Enjoy :)
Thank you Justin! I'm using this script and it works great.
There seems to be a typo in the following line of code:
$regsvrp = Start-Process regsvr32.exe -ArgumentList "/s $regflag <code>$p</code>" -Wait -NoNewWindow -PassThru
The code tag shoudn't be there. I changed it to the following with added escaped double quotes around path to support spaces in paths:
$regsvrp = Start-Process regsvr32.exe -ArgumentList "/s $regflag `"$p`"" -Wait -NoNewWindow -PassThru

Is it possible to send 'out of band data' from a remote Invoke-Command call?

I have a system in which I'm remoting into a single machine at a time and running commands, scripts, etc. It would be useful to be able to effectively return log messages from the remote script in "realtime". Some code to get an idea of what I'm trying to do.
Note that both the local Log-*Msg functions log to a database (and tee to standard out/err as appropriate). Also note that we have analogous Log-*Msg methods on the remote side (loaded from a module) that are meant to pitched back across the wire and recorded in the DB as if the local Log-*Msg function was called.
Local Methods
function Exec-Remote {
param(
[ValidateNotNull()]
[System.Management.Automation.Runspaces.PSSession]
$Session=$(throw "Session is mandatory ($($MyInvocation.MyCommand))"),
$argumentList,
$scriptBlock
)
if($argumentList -is [scriptblock]) {$scriptBlock = $argumentList}
if($scriptBlock -eq $null) { throw 'Scriptblock is required'}
Invoke-Command -Session $Session -ArgumentList $argumentList -scriptBlock $scriptBlock | Filter-RemoteLogs
}
Filter Filter-RemoteLogs {
if($_ -isnot [string]) { return $_ }
if($_.StartsWith('Log-VerboseMsg:')) {
Log-VerboseMsg $_.Replace("Log-VerboseMsg:", "") | Out-Null
return
}
if($_.StartsWith('Log-WarningMsg:')) {
Log-WarningMsg $_.Replace("Log-WarningMsg:", "") | Out-Null
return
}
if($_.StartsWith('Log-UserMsg:')) {
Log-UserMsg $_.Replace("Log-UserMsg:", "") | Out-Null
return
}
else { return $_ }
}
Example Remote Method
On the remote side I have a module that gets loaded with a few logging functions, here's one such function:
function Log-VerboseMsg {
param([ValidateNotNullOrEmpty()] $msg)
"Log-VerboseMsg:$msg"
}
For the most part it works, I can do the following
$val = Exec-Remote -Session $PSSession {
Log-VerboseMsg 'A test log message!'
return $true
}
And have it do the right thing transparently.
However, it fails in the following scenario.
$val = Exec-Remote -Session $PSSession {
function Test-Logging {
Log-VerboseMsg 'A test log message!'
return $true
}
$aVariable = Test-Logging
Do-ALongRunningOperation
return $aVariable
}
The above will not return anything until the 'long running operation' completes.
My question to you is the following.
Is there a way for me to reliably do this in Powershell? In some form, if the approach I'm using is really that terrible, feel free to lambast me and explain why.
NOTE: connecting to the DB from the remote environment and recording the log messages will not always be possible, so while that approach could work, for my specific needs it isn't sufficient.
In PowerShell v5 you can use new information stream for this. You should modify local functions as following:
function Exec-Remote {
param(
[ValidateNotNull()]
[System.Management.Automation.Runspaces.PSSession]
$Session=$(throw "Session is mandatory ($($MyInvocation.MyCommand))"),
$argumentList,
$scriptBlock
)
if($argumentList -is [scriptblock]) {$scriptBlock = $argumentList}
if($scriptBlock -eq $null) { throw 'Scriptblock is required'}
# 6>&1 will redirect information stream to output, so Filter-RemoteLogs can process it.
Invoke-Command -Session $Session -ArgumentList $argumentList -scriptBlock $scriptBlock 6>&1 | Filter-RemoteLogs
}
Filter Filter-RemoteLogs {
# Function should be advanced, so we can call $PSCmdlet.WriteInformation.
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[PSObject]$InputObject
)
if(
# If it is InformationRecord.
($InputObject -is [Management.Automation.InformationRecord]) -and
# And if it come from informational steam.
($WriteInformationStream=$InputObject.PSObject.Properties['WriteInformationStream']) -and
($WriteInformationStream.Value)
) {
# If it is our InformationRecord.
if($InputObject.Tags-contains'MyLoggingInfomation') {
# Write it to log.
&"Log-$($InputObject.MessageData.LogType)Msg" $InputObject.MessageData.Message | Out-Null
} else {
# Return not our InformationRecord to informational stream.
$PSCmdlet.WriteInformation($InputObject)
}
} else {
# Return other objects to output stream.
$PSCmdlet.WriteObject($InputObject)
}
}
And remote logging functions should write to information stream:
function Log-VerboseMsg {
param([ValidateNotNullOrEmpty()] $msg)
Write-Information ([PSCustomObject]#{Message=$msg;LogType='Verbose'}) MyLoggingInfomation
}
function Log-WarningMsg {
param([ValidateNotNullOrEmpty()] $msg)
Write-Information ([PSCustomObject]#{Message=$msg;LogType='Warning'}) MyLoggingInfomation
}
function Log-UserMsg {
param([ValidateNotNullOrEmpty()] $msg)
Write-Information ([PSCustomObject]#{Message=$msg;LogType='User'}) MyLoggingInfomation
}