Function Write-Log
{
<#
.SYNOPSIS
Log to a file in a format that can be read by CMTrace.exe
.DESCRIPTION
Write a line of data to a script log file in a format that can be parsed by CMTrace.exe.
Warnings will be highlighted in yellow. Errors are highlighted in red.
.PARAMETER LogFileDirectory
Set the directory where the log file will be saved. Default is $env:PROGRAMDATA\Logs.
.PARAMETER LogFileName
Set the name of the log file, default is the name of the script.
.PARAMETER ScriptSection
The heading for the portion of the script that is being executed
Use $script:ScriptSection = 'Section Name' to control the value of this variable throughout the calling script
.PARAMETER Message
The message you wish to write to the log file.
.PARAMETER Component
The source of the message. This should be the name of the function where the message is being generated.
.PARAMETER Severity
Set the severity of the logged line.
Options:
1 = Information
2 = Warning (Highlighed in yellow)
3 = Error (Highlighted in red)
.PARAMETER Thread
The thread which generated the message. Default is 1.
.PARAMETER WriteConsole
Determine whether the log entry should be written to the console.
.PARAMETER MaxLogFileSizeMB
Maximum size limit for log file in megabytes. Default is 10 MB.
.EXAMPLE
$script:ScriptSection = "Install Patch"
Write-Log -Message "Installing patch MS15-031" -Component 'Add-Patch' -Severity 1
Write-Log -Message "Installation of patch MS15-031 failed" -Component 'Add-Patch' -Severity 3
.NOTES
The tools to view the log:
CMTrace.exe is a log file viewer that comes with SCCM 2012.
CMTrace.exe - Installation directory on Configuration Manager 2012 Site Server - <Install Directory>\tools\
If you have the SCCM 2012 client tools installed, CMTrace.exe can also be found here:
C:\Program Files (x86)\ConfigMgr 2012 Toolkit R2\ClientTools\CMTrace.exe
#>
#Define and validate parameters
[CmdletBinding()]
Param
(
# Set the directory where the log file will be saved
[Parameter(Mandatory=$false)]
[ValidateNotNullorEmpty()]
[string]$LogFileDirectory = "$env:PROGRAMDATA\Logs",
# Set the name of the log file, default is the name of the script
[Parameter(Mandatory=$false)]
[ValidateNotNullorEmpty()]
[string]$LogFileName = [System.IO.Path]::GetFileNameWithoutExtension($script:MyInvocation.MyCommand.Definition) + '.log',
# The information to log
[Parameter(Mandatory=$true)]
[string]$Message,
# The heading for the portion of the script that is being executed
# Use $script:ScriptSection = 'Section Name' to control the value of this variable throughout the calling script
[Parameter(Mandatory=$false)]
[ValidateNotNullorEmpty()]
[string]$ScriptSection = $script:ScriptSection,
# The source of the message
[Parameter(Mandatory=$true)]
[ValidateNotNullorEmpty()]
[string]$Component,
# The severity (1=Information, 2=Warning, 3=Error)
[Parameter(Mandatory=$true)]
[ValidateNotNullorEmpty()]
[ValidateRange(1,3)]
[int16]$Severity,
# The thread which generated the message
[Parameter(Mandatory=$false)]
[ValidateNotNullorEmpty()]
[ValidateRange(0,65535)]
[int32]$Thread = 1,
# Write log entry to console?
[Parameter(Mandatory=$false)]
[switch]$WriteConsole,
# Maximum size limit for log file in megabytes
[Parameter(Mandatory=$false)]
[ValidateNotNullorEmpty()]
[int32]$MaxLogFileSizeMB = 10
)
Begin
{
# Get the name of this function
${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
# Get the parameters that this function was invoked with
$PSParameters = New-Object -TypeName PSObject -Property $PSBoundParameters
# Test to see if variable holding path to log file exists
$LogFileVariableExists = Test-Path 'variable:script:OutLogFile'
If (-not $LogFileVariableExists)
{
# Create the directory where the log file will be saved
If (!(Test-Path $LogFileDirectory))
{
# Create the log file directory if it does not exist
Try
{
New-Item -Path $LogFileDirectory -Type Directory | Out-Null
}
Catch
{
# Not able to create log file directory
Throw "${CmdletName} Function :: Failed to successfully create the directory for the log file. Function invoked with parameters [$PSParameters]"
}
}
# Assemble the fully qualified path to the log file
$LogFilePath = $LogFileDirectory + "\" + $LogFileName
# Set variable so that this "if block" is not executed again during the execution of this script
Set-Variable -Name 'OutLogFile' -Value $LogFilePath -Scope 'Script'
}
# The heading for the portion of the script that is being executed
# Use $script:ScriptSection = 'Section Name' to control the value of this variable throughout the script
# Test to see if variable holding name of script heading exists and is not $null, if $true, prepended to the $Message string
If ((Test-Path 'variable:script:ScriptSection') -and ((Get-Variable -Name 'ScriptSection' -Scope 'script' -Value) -ne $null))
{
$Message = $script:ScriptSection + ' :: ' + $Message
}
# Create the log entry
Try
{
$LogLine = "<![LOG[$Message]LOG]!>" +`
"<time=`"$(Get-Date -Format HH:mm:ss).000+0`" " +`
"date=`"$(Get-Date -Format M-d-yyyy)`" " +`
"component=`"$Component`" " +`
"context=`"`" " +`
"type=`"$Severity`" " +`
"thread=`"$Thread`" " +`
"file=`"`">"
}
Catch
{
Throw "${CmdletName} Function :: Failed to successfully generate the log entry for writing to the log file."
}
}
Process
{
Try
{
# Check to see if log file already exists
If (Test-Path $OutLogFile)
{
# Check if log file size is bigger than max log file size
If (((Get-ChildItem $OutLogFile).Length/1MB) -gt $MaxLogFileSizeMB)
{
# Archive existing log file and create new file
$LogFileArchiveDate = Get-Date -Format "MM-dd-yyyy_HH-mm-ss"
$ArchivedOutLogFile = $OutLogFile.Replace(".log", "_ARCHIVED_$LogFileArchiveDate.log")
# If renaming of file fails, script will continue writing to log file even if size goes over the max file size
Rename-Item -Path $OutLogFile -NewName $ArchivedOutLogFile -ErrorAction 'SilentlyContinue'
}
}
# Write the log entry to the log file
# Out-File is the preferred method of writing the log file over Add-Content because
# it will not read/write lock the file so that other applications can read the log file at the same time
Try
{
$LogLine | Out-File -FilePath $OutLogFile -Append -NoClobber -Encoding default -ErrorAction 'SilentlyContinue'
}
Catch
{
# If the log entry fails to write, then wait 5 seconds and try again.
Start-Sleep 5
$LogLine | Out-File -FilePath $OutLogFile -Append -NoClobber -Encoding default -ErrorAction 'SilentlyContinue' -ErrorVariable ErrTest
If ($ErrTest)
{
Write-Host "${CmdletName} Function :: Failed to write to the log file `n$(Write-ErrorStack)" -ForegroundColor Yellow
# If writing log entry to log file failed for a second time, then write message to console
$WriteConsole = $true
}
}
}
Catch
{
Throw "${CmdletName} Function :: Failed writing to the log file."
}
}
End
{
# Write the log entry to the console
If ($WriteConsole)
{
Switch ($Severity)
{
3 { Write-Host $Message -ForegroundColor 'Red' }
2 { Write-Host $Message -ForegroundColor 'Yellow' }
1 { Write-Host $Message }
}
}
}
}
↧
New Post: Write-Log: Create log file which can be read by SCCM 2012's CMTrace.exe tool
↧
New Post: Supersedence Behavior (SCCM2012r2)
Sorry, it doesn't run the installation as the logged in user, it runs it as the SYSTEM account under the currently logged in user session. From my logs:
<snip>
[07-07-2014 13:23:33] [Initialization] Current user is [<domain>\<computername$>]
<snip>
[07-07-2014 13:23:33] [Initialization] Session 0 not detected.
[07-07-2014 13:23:33] [Initialization] Installation is running in [Interactive] mode.
Dan
<snip>
[07-07-2014 13:23:33] [Initialization] Current user is [<domain>\<computername$>]
<snip>
[07-07-2014 13:23:33] [Initialization] Session 0 not detected.
[07-07-2014 13:23:33] [Initialization] Installation is running in [Interactive] mode.
Dan
↧
↧
New Post: Supersedence Behavior (SCCM2012r2)
Hey guys,
we now discovered another weird thing.
If you run the script in SYSTEM-context (using PSExec or SCCM) each function from Deploy-Application.ps1 would run two times.
Reproduce steps:
adding the following code in Deploy-Application.ps1:
[09-07-2014 17:08:14] [Installation] Test
[09-07-2014 17:08:14] [Post-Installation] Oracle_JavaRuntime_1.07.060-351__01 Installation completed with exit code [0].
[09-07-2014 17:08:14] [Post-Installation] ----------------------------------------------------------------------------------------------------------
[09-07-2014 17:08:18] [Initialization] Execution completed with return code 0.
[09-07-2014 17:08:18] [Initialization] ServiceUI: [winlogon.exe] Session: [1] PID [592] [Target Session [1] = Match]
[09-07-2014 17:08:18] [Initialization] ServiceUI: Program to launch : [C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe]
[09-07-2014 17:08:18] [Initialization] ServiceUI: Command line : [C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -NoProfile -WindowStyle Hidden -File D:#app\deploy-application.ps1]
[09-07-2014 17:08:18] [Initialization] ServiceUI: Process launching with PID [3604]
[09-07-2014 17:08:18] [Initialization] ServiceUI: Process exit code [0]
[09-07-2014 17:08:18] [Initialization] ServiceUI returned exit code [0]
[09-07-2014 17:08:18] [Pre-Installation] Spinning up Progress Dialog in a separate thread with message: [Installation im Gange. Bitte warten...]
[09-07-2014 17:08:20] [Installation] Executing [msiexec.exe /i "D:#app\Files\jre1.7.0_60.msi" TRANSFORMS="oracle-javaruntime-32Bit-1.07.060-351-01.mst" REBOOT=ReallySuppress /QN /Lv "C:\WINDOWS\MSILogs\351-01_Install.log"]...
[09-07-2014 17:08:20] [Installation] Working Directory is [D:#app\Files]
[09-07-2014 17:08:28] [Installation] Execution completed successfully with return code 0.
[09-07-2014 17:08:28] [Installation] Test
Thus every function would called two times. Of course not only Write-Log, even "Execute-MSI" and this maybe crashes the installation.
Reason:
if you run Deploy-Application.ps1 through psexec (or SCCM) the AppDeployToolkitMain.ps1 (Script-Body) would be called through following line(Deploy-Application.ps1 ):
Can u understand it and fix it ? :)
Thanks,
TorstenM and MarcoMMA
we now discovered another weird thing.
If you run the script in SYSTEM-context (using PSExec or SCCM) each function from Deploy-Application.ps1 would run two times.
Reproduce steps:
adding the following code in Deploy-Application.ps1:
#*===============================================
#* INSTALLATION
$installPhase = "Installation"
#*===============================================
# Perform installation tasks here
Write-Log -text "Test"
#*===============================================
Then you can look at the created Log and then u will see that the above line will be logged two times:[09-07-2014 17:08:14] [Installation] Test
[09-07-2014 17:08:14] [Post-Installation] Oracle_JavaRuntime_1.07.060-351__01 Installation completed with exit code [0].
[09-07-2014 17:08:14] [Post-Installation] ----------------------------------------------------------------------------------------------------------
[09-07-2014 17:08:18] [Initialization] Execution completed with return code 0.
[09-07-2014 17:08:18] [Initialization] ServiceUI: [winlogon.exe] Session: [1] PID [592] [Target Session [1] = Match]
[09-07-2014 17:08:18] [Initialization] ServiceUI: Program to launch : [C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe]
[09-07-2014 17:08:18] [Initialization] ServiceUI: Command line : [C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -NoProfile -WindowStyle Hidden -File D:#app\deploy-application.ps1]
[09-07-2014 17:08:18] [Initialization] ServiceUI: Process launching with PID [3604]
[09-07-2014 17:08:18] [Initialization] ServiceUI: Process exit code [0]
[09-07-2014 17:08:18] [Initialization] ServiceUI returned exit code [0]
[09-07-2014 17:08:18] [Pre-Installation] Spinning up Progress Dialog in a separate thread with message: [Installation im Gange. Bitte warten...]
[09-07-2014 17:08:20] [Installation] Executing [msiexec.exe /i "D:#app\Files\jre1.7.0_60.msi" TRANSFORMS="oracle-javaruntime-32Bit-1.07.060-351-01.mst" REBOOT=ReallySuppress /QN /Lv "C:\WINDOWS\MSILogs\351-01_Install.log"]...
[09-07-2014 17:08:20] [Installation] Working Directory is [D:#app\Files]
[09-07-2014 17:08:28] [Installation] Execution completed successfully with return code 0.
[09-07-2014 17:08:28] [Installation] Test
Thus every function would called two times. Of course not only Write-Log, even "Execute-MSI" and this maybe crashes the installation.
Reason:
if you run Deploy-Application.ps1 through psexec (or SCCM) the AppDeployToolkitMain.ps1 (Script-Body) would be called through following line(Deploy-Application.ps1 ):
# Dot source the App Deploy Toolkit Functions
."$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1"
In the Script-Body of the Main the Part of the ServiceUI.exe would call the InvokingScript (line 4495 ff.): $serviceUIArguments = "$PSHOME\powershell.exe -ExecutionPolicy Bypass -NoProfile -WindowStyle Hidden -File `"$invokingScript`""
And so the Deploy-Application runs a second time, now under the ServiceUI (User)-Context. Can u understand it and fix it ? :)
Thanks,
TorstenM and MarcoMMA
↧
Commented Unassigned: Feature Request: -LogOnly switch for Write-Log [66]
Can a -LogOnly switch for Write-Log be added at some point? Or is there another way already?
We'd like to log some things straight to the log to not overwhelm the user at the screen.
Thanks
That_annoying_guy
(BTW: The zipped log files feature is growing on me. I'm trying to sell it to the rest of the group)
Comments: Check out the logging function I posted here: https://psappdeploytoolkit.codeplex.com/discussions/550993
We'd like to log some things straight to the log to not overwhelm the user at the screen.
Thanks
That_annoying_guy
(BTW: The zipped log files feature is growing on me. I'm trying to sell it to the rest of the group)
Comments: Check out the logging function I posted here: https://psappdeploytoolkit.codeplex.com/discussions/550993
↧
New Post: Get-IniValue: Updated function to check for custom Add-Type before adding to avoid errors
If you are like me, you have one functions library and use it for all of your scripts. If you are calling one script from another or executing multiple scripts in the same PowerShell console, you will get Add-Type errors if you try to use a function that adds a custom type. The error states that the custom type already exists and cannot be added again.
I have updated the Get-IniValue function to avoid this problem. I am also using a custom logging function I wrote which is posted in another thread in this forum.
I have updated the Get-IniValue function to avoid this problem. I am also using a custom logging function I wrote which is posted in another thread in this forum.
Function Get-IniValue
{
<#
.SYNOPSIS
Parses an ini file and returns the value of the specified section and key
.DESCRIPTION
Parses an ini file and returns the value of the specified section and key
.EXAMPLE
Get-IniValue -FilePath "$envProgramFilesX86\IBM\Notes\notes.ini" -Section "Notes" -Key "KeyFileName"
.PARAMETER FilePath
Path to the ini file
.PARAMETER Section
Section within the ini file
.PARAMETER Key
Key within the section of the ini file
.PARAMETER ContinueOnError
Continue if an error is encountered
.NOTES
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true)]
[string]$FilePath,
[string]$Section,
[string]$Key,
[boolean]$ContinueOnError = $false
)
Begin
{
${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
$PSParameters = New-Object -TypeName PSObject -Property $PSBoundParameters
Write-Log -Message "Function Start" -Component ${CmdletName} -Severity 1
If (-not [string]::IsNullOrEmpty($PSParameters))
{
Write-Log -Message "Function invoked with bound parameters [$PSParameters]" -Component ${CmdletName} -Severity 1
}
Else
{
Write-Log -Message "Function invoked without any bound parameters" -Component ${CmdletName} -Severity 1
}
$source = @"
using System;
using System.Text;
using System.Runtime.InteropServices;
namespace GetIniFile
{
public sealed class GetIniValue
{
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
string lpAppName,
string lpKeyName,
string lpDefault,
StringBuilder lpReturnedString,
uint nSize,
string lpFileName);
}
}
"@
If (-not ([System.Management.Automation.PSTypeName]'GetIniFile.GetIniValue').Type)
{
Add-Type -TypeDefinition $source -Language CSharp
}
}
Process
{
If (Test-Path $FilePath)
{
$builder = New-Object -TypeName System.Text.StringBuilder -ArgumentList 1024
[GetIniFile.GetIniValue]::GetPrivateProfileString($Section, $Key, "", $builder, $builder.Capacity, $FilePath) | Out-Null
$inivalue = $builder.ToString()
Write-Log -Message "Read INI :: Section: $Section, Key: $Key, Value: $inivalue" -Component ${CmdletName} -Severity 1
Return $inivalue
}
Else
{
If ($ContinueOnError -eq $true)
{
Write-Log -Message "File [$filePath] could not be found." -Component ${CmdletName} -Severity 3
Continue
}
Else
{
Write-Log -Message "File [$filePath] could not be found." -Component ${CmdletName} -Severity 3
Exit
}
}
}
End
{
Write-Log -Message "Function End" -Component ${CmdletName} -Severity 1
}
}
↧
↧
New Post: Set-IniValue: Updated function to check for custom Add-Type before adding to avoid errors
If you are like me, you have one functions library and use it for all of your scripts. If you are calling one script from another or executing multiple scripts in the same PowerShell console, you will get Add-Type errors if you try to use a function that adds a custom type. The error states that the custom type already exists and cannot be added again.
I have updated the Set-IniValue function to avoid this problem. I am also using a custom logging function I wrote which is posted in another thread in this forum.
I have updated the Set-IniValue function to avoid this problem. I am also using a custom logging function I wrote which is posted in another thread in this forum.
Function Set-IniValue
{
<#
.SYNOPSIS
Opens an ini file and sets the value of the specified section and key
.DESCRIPTION
Opens an ini file and sets the value of the specified section and key
.EXAMPLE
Set-IniValue -FilePath "$envProgramFilesX86\IBM\Notes\notes.ini" -Section "Notes" -Key "KeyFileName" -Value "MyFile.ID"
.PARAMETER FilePath
Path to the ini file
.PARAMETER Section
Section within the ini file
.PARAMETER Key
Key within the section of the ini file
.PARAMETER Value
Value for the key within the section of the ini file
.PARAMETER ContinueOnError
Continue if an error is encountered
.NOTES
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true)]
[String] $FilePath,
[String] $Section,
[String] $Key,
[String] $Value, #To remove a value, set this variable to $null
[boolean] $ContinueOnError = $false
)
Begin
{
${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
$PSParameters = New-Object -TypeName PSObject -Property $PSBoundParameters
Write-Log -Message "Function Start" -Component ${CmdletName} -Severity 1
If (-not [string]::IsNullOrEmpty($PSParameters))
{
Write-Log -Message "Function invoked with bound parameters [$PSParameters]" -Component ${CmdletName} -Severity 1
}
Else
{
Write-Log -Message "Function invoked without any bound parameters" -Component ${CmdletName} -Severity 1
}
$source = @"
using System;
using System.Text;
using System.Runtime.InteropServices;
namespace SetIniFile
{
public sealed class SetIniValue
{
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool WritePrivateProfileString(
string lpAppName,
string lpKeyName,
StringBuilder lpString, // Don't use string, as Powershell replaces $null with an empty string
string lpFileName);
}
}
"@
If (-not ([System.Management.Automation.PSTypeName]'SetIniFile.SetIniValue').Type)
{
Add-Type -TypeDefinition $source -Language CSharp
}
}
Process
{
If (Test-Path $FilePath)
{
[SetIniFile.SetIniValue]::WritePrivateProfileString($Section, $Key, $Value, $FilePath) | Out-Null
Write-Log -Message "Write INI :: Section: $Section, Key: $Key, Value: $Value" -Component ${CmdletName} -Severity 1
}
Else
{
If ($ContinueOnError -eq $true)
{
Write-Log -Message "File [$filePath] could not be found." -Component ${CmdletName} -Severity 3
Continue
}
Else
{
Write-Log -Message "File [$filePath] could not be found." -Component ${CmdletName} -Severity 3
Exit
}
}
}
End
{
Write-Log -Message "Function End" -Component ${CmdletName} -Severity 1
}
}
↧
New Post: Refresh-Desktop: Updated function to check for custom Add-Type before adding to avoid errors
If you are like me, you have one functions library and use it for all of your scripts. If you are calling one script from another or executing multiple scripts in the same PowerShell console, you will get Add-Type errors if you try to use a function that adds a custom type. The error states that the custom type already exists and cannot be added again.
I have updated the Refresh-Desktop function to avoid this problem. I am also using a custom logging function I wrote which is posted in another thread in this forum. Write-ErrorStack function is also posted in another thread in this forum.
I have updated the Refresh-Desktop function to avoid this problem. I am also using a custom logging function I wrote which is posted in another thread in this forum. Write-ErrorStack function is also posted in another thread in this forum.
Function Refresh-Desktop
{
<#
.SYNOPSIS
Forces the Windows Exporer Shell to refresh, which causes environment variables and desktop icons to be reloaded
.DESCRIPTION
Forces the Windows Exporer Shell to refresh, which causes environment variables and desktop icons to be reloaded.
Informs the Explorer Shell to refresh its settings after you change registry values or other settings to avoid a reboot.
.EXAMPLE
Refresh-Desktop
.PARAMETER ContinueOnError
Continue if an error is encountered
.NOTES
#>
[CmdletBinding()]
Param
(
[boolean] $ContinueOnError = $true
)
Begin
{
${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
$PSParameters = New-Object -TypeName PSObject -Property $PSBoundParameters
Write-Log -Message "Function Start" -Component ${CmdletName} -Severity 1
If (-not [string]::IsNullOrEmpty($PSParameters))
{
Write-Log -Message "Function invoked with bound parameters [$PSParameters]" -Component ${CmdletName} -Severity 1
}
Else
{
Write-Log -Message "Function invoked without any bound parameters" -Component ${CmdletName} -Severity 1
}
$refreshDesktopCode = @'
private static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff);
private const int WM_SETTINGCHANGE = 0x1a;
private const int SMTO_ABORTIFHUNG = 0x0002;
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)] static extern bool SendNotifyMessage(IntPtr hWnd, uint Msg, UIntPtr wParam, IntPtr lParam);
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SendMessageTimeout ( IntPtr hWnd, int Msg, IntPtr wParam, string lParam, uint fuFlags, uint uTimeout, IntPtr lpdwResult );
[System.Runtime.InteropServices.DllImport("Shell32.dll")] private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2);
public static void Refresh()
{
SHChangeNotify(0x8000000, 0x1000, IntPtr.Zero, IntPtr.Zero);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, null, SMTO_ABORTIFHUNG, 100, IntPtr.Zero);
}
'@
If (-not ([System.Management.Automation.PSTypeName]'MyWinAPI.Explorer').Type)
{
Add-Type -MemberDefinition $refreshDesktopCode -Namespace MyWinAPI -Name Explorer
}
}
Process
{
Write-Log -Message "Refreshing desktop and environment variables for explorer" -Component ${CmdletName} -Severity 1
Try
{
[MyWinAPI.Explorer]::Refresh()
}
Catch
{
Write-Log -Message "Failed to refresh desktop and environment variables for explorer `n$(Write-ErrorStack)" -Component ${CmdletName} -Severity 3
If ($ContinueOnError -eq $true)
{
Continue
}
Else
{
Exit
}
}
}
End
{
Write-Log -Message "Function End" -Component ${CmdletName} -Severity 1
}
}
↧
New Post: Send-Keys: Send a sequence of keys to an application window based on Window Title
I am using a custom logging function posted on another thread in this forum. Write-ErrorStack function is also posted to another thread in this forum.
Function Send-Keys
{
<#
.SYNOPSIS
Send a sequence of keys to an application window
.DESCRIPTION
This Send-Keys script send a sequence of keys to an application window.
.PARAMETER WindowTitle
The title of the application window. This can be a partial title.
.PARAMETER Keys
The sequence of keys to send
.PARAMETER WaitSeconds
An optional number of seconds to wait after the sending of the keys
.EXAMPLE
Send-Keys "foobar - Notepad" "Hello world"
Send the sequence of keys "Hello world" to the application titled "foobar - Notepad".
.EXAMPLE
Send-Keys "foobar - Notepad" "Hello world" -WaitSeconds 5
Send the sequence of keys "Hello world" to the application titled "foobar - Notepad" and wait 5 seconds.
.EXAMPLE
New-Item foobar.txt -ItemType File
notepad foobar.txt
Send-Keys "foobar - Notepad" "Hello world{ENTER}Ciao mondo{ENTER}" -WaitSeconds 1
Send-Keys "foobar - Notepad" "^s"
This command sequence creates a new text file called foobar.txt, opens the file using notepad.exe,
writes some text, and saves the file using notepad.
.LINK
http://msdn.microsoft.com/en-us/library/System.Windows.Forms.SendKeys(v=vs.100).aspx
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true,Position=1)]
[string]$WindowTitle,
[Parameter(Mandatory=$true,Position=2)]
[string]$Keys,
[Parameter(Mandatory=$false)]
[int]$WaitSeconds
)
Begin
{
${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
$PSParameters = New-Object -TypeName PSObject -Property $PSBoundParameters
Write-Log -Message "Function Start" -Component ${CmdletName} -Severity 1
If (-not [string]::IsNullOrEmpty($PSParameters))
{
Write-Log -Message "Function invoked with bound parameters [$PSParameters]" -Component ${CmdletName} -Severity 1
}
Else
{
Write-Log -Message "Function invoked without any bound parameters" -Component ${CmdletName} -Severity 1
}
}
Process
{
Try
{
# Load assembly containing class System.Windows.Forms.SendKeys
Add-Type -AssemblyName System.Windows.Forms -ErrorAction 'Stop'
# Add a C# class to access the WIN32 API SetForegroundWindow
$SetForegroundWindow = @"
using System;
using System.Runtime.InteropServices;
public class WINDOW
{
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetForegroundWindow(IntPtr hWnd);
}
"@
If (-not ([System.Management.Automation.PSTypeName]'WINDOW').Type)
{
Add-Type $SetForegroundWindow -ErrorAction 'Stop'
}
# Get the process with the specified window title
$Process = Get-Process -ErrorAction 'Stop' | Where-Object { $_.MainWindowTitle.Contains($WindowTitle) }
If ($Process)
{
# Get the window handle of the first process only if there is more than one process returned
$ProcessHandle = $Process[0].MainWindowHandle
# Bring the process to the foreground
$ActivateWindow = [WINDOW]::SetForegroundWindow($ProcessHandle)
# Send the Key sequence
# Info on Key input at: http://msdn.microsoft.com/en-us/library/System.Windows.Forms.SendKeys(v=vs.100).aspx
If ($ActivateWindow)
{
[System.Windows.Forms.SendKeys]::SendWait($Keys)
}
Else
{
# Failed to bring the window to the foreground. Do nothing.
}
If ($WaitSeconds)
{
Start-Sleep -Seconds $WaitSeconds
}
}
}
Catch
{
Write-Log -Message "Failed to Send Keys `n$(Write-ErrorStack)" -Component ${CmdletName} -Severity 3
Exit
}
}
End
{
Write-Log -Message "Function End" -Component ${CmdletName} -Severity 1
}
}
↧
New Post: Invoke-TSQLCmd: Execute queries against a SQL database and get back results
I am using a custom logging function posted on another thread in this forum. Write-ErrorStack function is also posted to another thread in this forum.
Function Invoke-TSQLCmd
{
<#
.SYNOPSIS
Runs a T-SQL script. Invoke-TSQLCmd can handle T-SQL script with multiple GO statements. Do not add GO for the very last GO block.
.DESCRIPTION
Runs a T-SQL script. Invoke-TSQLCmd returns message output, such as the output of PRINT and RAISERROR.
Paramaterized queries are supported.
.INPUTS
None
You cannot pipe objects to this function.
.OUTPUTS
System.Data.DataTable
.EXAMPLE
Invoke-TSQLCmd -ServerInstance "MyComputer\MyInstance" -Query "SELECT login_time AS 'StartTime' FROM sysprocesses WHERE spid = 1"
This example connects to a named instance of the Database Engine on a computer and runs a basic T-SQL query.
StartTime
-----------
2010-08-12 21:21:03.593
.EXAMPLE
Invoke-TSQLCmd -ServerInstance "MyComputer\MyInstance" -InputFile "C:\MyFolder\tsqlscript.sql" | Out-File -filePath "C:\MyFolder\tsqlscript.rpt"
This example reads a file containing T-SQL statements, runs the file, and writes the output to another file.
.NOTES
#>
[CmdletBinding(DefaultParameterSetName='Query')]
Param
(
[Parameter(Position=0, Mandatory=$true)]
[ValidateNotNullorEmpty()]
[string]$ServerInstance,
[Parameter(Position=1, Mandatory=$false)]
[ValidateNotNullorEmpty()]
[string]$Database,
# If you pass a query with multiple GO statements, do not add GO to the very last statement.
[Parameter(Position=2, Mandatory=$true, ParameterSetName="Query")]
[ValidateNotNullorEmpty()]
[string]$Query,
# If you pass a query with multiple GO statements, do not add GO to the very last statement.
[Parameter(Position=2, Mandatory=$true, ParameterSetName="File")]
[ValidateNotNullorEmpty()]
[ValidateScript({Test-Path $_})]
[string]$InputFile,
[Parameter(Position=3, Mandatory=$false)]
[ValidateNotNullorEmpty()]
[string]$Username,
[Parameter(Position=4, Mandatory=$false)]
[ValidateNotNullorEmpty()]
[string]$Password,
# The time (in seconds) to wait for the command to execute
[Parameter(Position=5, Mandatory=$false)]
[ValidateNotNullorEmpty()]
[Int32]$QueryTimeout=120,
# The time (in seconds) to wait for a connection to open
[Parameter(Position=6, Mandatory=$false)]
[ValidateNotNullorEmpty()]
[Int32]$ConnectionTimeout=120,
[Parameter(Position=7, Mandatory=$false)]
[ValidateSet("DataSet", "DataTables", "DataRows","SingleValue")]
[string]$OutputFormat="DataRows",
[Parameter(Position=8, Mandatory=$false)]
[ValidateNotNullorEmpty()]
[System.Collections.IDictionary]$SqlParameters
)
Begin
{
Function Get-SqlBatchesFromString($string)
{
# If you pass a query with multiple GO statements, do not add GO to the very last statement.
$buffer = New-Object System.Text.StringBuilder
$string -Split "`n" | ForEach {
Switch -regex ($_)
{
"^\s*GO[\s\d]*$" { $buffer.ToString(); $buffer.Length = 0 }
default { $temp = $buffer.AppendLine($_) }
}
}
$buffer.ToString()
}
${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
$PSParameters = New-Object -TypeName PSObject -Property $PSBoundParameters
Write-Log -Message "Function Start" -Component ${CmdletName} -Severity 1
If (-not [string]::IsNullOrEmpty($PSParameters))
{
Write-Log -Message "Function invoked with bound parameters [$PSParameters]" -Component ${CmdletName} -Severity 1
}
Else
{
Write-Log -Message "Function invoked without any bound parameters" -Component ${CmdletName} -Severity 1
}
}
Process
{
Try
{
If ($InputFile)
{
$FilePath = $(Resolve-Path $InputFile).Path
[string]$Query = [System.IO.File]::ReadAllText("$FilePath")
}
$SQLConnection = New-Object System.Data.SqlClient.SQLConnection
If ($Username)
{
$ConnectionString = "Server={0};Database={1};User ID={2};Password={3};Trusted_Connection=False;Connect Timeout={4}" -f $ServerInstance,$Database,$Username,$Password,$ConnectionTimeout
}
Else
{
$ConnectionString = "Server={0};Database={1};Integrated Security=True;Connect Timeout={2}" -f $ServerInstance,$Database,$ConnectionTimeout
}
# Set the connection string used to open a connection to the data source
$SQLConnection.ConnectionString = $ConnectionString
# Following EventHandler is used for logging PRINT and RAISERROR T-SQL statements.
# When you set FireInfoMessageEventOnUserErrors to true, errors with a severity level of 16 or below that were previously
# treated as exceptions are now handled as InfoMessage events.
# All events fire immediately and are handled by the event handler.
# If FireInfoMessageEventOnUserErrors is set to false, then InfoMessage events are handled at the end of the procedure.
$SQLConnection.FireInfoMessageEventOnUserErrors = $true
# Define method that will handle InfoMessage events which are fired when SQL commands are processed
# We will log all messages generated by processing of SQL commands
$EventHandler = [System.Data.SqlClient.SqlInfoMessageEventHandler]{Write-Log -Message "SQL InfoMessage: $($_)" -Component ${CmdletName} -Severity 2}
# Add_InfoMessage occurs when SQL Server returns a warning or informational message
$SQLConnection.Add_InfoMessage($EventHandler)
# Open a connection to the data source
Write-Log -Message "Open connection to data source" -Component ${CmdletName} -Severity 1
$SQLConnection.Open()
# Define object for holding the results of the T-SQL statement or stored procedure
$DataSet = New-Object System.Data.DataSet
Get-SqlBatchesFromString $Query | % {
# If you pass a query with multiple GO statements, do not add GO to the very last statement.
If ([string]::IsNullOrEmpty($_))
{
Continue
}
# Configure the T-SQL statement or stored procedure to be executed against the data source
$SQLCmd = New-Object System.Data.SqlClient.SqlCommand($_,$SQLConnection)
# Configure the wait time (in seconds) before terminating the attempt to execute the T-SQL statement or stored procedure
$SQLCmd.CommandTimeout = $QueryTimeout
# Add any specified parameters to the T-SQL command being executed
If ($SqlParameters -ne $null)
{
$SqlParameters.GetEnumerator() |
ForEach-Object
{
If ($_.Value -ne $null)
{
$SQLCmd.Parameters.AddWithValue($_.Key, $_.Value) | Out-Null
}
Else
{
$SQLCmd.Parameters.AddWithValue($_.Key, [DBNull]::Value) | Out-Null
}
}
}
# Define object to represent a set of data commands and a database connection
$DataSource = New-Object System.Data.SqlClient.SqlDataAdapter($SQLCmd)
# Execute the T-SQL statement or stored procedure against the data source and store the results
# The Fill method retrieves the data from the data source using a SELECT statement.
# If a command does not return any rows, no tables are added to the DataSet, and no exception is raised.
# http://msdn.microsoft.com/en-us/library/zxkb3c3d.aspx
Write-Log -Message "Execute T-SQL batch `n$_" -Component ${CmdletName} -Severity 1
[void]$DataSource.Fill($DataSet)
}
# Output results
Switch ($OutputFormat)
{
'DataSet' {
Write-Output ($DataSet)
}
'DataTables' {
Write-Output ($DataSet.Tables | Select-Object -Expand Rows)
}
'DataRows' {
Write-Output ($DataSet.Tables[0])
}
'SingleValue' {
Write-Output ($DataSet.Tables[0] | Select-Object -Expand $DataSet.Tables[0].Columns[0].ColumnName)
}
}
}
Catch
{
Write-Log -Message "Failed to execute T-SQL statement `n$(Write-ErrorStack)" -Component ${CmdletName} -Severity 3
Exit
}
Finally
{
# Close the connection to the data source if it is open and release all resources
If ($SQLConnection -and ($SQLConnection.State -ne [System.Data.ConnectionState]::Closed))
{
Write-Log -Message "Closing connection to data source and releasing all resources" -Component ${CmdletName} -Severity 1
$SQLConnection.Close()
$SQLConnection.Dispose()
}
}
}
End
{
Write-Log -Message "Function End" -Component ${CmdletName} -Severity 1
}
}
↧
↧
New Post: Regarding the SCCM limitation with Applications and "allow user to interact with program installation"
Edit: NVM, someone already found and posted this above :), I guess I scrolled through too fast.
Someone has done a really nice job coding this in PowerShell using C# code.
Here is a blog on the script: http://clymb3r.wordpress.com/2013/11/03/powershell-and-token-impersonation/
Here is the script: https://github.com/clymb3r/PowerShell/tree/master/Invoke-TokenManipulation
I would probably rename his function to something that makes a bit more sense, such as: Invoke-ProcessWithLogonToken
The script will list all available logon token and let you choose which one you want to use to create a new process. For the use case being discussed here, we want to use the logon token of the person that is the console user (one actively using the computer at the moment and not simply logged into another non-active session). I forget the name of the Windows API, but there is one which identifies the session ID of the user that is the active console user. Then you can use this session ID to find the username from a Windows process associated with that session id/user. That lets you know what logon token to use for creating your new process.
I can look up the Windows API for discovering the active console user if there is any interest in developing a PowerShell based solution for this instead of using ServiceUI.exe.
Someone has done a really nice job coding this in PowerShell using C# code.
Here is a blog on the script: http://clymb3r.wordpress.com/2013/11/03/powershell-and-token-impersonation/
Here is the script: https://github.com/clymb3r/PowerShell/tree/master/Invoke-TokenManipulation
I would probably rename his function to something that makes a bit more sense, such as: Invoke-ProcessWithLogonToken
The script will list all available logon token and let you choose which one you want to use to create a new process. For the use case being discussed here, we want to use the logon token of the person that is the console user (one actively using the computer at the moment and not simply logged into another non-active session). I forget the name of the Windows API, but there is one which identifies the session ID of the user that is the active console user. Then you can use this session ID to find the username from a Windows process associated with that session id/user. That lets you know what logon token to use for creating your new process.
I can look up the Windows API for discovering the active console user if there is any interest in developing a PowerShell based solution for this instead of using ServiceUI.exe.
↧
New Post: Regarding the SCCM limitation with Applications and "allow user to interact with program installation"
So I dug around a bit in some old code I had and found the name of the Windows API which gives you the session ID of the console user, this is the user with control of the physical mouse, keyboard, and monitor on the system. I hope this is useful information and not something that has already been figured out.
Win API Function: WTSGetActiveConsoleSessionId
There is some code for it referenced here: http://gallery.technet.microsoft.com/scriptcenter/e8c3af96-db10-45b0-88e3-328f087a8700
Here is a small excerpt:
[DllImport("kernel32.dll", SetLastError = false, CharSet = CharSet.Auto)]
public static extern int WTSGetActiveConsoleSessionId();
Some Notes On Sessions:
In Vista or Later:
The first user to log on gets a Session ID of 1.
Each successive user that logs on gets a Session ID of 2, 3, etc.
A Session ID of 0 is reserved for services.
If XP:
The first user to log on gets a Session ID of 0.
If using Fast User Switching, additional users might get Session IDs of 1, 2, etc.
Services also have a Session ID of 0.
The Console Session (actively logged in user) can dynamically switch based on user switching.
WTSGetActiveConsoleSessionId should always give you the correct Console Session ID at the time of the call.
The windows shell is almost always explorer.exe but during imaging of PCs, the shell can sometimes be different. You can find out what the current Windows shell is from here: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Shell
Then you can use WMI or an API call to get the ProcessOwner, or Domain\Username, of the Windows Shell which matches the Session ID that we got from WTSGetActiveConsoleSessionId. Now we know which user's privilege token to use for creating new processes.
Also, I've used the GetUserNameA function in ADVAPI32.dll to find out if I am in the SYSTEM context or not. It returns "SYSTEM" if the process is running under the SYSTEM account.
Win API Function: WTSGetActiveConsoleSessionId
There is some code for it referenced here: http://gallery.technet.microsoft.com/scriptcenter/e8c3af96-db10-45b0-88e3-328f087a8700
Here is a small excerpt:
[DllImport("kernel32.dll", SetLastError = false, CharSet = CharSet.Auto)]
public static extern int WTSGetActiveConsoleSessionId();
Some Notes On Sessions:
In Vista or Later:
The first user to log on gets a Session ID of 1.
Each successive user that logs on gets a Session ID of 2, 3, etc.
A Session ID of 0 is reserved for services.
If XP:
The first user to log on gets a Session ID of 0.
If using Fast User Switching, additional users might get Session IDs of 1, 2, etc.
Services also have a Session ID of 0.
The Console Session (actively logged in user) can dynamically switch based on user switching.
WTSGetActiveConsoleSessionId should always give you the correct Console Session ID at the time of the call.
The windows shell is almost always explorer.exe but during imaging of PCs, the shell can sometimes be different. You can find out what the current Windows shell is from here: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Shell
Then you can use WMI or an API call to get the ProcessOwner, or Domain\Username, of the Windows Shell which matches the Session ID that we got from WTSGetActiveConsoleSessionId. Now we know which user's privilege token to use for creating new processes.
Also, I've used the GetUserNameA function in ADVAPI32.dll to find out if I am in the SYSTEM context or not. It returns "SYSTEM" if the process is running under the SYSTEM account.
↧
New Post: Start-SleepProgress: Pause script, show countdown timer, progress bar, message, and nicely formatted time strings
Function Start-SleepProgress
{
<#
.SYNOPSIS
Pause script execution for specified time and show a countdown timer with progress bar and message.
.DESCRIPTION
Sleep for a specified amout of time. Show a progress bar with a countdown timer and message.
The countdown timer formats the time so that it is easy to tell time remaining
using the largest time values that makes sense for the amount of time remaining.
.PARAMETER SleepHash
The length of time that script should be paused passed as a hash value.
Time can be specified in any combination of seconds, minutes, or hours.
Examples:
$SleepTime = @{"Seconds"=10}
$SleepTime = @{"Minutes"=10}
$SleepTime = @{"Hours"=10}
$SleepTime = @{"Hours"=1; "Minutes"=10; "Seconds"=30}
.PARAMETER Message
The message you wish to display on the progress message.
Default message is: Please wait for script execution to continue...
.EXAMPLE
$SleepTime = @{"Minutes" = 10}
Start-SleepProgress $SleepTime
$SleepTime = @{"Hours"=1; "Minutes"=10; "Seconds"=30}
Start-SleepProgress $SleepTime
.NOTES
#>
[CmdletBinding()]
Param
(
[parameter(Mandatory=$true)]
[hashtable]$SleepHash,
[parameter(Mandatory=$false)]
[string]$Message = "Please wait for script execution to continue..."
)
Function Get-FormattedTimeString ($Seconds)
{
$TimeSpan = New-TimeSpan -Seconds $Seconds
If (($TimeSpan.Hours -eq 0) -and ($TimeSpan.Minutes -ge 1))
{
$TimeSpanString = '{0:00}m:{1:00}s' -f $TimeSpan.Minutes,$TimeSpan.Seconds
}
ElseIf (($TimeSpan.Hours -eq 0) -and ($TimeSpan.Minutes -eq 0))
{
$TimeSpanString = '{0:00}s' -f $TimeSpan.Seconds
}
ElseIf ($TimeSpan.Hours -ge 1)
{
$TimeSpanString = '{0:00}h:{1:00}m:{2:00}s' -f $TimeSpan.Hours,$TimeSpan.Minutes,$TimeSpan.Seconds
}
Return $TimeSpanString
}
[int]$SleepSeconds = 0
ForEach ($Key in $SleepHash.Keys)
{
Switch($Key)
{
"Seconds" {
$SleepSeconds = $SleepSeconds + $SleepHash.Get_Item($Key)
}
"Minutes" {
$SleepSeconds = $SleepSeconds + ($SleepHash.Get_Item($Key) * 60)
}
"Hours" {
$SleepSeconds = $SleepSeconds + ($SleepHash.Get_Item($Key) * 60 * 60)
}
}
}
$TotalTimeSpanString = Get-FormattedTimeString -Seconds $SleepSeconds
For ($Count=0; $Count -lt $SleepSeconds; $Count++)
{
$CountDownString = Get-FormattedTimeString -Seconds ($SleepSeconds - $Count)
Write-Progress -Activity $Message -Status "Waiting for $TotalTimeSpanString, $CountDownString remaining" -PercentComplete ($Count/$SleepSeconds*100)
Start-Sleep -Seconds 1
}
Write-Progress -Activity "Waiting for $TotalTimeSpanString, $CountDownString remaining" -Completed
}
↧
New Post: Supersedence Behavior (SCCM2012r2)
Hey guys,
we again.
We have now implemented solution which works for us :)
We customized the Deploy-Application.ps1.
We only run the Script-Body from Deploy-Application if:
Thanks,
TorstenM and MarcoMMA
we again.
We have now implemented solution which works for us :)
We customized the Deploy-Application.ps1.
We only run the Script-Body from Deploy-Application if:
- no User is logged on or
- a Tasksequence is running or
-
Session ID =! 0
Thanks,
TorstenM and MarcoMMA
↧
↧
Commented Unassigned: Feature Request: -LogOnly switch for Write-Log [66]
Can a -LogOnly switch for Write-Log be added at some point? Or is there another way already?
We'd like to log some things straight to the log to not overwhelm the user at the screen.
Thanks
That_annoying_guy
(BTW: The zipped log files feature is growing on me. I'm trying to sell it to the rest of the group)
Comments: Wow that a lot of overhead. We don't have SCCM 2012 yet and I don't know if the rest of my group at work cares for CMTrace.exe compatible logs. Lots of people hate(d) Trace32.exe because it's clunky. (I don't care either way) I might still just shim-in a -LogOnly switch in the Write-Log function any way. I'm still hoping for sintaxasn to do it but it's his show. If modding a few things is the price of admission, so be it.
We'd like to log some things straight to the log to not overwhelm the user at the screen.
Thanks
That_annoying_guy
(BTW: The zipped log files feature is growing on me. I'm trying to sell it to the rest of the group)
Comments: Wow that a lot of overhead. We don't have SCCM 2012 yet and I don't know if the rest of my group at work cares for CMTrace.exe compatible logs. Lots of people hate(d) Trace32.exe because it's clunky. (I don't care either way) I might still just shim-in a -LogOnly switch in the Write-Log function any way. I'm still hoping for sintaxasn to do it but it's his show. If modding a few things is the price of admission, so be it.
↧
New Post: Regarding the SCCM limitation with Applications and "allow user to interact with program installation"
Hi,
I was checking this discussion and I think that there could be another way to do this with the CreateProcessAsUserW alternative that Dan suggested:
Do you guys think it's possible to achieve the functionality by proceeding this way?
My company has been using serviceui.exe to allow user interaction on installations. In this case we always have to check the option to run the process as 32 bit. This is what we found so far:
I was checking this discussion and I think that there could be another way to do this with the CreateProcessAsUserW alternative that Dan suggested:
- Always set the deployment type to run without interaction in the SCCM application and setting the option as whether or not a user is logged on.
- Always launch the toolkit under the session 0 running as SYSTEM.
- Inside the toolkit detect whether or not a user is logged on. If a user is logged on set it to interactive mode otherwise no.
- If no interactive mode is enabled the installation will proceed without any prompts under the SYSTEM user.
-
If the interactive mode is enabled then only launch the user interaction part in the user mode, as balloons and dialogs. The process running under the user mode would be responsible to send a response back to the process running as SYSTEM. The installation and any other part that doesn't interact with the user would still run under the SYSTEM process in this case.
Do you guys think it's possible to achieve the functionality by proceeding this way?
My company has been using serviceui.exe to allow user interaction on installations. In this case we always have to check the option to run the process as 32 bit. This is what we found so far:
- We were able to successfully view the process under the user session in all cases only on Windows 7 64 bits.
- If using Windows 7 32 bits serviceui.exe always fails with exit code -1 on the AppEnforce.log.
-
If using Windows XP 32 bits the installation is successful only if the user is directly connected to the session. If connected using remote desktop to Windows XP 32 bits serviceui.exe will also fail with exit code -1 on the AppEnforce.log (we think it's due to this error: http://wishmesh.com/2011/08/createprocessasuser-fails-on-windows-xp-with-system-error-233/).
↧
New Post: Allowing applications to automatically close while using the defer option
Hi,
We are currently planning to adopt the toolkit for deployments and during the discussions we got a request that besides providing the defer option to the users we should also make sure that there is a timeout to automatically close the applications after a certain amount of time and proceed with the installation.
The code I thought it would allow this is:
Would it be possible to have a ForceCloseAppsCountdown in the toolkit that would always display the countdown and close the applications regardless of how many deferrals the user still has?
We want to use this option to ensure that the deployment would be successful if the user is not on his desk at the time of the deployment but he is logged on to the machine.
Anderson Cassimiro
We are currently planning to adopt the toolkit for deployments and during the discussions we got a request that besides providing the defer option to the users we should also make sure that there is a timeout to automatically close the applications after a certain amount of time and proceed with the installation.
The code I thought it would allow this is:
Show-InstallationWelcome -CloseApps "iexplore,firefox,chrome" -AllowDeferCloseApps -CloseAppsCountdown 3600 -BlockExecution -DeferTimes 3 -PersistPrompt
But by default when you still have remaining deferrals the CloseAppsCountdown will be ignored.Would it be possible to have a ForceCloseAppsCountdown in the toolkit that would always display the countdown and close the applications regardless of how many deferrals the user still has?
We want to use this option to ensure that the deployment would be successful if the user is not on his desk at the time of the deployment but he is logged on to the machine.
Anderson Cassimiro
↧
New Post: Supersedence Behavior (SCCM2012r2)
Hmm, have you been using the latest version of Deploy-Application.ps1? There is already code in there to handle this issue. Basically when the main script is dot sourced and ServiceUI is called the exit code is stored in a variable $serviceUIExitCode which is read by Deploy-Application.ps1 directly after the dot-sourcing. If ServiceUI was invoked, the Deploy-Application.ps1 script will exit as we know the code has already been executed (or attempted) via ServiceUI.
So our testing with ServiceUI has proved mysteriously inconsistent, we can't seem to get it to work reliably with the different combinations we have tried. If anyone can find a pattern or an explanation we'd love to hear it. Otherwise I think we need Cameron King from Microsoft to help out or else we need to code this functionality natively in PowerShell (something I don't have time to commit to currently).
Dot source the App Deploy Toolkit Functions
."$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1"Handle ServiceUI invocation
If ($serviceUIExitCode -ne $null) { Exit-Script $serviceUIExitCode }So our testing with ServiceUI has proved mysteriously inconsistent, we can't seem to get it to work reliably with the different combinations we have tried. If anyone can find a pattern or an explanation we'd love to hear it. Otherwise I think we need Cameron King from Microsoft to help out or else we need to code this functionality natively in PowerShell (something I don't have time to commit to currently).
↧
↧
New Post: Regarding the SCCM limitation with Applications and "allow user to interact with program installation"
@mmashwani,
Not sure how much time you have to spend on this, but sounds like you're up to speed on your Win APIs. Sintaxasn had put together some code above on
this, if you were able to build on it with your findings that would be really appreciated!
@Anderson,
We already have the logic around when interaction is needed and when not, the difficulty is in breaking out of session 0 using the logged on user's credentials token to show a UI. We're losing faith in ServiceUI - there's just no documentation or support around it.
Not sure how much time you have to spend on this, but sounds like you're up to speed on your Win APIs. Sintaxasn had put together some code above on
this, if you were able to build on it with your findings that would be really appreciated!
@Anderson,
We already have the logic around when interaction is needed and when not, the difficulty is in breaking out of session 0 using the logged on user's credentials token to show a UI. We're losing faith in ServiceUI - there's just no documentation or support around it.
↧
New Post: Regarding the SCCM limitation with Applications and "allow user to interact with program installation"
@Anderson
The -1 exit codes you are seeing sounds like ServiceUI.exe is not able to detect a logged on user because it's using this API to detects logged on users: WTSGetActiveConsoleSessionId . Why it is not working on Win 7 32 bits seems like a bug in ServiceUI.exe perhaps. See below info:
WTSGetActiveConsoleSessionId should always give you the correct Console Session ID at the time of the call.
This API will also return a -1 if there is no active user logged into the interactive session (that is, logged in locally to the physical machine, as opposed to using RDP).
Also, I think this is the appropriate API to use to determine if a user is logged on to a machine or not. An RDP user should not be presented with prompts to make decisions about a machine. The user in control of the machine via keyboard, mouse, monitor should be making those decisions.
@PowerSheller
I probably don't know as much as I should, but I will see how much time I can spend on this. Finishing up a project right now so pretty busy but will do my best. I've dealt with this issue as a packager in the WinXP world before and would like a PowerShell based solution for this.
The -1 exit codes you are seeing sounds like ServiceUI.exe is not able to detect a logged on user because it's using this API to detects logged on users: WTSGetActiveConsoleSessionId . Why it is not working on Win 7 32 bits seems like a bug in ServiceUI.exe perhaps. See below info:
WTSGetActiveConsoleSessionId should always give you the correct Console Session ID at the time of the call.
This API will also return a -1 if there is no active user logged into the interactive session (that is, logged in locally to the physical machine, as opposed to using RDP).
Also, I think this is the appropriate API to use to determine if a user is logged on to a machine or not. An RDP user should not be presented with prompts to make decisions about a machine. The user in control of the machine via keyboard, mouse, monitor should be making those decisions.
@PowerSheller
I probably don't know as much as I should, but I will see how much time I can spend on this. Finishing up a project right now so pretty busy but will do my best. I've dealt with this issue as a packager in the WinXP world before and would like a PowerShell based solution for this.
↧
New Post: Deployment Script: Adobe Flash player 11.9.900.152
sintaxasn wrote:
Hey,Would you please tell us / supply code on how to detect if Firefox is installed? I would like to only install the flash player plugin if FIrefox is found on the machine. Thank you!
Great work on posting these. I have one suggestion as a replacement for the Installation section. The Flash ActiveX MSI will fail to install if IE version is 10 or higher. This check should work:Also, Chrome has Flash built in so doesn't need to be shut down prior to install (doesn't get touched by these MSIs).# Get the Internet Explorer version $ieVersion = Get-Item "HKLM:Software\Microsoft\Internet Explorer" -ErrorAction SilentlyContinue | Get-ItemProperty | Select "svcVersion" -ExpandProperty "svcVersion" $ieSubstringPos = $ieVersion.IndexOf(".") [int]$ieShortVersion = $ieVersion.Substring(0, $ieSubstringPos) # Install Flash Player ActiveX Control for IE versions prior to IE10 If ($ieShortVersion -lt 10) { Execute-MSI -Action Install -Path "install_flash_player_11_active_x.msi" } # Install Flash Player Plugin (Firefox / Adobe Reader) Execute-MSI -Action Install -Path "install_flash_player_11_plugin.msi"
Cheers, Dan
↧