I would like to introduce you to a PowerShell tool I originally created in early 2024, which was in the research and planning stage for about 12 months previous to that. If you or your customer runs Micromine Alastri Software, especially in a virtualised desktop platform from Vendors such as Citrix, Omnissa, Parallels, Microsoft and Amazon, you may want to consider using this tool to help increase performance and reduce potential issues caused by the amount of temporary data (reads & writes) generated and consumed by the Alastri applications.
The Challenge
The Micromine Alastri software is used for mine planning processes, with the “Hub” being the central tool for licensing, downloading and launching the various applications in the software suite. The Alastri Hub is one of the coolest written apps I’ve ever deployed. So cleverly developed! It is certainly one of the easiest applications to maintain in a virtual desktop platform. The Hub is a computer wide install, where as the individual suite of applications install into the users %LOCALAPPDATA% folder like a Squirrel deployment. This allows both the Vendor and Customer to be agile when introducing fixes, enhancements and releases without the need to involve the IT support.
It provided a few challenges in a Citrix PVS non-persistent image. I am grateful that I was able to have some valuable technical conversations with the Vendor support and development teams that allowed us to work together for a good understanding and great outcome for the business. The main challenge was the rapid consumption of Citrix PVS write-cache, depending on what tasks users were executing. With many mining apps there is a lot of importing and exporting, where a considerable ammount of data will cycle through the users TEMP folder. Much of it automatically happens through the simulation/projection phases of the process that the apps step through. So the write-cache consumption could be quite aggressive.
Moving this to a working folder on a network share was out of the question, as the Vendor said that this did not meet their performance requirements. They would only support local SSD performance. Fair call. But I couldn’t just leave it at that! Managing this TEMP data was causing operational issues and performance complaints from the users, damaging the reputation of the desktop virtualisation service.
This is where things got even cooler. They went to the effort of implementing a new variable called “AlastriTempWorkingDirectory” from version 22.3 of their program suite, allowing us to set a different location from the users standard TEMP folder for certain parts of the working dataset. This is where my requirement to find and implement a reliable RAM Disk solution became essential.
Hence why a prerequisite to deploying this script is to implement a RAM Disk solution. The PowerShell script is coded to leverage Arsenal Image Mounter (AIM) from Arsenal Recon for the detection of the Arsenal Image Mounter SCSI device, and uses the command line tool “C:\Program Files\Arsenal Image Mounter\aim_cli.exe” to enumerate available RAM Disks. Please refer to the following articles on why I chose this and how it’s deployed:
- The best free for commercial use RAM Disk that works perfectly with Desktop Virtualisation
- Silently Installing and Automating the Arsenal Image Mounter (AIM) RAM Disk Feature
About The User Interface
A lot of thought went into the User Interface (UI) to ensure it was presented clearly to the users. You’ll notice…
- It says that “This is not a Micromine developed or supported tool”. This was important to remove confusion and ensure the users didn’t go directly to Micromine for any issues they may of experienced with the tool itself.
- It tells the users which Host they are running on. This is helpful from a support point of view.
- The instructions are clear and accurate without trying to over explain, but of course there is always room for improvement.
- It tells them the amount of system memory in the VDI Session Hosts so they know that as they consume the RAM Disk it will reduce the overall memory availability. This is important to help manage their expectations, especially when multiple large models are loaded across different applications.
- I also give the user an opt-out option. So if they or Micromine Support feel that this is causing issues, they can simply select “A RAM disk is not required” before selecting to “Start Alastri Hub”.
- When they select the “Start Alastri Hub” button, verbose output is displayed in the message area at the bottom and the UI automatically closes 10 seconds later.
- Once launched the UI will stay open for a maximum of 30 minutes, which ensures it is not left sitting idle keeping sessions open.
Deployment
Download the script as documented below, which I place in the “C:\Program Files\Alastri Software\Alastri Hub 2.0” folder.
Setup the Start Menu shortcut, Desktop shortcut or Published App so that users will start Alastri Hub by running the AlastriHub-Launcher.ps1 PowerShell script instead of AlastriHub.exe. The AlastriHub-Launcher.ps1 will then start the AlastriHub.exe.
There are 4 possible ways to launch the PowerShell script.
1. Launch the script with default settings. If a RAM disk is present, the UI will start, otherwise it will start Alastri Hub as per normal. This is how the tool has been running in production since March 2024.
powershell -executionpolicy bypass "C:\Program Files\Alastri Software\Alastri Hub 2.0\AlastriHub-Launcher.ps1"
2. Launch the script with default settings in silent mode. If a RAM disk is present, it will set the ‘AlastriTempWorkingDirectory’ environment variable for the current process to point to it before starting Alastri Hub, otherwise it will start Alastri Hub as per normal. After several months in production the users no longer wanted to see the UI. They were happy that the AlastriTempWorkingDirectory variable and RAM Disk were doing the job.
powershell -executionpolicy bypass "C:\Program Files\Alastri Software\Alastri Hub 2.0\AlastriHub-Launcher.ps1" -ShowUI:$False
If the default settings are accepted and the ‘AlastriTempWorkingDirectory’ environment variable is set, this is passed to the AlastriHub.exe process, which then passes it to the 3DMark.exe process for the application within the suite that is launched. This then passes it to any Alastri.Mesh.Program.exe processes that the 3DMark.exe process starts. You can see from the following screen shot that the ‘AlastriTempWorkingDirectory’ environment variable is set as expected and the TEMP and TMP variables are left as per standard.
3. Launch the script with change TEMP & TMP option set as the default in the UI. If a RAM disk is present, the UI will start, otherwise it will start Alastri Hub as per normal.
powershell -executionpolicy bypass "C:\Program Files\Alastri Software\Alastri Hub 2.0\AlastriHub-Launcher.ps1" -ChangeTEMP
4. Launch the script with change TEMP & TMP option set in silent mode. If a RAM disk is present, it will set the TEMP & TMP environment variables for the current process to point to it before starting Alastri Hub, otherwise it will start Alastri Hub as per normal.
powershell -executionpolicy bypass "C:\Program Files\Alastri Software\Alastri Hub 2.0\AlastriHub-Launcher.ps1" -ChangeTEMP -ShowUI:$False
You can see from the following screen shot that the ‘AlastriTempWorkingDirectory’ environment variable is not set and the TEMP and TMP variables are now set to the RAM Disk for the current process and child/sub processes ONLY.
Logging
The users will see logging in the message area at the bottom and the UI like this…
There is also a full AlastriHub-Launcher.log transcription file written to their standard TEMP folder…
The Script
Here is the AlastriHub-Launcher.ps1 (4 downloads) script.
<# This PowerShell script with a UI has been written to validate the existence/presence of a usable RAM disk and then provide the user with a choice to set either the 'AlastriTempWorkingDirectory' environment variable or TEMP/TMP variables to point to it before launching Alastri Hub. It can also be run silently to avoid the UI prompting the user for action. If using this script to change the TEMP/TMP variables, the Alastri applications using the .NET Path.GetTempPath Method from the System.IO Namespace will return the new path of the current user's temporary folder. It is important to note that this ONLY changes the variables for the current process, which is this PowerShell script. The variable change is passed to all sub/child processes which inherit the current environment by default. So these variable changes are not permanent and will not effect other processes (applications). The Set-MyVariable function has a parameter called SetUserVariableTarget that can be set to True to if you would like to change this behavior. As well as setting the variable, it also creates the folder structure on the RAM disk. The detection of a RAM disk is reliant on the Arsenal Image Mounter (AIM) software being installed and the command line tool "C:\Program Files\Arsenal Image Mounter\aim_cli.exe" being present. As part of the AIM deployment a Scheduled Task is created that creates a RAM disk at system startup under the local System account since a RAM disk cannot be created under the context of a standard (non-Administrative) user. Example Syntax: - Launch the script with default settings. If a RAM disk is present, the UI will start, otherwise it will start Alastri Hub as per normal. powershell -executionpolicy bypass .\AlastriHub-Launcher.ps1 - Launch the script with default settings in silent mode. If a RAM disk is present, it will set the 'AlastriTempWorkingDirectory' environment variable for the current process to point to it before starting Alastri Hub, otherwise it will start Alastri Hub as per normal. powershell -executionpolicy bypass .\AlastriHub-Launcher.ps1 -ShowUI:$False - Launch the script with change TEMP & TMP option set as the default. If a RAM disk is present, the UI will start, otherwise it will start Alastri Hub as per normal. powershell -executionpolicy bypass .\AlastriHub-Launcher.ps1 -ChangeTEMP - Launch the script with change TEMP & TMP option set in silent mode. If a RAM disk is present, it will set the TEMP & TMP environment variables for the current process to point to it before starting Alastri Hub, otherwise it will start Alastri Hub as per normal. powershell -executionpolicy bypass .\AlastriHub-Launcher.ps1 -ChangeTEMP -ShowUI:$False Script Name: AlastriHub-Launcher.ps1 Release 1.1 Written by Jeremy Saunders (jeremy@jhouseconsulting.com) 15th March 2023 Modified by Jeremy Saunders (jeremy@jhouseconsulting.com) 17th February 2025 #> #------------------------------------------------------------- [cmdletbinding()] param ( [switch]$HideConsole, [switch]$ChangeTEMP, [switch]$ShowUI=$True ) # Set Powershell Compatibility Mode Set-StrictMode -Version 2.0 # Enable verbose, warning and error mode $VerbosePreference = 'Continue' $WarningPreference = 'Continue' $ErrorPreference = 'Continue' #------------------------------------------------------------- $StartDTM = (Get-Date) # Set the working folder to the users TEMP folder $LogFolder = [System.IO.Path]::GetTempPath() # Get the script path $ScriptPath = {Split-Path $MyInvocation.ScriptName} $ScriptPath = $(&$ScriptPath) # Get the script name $ScriptName = [System.IO.Path]::GetFilenameWithoutExtension($MyInvocation.MyCommand.Path.ToString()) # Start the transcript try { Start-Transcript "$LogFolder$ScriptName.log" } catch { Write-Verbose "$(Get-Date -format "dd/MM/yyyy HH:mm:ss"): This host does not support transcription" } #------------------------------------------------------------- # Make sure the release version is updated accordingly so it's correct in the form Title Bar. $Release = "1.1" # Setting an empty string will exclude it from the form Title Bar. $DevelopedBy = "Jeremy Saunders" # Set the name of the app to launch $AppName = "Alastri Hub" # Set the full path to the executable of the app you want to launch $AppExecutable = "${env:ProgramFiles}\Alastri Software\Alastri Hub 2.0\AlastriHub.exe" # Set the file to extract the icon from to be used in the form. $IconFile = "$AppExecutable" # Set the Temp Child Folder name to create that will be assigned to the AlastriTempWorkingDirectory variable. $TempChildFolder = "AlastriTemp" #------------------------------------------------------------- # Hide the PowerShell console window without hiding the other child windows that it spawns # - http://powershell.cz/2013/04/04/hide-and-show-console-window-from-gui/ $Code = @" [DllImport("Kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow); "@ # Create new types as per the definition above. Add-Type -Namespace Console -Name Window -MemberDefinition $code -PassThru | out-null Function Show-Console { $consolePtr = [Console.Window]::GetConsoleWindow() #5 show [Console.Window]::ShowWindow($consolePtr, 5) } Function Hide-Console { $consolePtr = [Console.Window]::GetConsoleWindow() #0 hide [Console.Window]::ShowWindow($consolePtr, 0) } If ($HideConsole) { Hide-Console | out-null } #------------------------------------------------------------- Function Get-TotalSystemRAM { $TotalRAM = 0 $CanConnect = $false Try { $ComputerInformation = Get-WmiObject -Class Win32_ComputerSystem -ErrorAction Stop | Select-Object ` @{N="TotalPhysicalRam"; E={[math]::round(($_.TotalPhysicalMemory / 1GB),0)}} $CanConnect = $true } Catch { $ErrorDescription = "Error connecting using the Get-WmiObject cmdlet." write-warning "*ERROR*: $ErrorDescription" -verbose } if ($CanConnect) { $TotalRam = $ComputerInformation.totalphysicalram } return $TotalRam } #------------------------------------------------------------- Function Get-UserProfileChildFolder { $userProfileChildFolder = "" # Get the USERPROFILE environment variable #$userProfile = [Environment]::GetEnvironmentVariable("USERPROFILE", "Process") $userProfile = $env:USERPROFILE # Extract the child folder from the USERPROFILE path $userProfileChildFolder = (Split-Path -Path "${env:USERPROFILE}" -Leaf) return $userProfileChildFolder } #------------------------------------------------------------- Function EnvironmentRefresh { # Send a WM_SETTINGCHANGE broadcast message with "Environment" as the parameter to all open windows, indicating a change in the # environment variables. It is used to reload/refresh the environment variables in all running processes without the need for a # system restart. # This is used in scenarios where a change in user and system environment variables needs to be propagated immediately to all # running applications and services. # Notes: # - Applications and services that have a top-level window will be notified of a change in the environment variables. # - A restart of some applications and services may still be necessary for the changes to take effect, as some processes only # read environment variables at startup. # - Not all applications will respond to the WM_SETTINGCHANGE message, especially those that do not have a top-level window or # those that simply choose to ignore the message. In those cases, a system restart or individual application/service restart # might still be necessary. param ( [switch]$EnableDebug ) $HWND_BROADCAST = [IntPtr] 0xffff; $WM_SETTINGCHANGE = 0x1a; $result = [UIntPtr]::Zero if (-not ("Win32.NativeMethods" -as [Type])) { # import sendmessagetimeout from win32 Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @" [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern IntPtr SendMessageTimeout( IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult); "@ } # notify all windows of environment block change If ($EnableDebug) { write-verbose "Sending a WM_SETTINGCHANGE message to all the open windows..." -verbose } [void][Win32.Nativemethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, "Environment", 2, 5000, [ref] $result) If ($EnableDebug) { if ($result -eq 0) { write-warning "- Failed to reload environment variables." -verbose } else { write-verbose "- Environment variables have been reloaded." -verbose } } } #------------------------------------------------------------- Function Set-MyVariable{ Param ( [ValidateSet('Set','Remove')] [string]$Action, [string]$Name, [string]$Value, [switch]$SetUserVariableTarget, [switch]$Redundant, [switch]$SHowUI ) # https://stackoverflow.com/questions/40679456/what-is-the-proper-way-to-set-a-user-environment-variable-in-a-powershell-script If ($Action -eq "Set") { $StatusMessage = "Setting the `"$Name`" user environment variable to..." If ($SHowUI) { $status1TextBox.AppendText("$StatusMessage `r`n") $status1TextBox.ScrollToCaret() } write-verbose "$StatusMessage" -verbose $StatusMessage = "- $Value" If ($SHowUI) { $status1TextBox.AppendText("$StatusMessage `r`n") $status1TextBox.ScrollToCaret() } write-verbose "$StatusMessage" -verbose ${env:Name} = $Value [System.Environment]::SetEnvironmentVariable($Name, $Value, [System.EnvironmentVariableTarget]::Process) If ($SetUserVariableTarget) { [System.Environment]::SetEnvironmentVariable($Name, $Value, [System.EnvironmentVariableTarget]::User) } } If ($Action -eq "Remove") { $StatusMessage = "Clearing the `"$Name`" user environment variable" If ($Redundant) { $StatusMessage = "Clearing the `"$Name`" user environment variable as it's no longer used" } If ($SHowUI) { $status1TextBox.AppendText("$StatusMessage `r`n") $status1TextBox.ScrollToCaret() } write-verbose "$StatusMessage" -verbose ${env:Name} = $null [System.Environment]::SetEnvironmentVariable($Name, $null, [System.EnvironmentVariableTarget]::Process) If ($SetUserVariableTarget) { [System.Environment]::SetEnvironmentVariable($Name, $null, [System.EnvironmentVariableTarget]::User) } } } #------------------------------------------------------------- Function Create-Folder{ Param ( [string]$Name, [switch]$SHowUI ) $StatusMessage = "" If (-not(TEST-PATH "$Name")) { $StatusMessage = "Creating the `"$Name`" folder" New-Item -Path "$Name" -ItemType Directory | out-null } Else { $StatusMessage = "The `"$Name`" folder already exists" } if (!([String]::IsNullOrEmpty($StatusMessage))) { write-verbose "$StatusMessage" -verbose If ($SHowUI) { $status1TextBox.AppendText("$StatusMessage `r`n") $status1TextBox.ScrollToCaret() } } } #------------------------------------------------------------- Function Launch-App{ Param ( [string]$AppName, [string]$AppExecutable, [string]$CmdLineArgs="", [string]$WorkingDirectory = "${env:HOMEDRIVE}${env:HOMEPATH}\", [switch]$SHowUI, [int]$UICloseInSeconds ) $StatusMessage = "Starting ${AppName}..." If ($ShowUI) { $status1TextBox.AppendText("$StatusMessage `r`n") $status1TextBox.ScrollToCaret() } write-verbose "$StatusMessage" -verbose $Successful = $False If (Test-Path -Path "$AppExecutable") { $LaunchProcess = "$AppExecutable" $LaunchArguments = "$CmdLineArgs" $pinfo = New-Object System.Diagnostics.ProcessStartInfo $pinfo.FileName = $LaunchProcess $pinfo.UseShellExecute = $false $pinfo.Arguments = $LaunchArguments $pinfo.WorkingDirectory = $WorkingDirectory $p = New-Object System.Diagnostics.Process $p.StartInfo = $pinfo Try { $p.Start() | Out-Null Write-Verbose "Successfully executed" -Verbose $Successful = $True } Catch { Write-Warning "Failed to execute" -Verbose } $p.Dispose() } Else { write-verbose "${AppName} was not found on this computer." -verbose } If ($ShowUI -AND $Successful) { $StatusMessage = "Closing this form in $UICloseInSeconds seconds..." $status1TextBox.AppendText("$StatusMessage `r`n") $status1TextBox.ScrollToCaret() write-verbose "$StatusMessage" -verbose # Enable and start the timer $GuiAppLaunchTimer.Enabled = $true $GuiAppLaunchTimer.Start() } } #------------------------------------------------------------- Function Get-FirstAvailableRAMDisk { # Set the path for the Arsenal Image Mounter CLI (AIM CLI) $Executable = "${env:ProgramFiles}\Arsenal Image Mounter\aim_cli.exe" $DriveLetters = @() $IsDriverInstalled = $false $CanConnect = $false Try { $PnPSignedDriver = Get-WmiObject -Class Win32_PnPSignedDriver -ErrorAction Stop | Where {$_.Description -eq "Arsenal Image Mounter"} $CanConnect = $true } Catch { $ErrorDescription = "Error connecting using the Get-WmiObject cmdlet." write-warning "*ERROR*: $ErrorDescription" -verbose } if ($CanConnect -AND $null -ne $PnPSignedDriver) { $IsDriverInstalled = $true write-verbose "The Arsenal Image Mounter SCSI device is installed" -verbose write-verbose "- Device ID: $($PnPSignedDriver.DeviceID)" -verbose write-verbose "- Device Version: $($PnPSignedDriver.DriverVersion)" -verbose } Else { write-verbose "The Arsenal Image Mounter SCSI device is not installed" -verbose } If ($IsDriverInstalled -AND (Test-Path -Path "$Executable")) { $AllRamDisks = $null $MyArgs = "--list" write-verbose "Get all RAM disk devices" -verbose write-verbose "- Starting the process: `"$Executable`"" -verbose write-verbose "- with the arguments: $MyArgs" -verbose $pinfo = New-Object System.Diagnostics.Process $pinfo.StartInfo.FileName = $Executable # WindowStyle; 1 = hidden, 2 =maximized, 3=minimized, 4=normal $pinfo.StartInfo.WindowStyle = 1 $pinfo.StartInfo.Arguments = $MyArgs $pinfo.StartInfo.RedirectStandardError = $True $pinfo.StartInfo.RedirectStandardOutput = $True $pinfo.StartInfo.UseShellExecute = $false Try { $null = $pinfo.start() | Out-Null $AllRamDisks = $pinfo.StandardOutput.ReadToEnd().Trim().Split("`r`n") $AllRamDisks += $pinfo.StandardError.ReadToEnd().Trim().Split("`r`n") } Catch { # } $pinfo.Dispose() If ($AllRamDisks -Like "*No virtual disks*") { write-verbose "No virtual disks mounted" -verbose } Else { $TotalRAMDisks = 0 ForEach ($Line in $AllRamDisks) { $MountedAt = "" If ($Line -like "*Mounted at*") { $MountedAt = ($Line.Trim() -Split("Mounted at"))[1].Trim() } if (!([String]::IsNullOrEmpty($MountedAt))) { $TotalRAMDisks++ $DriveLetters += $MountedAt } } If ($TotalRAMDisks -eq 1) { write-verbose "$TotalRAMDisks RAM disk found" -verbose } Else { write-verbose "$TotalRAMDisks RAM disks found" -verbose } } } return $DriveLetters } #------------------------------------------------------------- Function IsAccessAllowed { param( [string]$Folder, [switch]$ConsoleOutput ) If (TEST-PATH $Folder) { If ($ConsoleOutput) { write-verbose "Testing access to: $Folder" -verbose } Get-ChildItem -path $Folder -EA SilentlyContinue -ErrorVariable ErrVar is # The -ErrorVariable common parameter creates an ArrayList. This variable # always initialized which means it will never be $null. The proper way # to test if an ArrayList is empty or not is to use the Count property. It # should be empty or equal to 0 if there are no errors. If ($ErrVar.count -eq 0) { If ($ConsoleOutput) { write-verbose "Access is good" -verbose } $return = $True } Else { If ($ConsoleOutput) { write-warning "Access denied" -verbose } $return = $False } } Else { If ($ConsoleOutput) { write-warning "The `"$Folder`" does not exist" -verbose } $return = $False } return $return } #------------------------------------------------------------- Function Get-RamDisks { # Check for existance of a RAM disk # - Get the drive letter, size, freespace, and build an array for the drop down list. # - Set the largest available RAM disk as the preferred for the drop down list. $ResultProps = @{ RAMDiskFound = $False RAMDISKS = @() PreferredRAMDisk = "" } $RAMDiskLetters = Get-FirstAvailableRAMDisk $LargestRAMDiskSize = 0 ForEach ($Letter in $RAMDiskLetters) { $RAMDiskLetter = $Letter.TrimEnd("\") $RAMDiskLetter = $RAMDiskLetter.TrimEnd(":") If (IsAccessAllowed -Folder "${RAMDiskLetter}:\") { $ResultProps.RAMDiskFound = $True $RAMDiskSize = 0 $RAMDiskFreeSpace = 0 $CanConnect = $false Try { $LogicalDiskInformation = Get-WmiObject -Class Win32_LogicalDisk -Filter "DeviceID='${RAMDiskLetter}:'" -ErrorAction Stop | Select-Object ` @{N="TotalSize"; E={[math]::round(($_.Size / 1GB),0)}},@{N="TotalFreeSpace"; E={[math]::round(($_.FreeSpace / 1GB),0)}} $CanConnect = $true } Catch { $ErrorDescription = "Error connecting using the Get-WmiObject cmdlet." write-warning "*ERROR*: $ErrorDescription" -verbose } if ($CanConnect) { $RAMDiskSize = ($LogicalDiskInformation.TotalSize).ToString() $RAMDiskFreeSpace = ($LogicalDiskInformation.TotalFreeSpace).ToString() } If ($RAMDiskSize -gt $LargestRAMDiskSize) { $LargestRAMDiskSize = $RAMDiskSize $ResultProps.PreferredRAMDisk = $RAMDiskLetter } $ResultProps.RAMDISKS += @([pscustomobject]@{name=" ${RAMDiskLetter}: Drive - Total size of $RAMDiskSize GB with $RAMDiskFreeSpace GB of free space";description=$RAMDiskLetter}) } } return $ResultProps } #------------------------------------------------------------- function Load-ComboBox { <# .SYNOPSIS This functions helps you load items into a ComboBox. .DESCRIPTION Use this function to dynamically load items into the ComboBox control. With the Load-ComboBox function the ValueMember only works if you are data binding and using the ComboBox's DataSource property. Instead of ValueMember you could just access the property from the selected object: $combobox1.SelectedItem.Description .PARAMETER ComboBox The ComboBox control you want to add items to. .PARAMETER Items The object or objects you wish to load into the ComboBox's Items collection. .PARAMETER DisplayMember Indicates the property to display for the items in this control. .PARAMETER Append Adds the item(s) to the ComboBox without clearing the Items collection. #> Param ( [Parameter(Mandatory=$true)] [System.Windows.Forms.ComboBox]$ComboBox, [Parameter(Mandatory=$true)]$Items, [Parameter(Mandatory=$false)] [string]$DisplayMember, [string]$ValueMember, [switch]$Append ) if(-not $Append) { $comboBox.Items.Clear() } if($Items -is [Array]) { $comboBox.Items.AddRange($Items) } else { $comboBox.Items.Add($Items) } $comboBox.ValueMember = "description" $comboBox.DisplayMember = "name" } #------------------------------------------------------------- Function Call-Silent { # This function will be executed if a RAM disk is found and the script # is launched with the $ShowUI variable set to $False param ( [string]$PreferredRAMDisk, [switch]$ChangeTEMP, [string]$TempChildFolder, [string]$AppName, [string]$AppExecutable ) $userProfileChildFolder = Get-UserProfileChildFolder $PathToSet = "${PreferredRAMDisk}:\${userProfileChildFolder}" If ($ChangeTEMP -eq $False) { $PathToSet = $PathToSet + "\${TempChildFolder}" Set-MyVariable -Action:"Set" -Name:"AlastriTempWorkingDirectory" -Value:"$PathToSet" -SetUserVariableTarget:$False # Reset the variables $CurrentTemp = [System.IO.Path]::GetTempPath().TrimEnd('\') If ($CurrentTemp -ne "${env:LOCALAPPDATA}\Temp") { Set-MyVariable -Action:"Set" -Name:"TEMP" -Value:"${env:LOCALAPPDATA}\Temp" -SetUserVariableTarget:$False Set-MyVariable -Action:"Set" -Name:"TMP" -Value:"${env:LOCALAPPDATA}\Temp" -SetUserVariableTarget:$False } } Else { $PathToSet = $PathToSet + "\Temp" Set-MyVariable -Action:"Set" -Name:"TEMP" -Value:"$PathToSet" -SetUserVariableTarget:$False Set-MyVariable -Action:"Set" -Name:"TMP" -Value:"$PathToSet" -SetUserVariableTarget:$False # Clear variables Set-MyVariable -Action:"Remove" -Name:"AlastriTempWorkingDirectory" -SetUserVariableTarget:$True } Create-Folder -Name:"$PathToSet" # Refresh environment variables EnvironmentRefresh # Launching the app Launch-App -AppName:"$AppName" -AppExecutable:"$AppExecutable" } #------------------------------------------------------------- Function Call-NoRAMDisk { # This function will be executed if a no RAM disk is found. param ( [string]$AppName, [string]$AppExecutable ) # Clear and/or reset the variables Set-MyVariable -Action:"Remove" -Name:"AlastriTempWorkingDirectory" -SetUserVariableTarget:$True $CurrentTemp = [System.IO.Path]::GetTempPath().TrimEnd('\') If ($CurrentTemp -ne "${env:LOCALAPPDATA}\Temp") { Set-MyVariable -Action:"Set" -Name:"TEMP" -Value:"${env:LOCALAPPDATA}\Temp" -SetUserVariableTarget:$False Set-MyVariable -Action:"Set" -Name:"TMP" -Value:"${env:LOCALAPPDATA}\Temp" -SetUserVariableTarget:$False } EnvironmentRefresh Launch-App -AppName:"$AppName" -AppExecutable:"$AppExecutable" } #------------------------------------------------------------- Function Call-MainForm { # This is the main user form and will be executed if a RAM disk is found # and the script is launched with the $ShowUI variable set to $True param ( [string]$ToolRelease, [array]$RAMDISKS, [string]$PreferredRAMDisk, [switch]$ChangeTEMP, [string]$TempChildFolder, [string]$AppName, [string]$AppExecutable, [string]$IconFile, [int]$CloseAfterInMinutes=30 ) $codeDisableX = @" using System.Windows.Forms; namespace MyForm { public class FormWithoutX : Form { protected override CreateParams CreateParams { get { CreateParams cp = base.CreateParams; cp.ClassStyle = cp.ClassStyle | 0x200; return cp; } } } } "@ Add-Type -AssemblyName System.Windows.Forms Add-Type -TypeDefinition $codeDisableX -ReferencedAssemblies System.Windows.Forms Add-Type -AssemblyName System.Drawing Add-Type -AssemblyName PresentationCore,PresentationFramework $form = [MyForm.FormWithoutX]::new() #$form = New-Object System.Windows.Forms.Form $form.Text = "Alastri Hub Launcher Tool v$ToolRelease" $form.ClientSize = New-Object System.Drawing.Size(545,755) $form.MaximizeBox = $False $form.MinimizeBox = $False $form.MinimumSize = New-Object System.Drawing.Size(545,755) $form.MaximumSize = New-Object System.Drawing.Size(545,755) $form.FormBorderStyle = "FixedDialog" $form.SizeGripStyle = "Hide" If (Test-Path "$IconFile") { $form.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$IconFile") } $form.WindowState = 'Normal' $form.KeyPreview = $True # Start the form from the top left eighth position of the primary screen. This ensures # it starts consistently in the same location and out of the way of any popups. $LocationWidth = $(([System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea.Width /8)) $LocationHeight = $(([System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea.Height /8)) $form.Location = New-Object System.Drawing.Point($LocationWidth,$LocationHeight) $form.StartPosition = 'Manual' # Set the Maximum width of the header section so that the text can be centred correctly. $MaxWidth = 540 $labelheader = New-Object 'System.Windows.Forms.Label' $labelheader.Font = 'Microsoft Sans Serif, 14.25pt' $labelheader.ForeColor = 'White' $labelheader.Name = 'labelheader' $labelheader.Text = 'Alastri Hub Launcher Tool' $TextSize = [System.Windows.Forms.TextRenderer]::MeasureText($labelheader.Text, $labelheader.Font) $TextWidth = ($MaxWidth - $TextSize.Width) / 2 If ($TextWidth -le 0) { $TextWidth = 1 } $labelheader.Location = New-Object System.Drawing.Point($TextWidth,10) $labelheader.Size = New-Object System.Drawing.Size($MaxWidth,23) $labelheader.TabIndex = 1 $labelheader.TextAlign = 'MiddleLeft' $labelsubheader1 = New-Object 'System.Windows.Forms.Label' $labelsubheader1.Font = 'Microsoft Sans Serif, 12pt' $labelsubheader1.ForeColor = 'White' $labelsubheader1.Name = 'labelheader' $labelsubheader1.Text = "This is not a Micromine developed or supported tool" $TextSize = [System.Windows.Forms.TextRenderer]::MeasureText($labelsubheader1.Text, $labelsubheader1.Font) $TextWidth = ($MaxWidth - $TextSize.Width) / 2 If ($TextWidth -le 0) { $TextWidth = 1 } $labelsubheader1.Location = New-Object System.Drawing.Point($TextWidth,40) $labelsubheader1.Size = New-Object System.Drawing.Size($MaxWidth, 23) $labelsubheader1.TabIndex = 1 $labelsubheader1.TextAlign = 'MiddleLeft' $labelsubheader2 = New-Object 'System.Windows.Forms.Label' $labelsubheader2.Font = 'Microsoft Sans Serif, 12pt' $labelsubheader2.ForeColor = 'White' $labelsubheader2.Name = 'labelheader' $labelsubheader2.Text = "This is running on ${env:COMPUTERNAME}" $TextSize = [System.Windows.Forms.TextRenderer]::MeasureText($labelsubheader2.Text, $labelsubheader2.Font) $TextWidth = ($MaxWidth - $TextSize.Width) / 2 If ($TextWidth -le 0) { $TextWidth = 1 } $labelsubheader2.Location = New-Object System.Drawing.Point($TextWidth,70) $labelsubheader2.Size = New-Object System.Drawing.Size($MaxWidth, 23) $labelsubheader2.TabIndex = 1 $labelsubheader2.TextAlign = 'MiddleLeft' $panelheader = New-Object 'System.Windows.Forms.Panel' $panelheader.Controls.Add($labelheader) $panelheader.Controls.Add($labelsubheader1) $panelheader.Controls.Add($labelsubheader2) $panelheader.BackColor = '0, 114, 198' $panelheader.Location = New-Object System.Drawing.Point(0, 0) $panelheader.Name = 'header' $panelheader.Size = New-Object System.Drawing.Size($MaxWidth, 100) $panelheader.TabIndex = 8 $form.Controls.Add($panelheader) $closeButton = New-Object System.Windows.Forms.Button $closeButton.Location = New-Object System.Drawing.Point(445,565) $closeButton.Size = New-Object System.Drawing.Size(75,23) $closeButton.Text = 'Close' $closeButton.DialogResult = [System.Windows.Forms.DialogResult]::OK $form.CancelButton = $closeButton $form.Controls.Add($closeButton) $status1label = New-Object System.Windows.Forms.Label $status1label.Font = 'Microsoft Sans Serif, 9pt' $status1label.Location = New-Object System.Drawing.Point(10,570) $status1label.Name = 'status1label' $status1label.Size = New-Object System.Drawing.Size(100,23) $status1label.UseMnemonic = $false $status1label.Text = "Output message:" $form.Controls.Add($status1label) $status1TextBox = New-Object System.Windows.Forms.RichTextBox $status1TextBox.Location = New-Object System.Drawing.Point(10,595) $status1TextBox.Size = New-Object System.Drawing.Size(510,120) $status1TextBox.Multiline = $true $status1TextBox.WordWrap = $true $status1TextBox.ScrollBars = [System.Windows.Forms.ScrollBars]::'Vertical' $status1TextBox.Enabled = $true $status1TextBox.ReadOnly = $true $oldFont = $status1label.Font $newFont = New-Object System.Drawing.Font($oldFont.FontFamily, $oldFont.Size, [System.Drawing.FontStyle]::Bold) $status1TextBox.Font = $newFont $form.Controls.Add($status1TextBox) # Create the Timer object to close the form if left idle $GuiIdleTimer = New-Object System.Windows.Forms.Timer # Set the interval in milliseconds (the interval is how often the timer will "tick" once it's started). $GuiIdleTimer.Interval = 1800000 # Specify the action to run every time the timer ticks. $GuiIdleTimer.Add_Tick({ # Disable the timer $GuiIdleTimer.Enabled = $false $StatusMessage = "Idle timeout has been reached. Closing the form." $status1TextBox.AppendText("$StatusMessage `r`n") $status1TextBox.ScrollToCaret() write-verbose "$StatusMessage" -verbose # Close the form $form.Close() }) # Create the Timer object to close the form if after the app has been launched $GuiAppLaunchTimer = New-Object System.Windows.Forms.Timer # Set the interval in milliseconds (the interval is how often the timer will "tick" once it's started). $GuiAppLaunchTimer.Interval = 10000 # Specify the action to run every time the timer ticks. $GuiAppLaunchTimer.Add_Tick({ # Disable the timer $GuiAppLaunchTimer.Enabled = $false $StatusMessage = "App has been launched. Closing the form." $status1TextBox.AppendText("$StatusMessage `r`n") $status1TextBox.ScrollToCaret() write-verbose "$StatusMessage" -verbose # Close the form $form.Close() }) #------------------------------------------------------------- $instructionsLabel1 = New-Object System.Windows.Forms.Label $instructionsLabel1.Font = 'Microsoft Sans Serif, 9pt' #$instructionsLabel1.Font = [System.Drawing.Font]::new("Microsoft Sans Serif", 9, [System.Drawing.FontStyle]::Bold) $instructionsLabel1.Location = New-Object System.Drawing.Point(10,110) $instructionsLabel1.Name = 'status1label' $instructionsLabel1.Size = New-Object System.Drawing.Size(510,23) $instructionsLabel1.UseMnemonic = $false $instructionsLabel1.Text = "Explanation:" $form.Controls.Add($instructionsLabel1) $instructionsLabel2 = New-Object System.Windows.Forms.Label $instructionsLabel2.Font = 'Microsoft Sans Serif, 9pt' $instructionsLabel2.Location = New-Object System.Drawing.Point(20,135) $instructionsLabel2.Name = 'status1label' $instructionsLabel2.Size = New-Object System.Drawing.Size(510,20) $instructionsLabel2.UseMnemonic = $false $instructionsLabel2.Text = "- A RAM disk has been detected on this computer." $form.Controls.Add($instructionsLabel2) $instructionsLabel3 = New-Object System.Windows.Forms.Label $instructionsLabel3.Font = 'Microsoft Sans Serif, 9pt' $instructionsLabel3.Location = New-Object System.Drawing.Point(20,160) $instructionsLabel3.Name = 'status1label' $instructionsLabel3.Size = New-Object System.Drawing.Size(510,20) $instructionsLabel3.UseMnemonic = $false $instructionsLabel3.Text = "- This tool can be used to either..." $form.Controls.Add($instructionsLabel3) $instructionsLabel4 = New-Object System.Windows.Forms.Label $instructionsLabel4.Font = 'Microsoft Sans Serif, 9pt' $instructionsLabel4.Location = New-Object System.Drawing.Point(30,185) $instructionsLabel4.Name = 'status1label' $instructionsLabel4.Size = New-Object System.Drawing.Size(510,50) $instructionsLabel4.UseMnemonic = $false $instructionsLabel4.Text = "1) Set the 'AlastriTempWorkingDirectory' environment variable to a folder on the RAM disk. This variable was introduced from version 22.3 of the Alastri program suite, allowing us to set a different location from your standard TEMP folder." $form.Controls.Add($instructionsLabel4) $instructionsLabel5 = New-Object System.Windows.Forms.Label $instructionsLabel5.Font = 'Microsoft Sans Serif, 9pt' $instructionsLabel5.Location = New-Object System.Drawing.Point(30,235) $instructionsLabel5.Name = 'status1label' $instructionsLabel5.Size = New-Object System.Drawing.Size(510,20) $instructionsLabel5.UseMnemonic = $false $instructionsLabel5.Text = "OR" $form.Controls.Add($instructionsLabel5) $instructionsLabel6 = New-Object System.Windows.Forms.Label $instructionsLabel6.Font = 'Microsoft Sans Serif, 9pt' $instructionsLabel6.Location = New-Object System.Drawing.Point(30,260) $instructionsLabel6.Name = 'status1label' $instructionsLabel6.Size = New-Object System.Drawing.Size(510,20) $instructionsLabel6.UseMnemonic = $false $instructionsLabel6.Text = "2) Change the location of the TEMP & TMP variables to a folder on the RAM disk." $form.Controls.Add($instructionsLabel6) $instructionsLabel7 = New-Object System.Windows.Forms.Label $instructionsLabel7.Font = 'Microsoft Sans Serif, 9pt' $instructionsLabel7.Location = New-Object System.Drawing.Point(20,285) $instructionsLabel7.Name = 'status1label' $instructionsLabel7.Size = New-Object System.Drawing.Size(510,50) $instructionsLabel7.UseMnemonic = $false $instructionsLabel7.Text = "- Setting either of these variables to a RAM disk will increase performance and help to reduce potential issues caused by the amount of temporary data (reads & writes) generated by the Alastri applications." $form.Controls.Add($instructionsLabel7) $TotalRAM = Get-TotalSystemRAM $instructionsLabel8 = New-Object System.Windows.Forms.Label $instructionsLabel8.Font = 'Microsoft Sans Serif, 9pt' $instructionsLabel8.Location = New-Object System.Drawing.Point(20,335) $instructionsLabel8.Name = 'status1label' $instructionsLabel8.Size = New-Object System.Drawing.Size(510,40) $instructionsLabel8.UseMnemonic = $false $instructionsLabel8.Text = "- There is $TotalRAM GB of system RAM assigned to this computer. The RAM disk will consume some of this system RAM up to the size of the RAM disk selected." $form.Controls.Add($instructionsLabel8) #------------------------------------------------------------- $GroupBox1 = New-Object System.Windows.Forms.GroupBox $GroupBox1.Location = '10,380' $GroupBox1.size = '510,160' $GroupBox1.text = "Set the options and then select the 'Start Alastri Hub' button" $GroupBox2 = New-Object System.Windows.Forms.GroupBox $GroupBox2.Location = '15,50' $GroupBox2.size = '480,70' $GroupBox2.text = "Which variable do you want to set?" $RadioButton1 = New-Object System.Windows.Forms.RadioButton $RadioButton1.Location = '10,20' $RadioButton1.size = '200,20' $RadioButton1.Checked = $true $RadioButton1.Text = "Use the detected RAM disk - " $DropDown_RAM_DISK = new-object System.Windows.Forms.ComboBox $DropDown_RAM_DISK.Location = new-object System.Drawing.Point(180,20) $DropDown_RAM_DISK.Size = new-object System.Drawing.Size(310,20) $DropDown_RAM_DISK.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList # Load the values into the combo box Load-ComboBox $DropDown_RAM_DISK $RAMDISKS -DisplayMember name -ValueMember description # Set the selected item for the display $index_RAM_DISK = 0 for($i=0;$i-le $RAMDISKS.length-1;$i++) { if ($RAMDISKS[$i].description -eq $PreferredRAMDisk) { $index_RAM_DISK = $i } } $DropDown_RAM_DISK.SelectedIndex = $index_RAM_DISK $RAMDiskLetter = $DropDown_RAM_DISK.SelectedItem.description $userProfileChildFolder = Get-UserProfileChildFolder $PathToSet = "${RAMDiskLetter}:\${userProfileChildFolder}" $RadioButton2 = New-Object System.Windows.Forms.RadioButton $RadioButton2.Location = '10,20' $RadioButton2.size = '465,20' $RadioButton2.Checked = $false If ($ChangeTEMP -eq $False) { $RadioButton2.Checked = $true } $RadioButton2.Text = "Set the AlastriTempWorkingDirectory variable to: ${PathToSet}\${TempChildFolder}" $RadioButton3 = New-Object System.Windows.Forms.RadioButton $RadioButton3.Location = '10,40' $RadioButton3.size = '440,20' $RadioButton3.Checked = $false If ($ChangeTEMP) { $RadioButton3.Checked = $true } $RadioButton3.UseMnemonic = $false $RadioButton3.Text = "Change the TEMP & TMP variables to: ${PathToSet}\Temp" $DropDown_RAM_DISK.Add_SelectedIndexChanged({ # Inside here you can refer to the ComboBox object as $this $RAMDiskLetter = $this.SelectedItem.description $PathToSet = "${RAMDiskLetter}:\${userProfileChildFolder}" $RadioButton2.Text = "Set the AlastriTempWorkingDirectory variable to: ${PathToSet}\${TempChildFolder}" $RadioButton3.Text = "Change the TEMP & TMP variables to: ${PathToSet}\Temp" }) $RadioButton4 = New-Object System.Windows.Forms.RadioButton $RadioButton4.Location = '10,130' $RadioButton4.size = '200,20' $RadioButton4.Checked = $false $RadioButton4.Text = "A RAM disk is not required." $RadioButton1.Add_Click({ If ($RadioButton1.Checked) { $DropDown_RAM_DISK.Enabled = $True $GroupBox2.Enabled = $True } }) $RadioButton4.Add_Click({ If ($RadioButton4.Checked) { $DropDown_RAM_DISK.Enabled = $False $GroupBox2.Enabled = $false } }) # Add all the GroupBox controls on one line $GroupBox1.Controls.Add($DropDown_RAM_DISK) $GroupBox1.Controls.Add($GroupBox2) $GroupBox1.Controls.AddRange(@($Radiobutton1,$RadioButton4)) $GroupBox2.Controls.AddRange(@($Radiobutton2,$RadioButton3)) $form.Controls.Add($GroupBox1) #------------------------------------------------------------- $action1Button = New-Object System.Windows.Forms.Button $action1Button.Location = New-Object System.Drawing.Point(180,550) $action1Button.Size = New-Object System.Drawing.Size(150,23) $action1Button.TabIndex = 0 $buttonText1 = "Start Alastri Hub" $action1Button.Text = $buttonText1 $action1Button.Add_Click({ $ButtonType = [System.Windows.MessageBoxButton]::OK $MessageIcon = [System.Windows.MessageBoxImage]::Information If ($RadioButton1.Checked -AND (!($RadioButton4.Checked))) { $RAMDiskLetter = $DropDown_RAM_DISK.SelectedItem.description $PathToSet = "${RAMDiskLetter}:\${userProfileChildFolder}" If ($RadioButton2.Checked -AND (!($RadioButton3.Checked))) { $PathToSet = $PathToSet + "\${TempChildFolder}" Set-MyVariable -Action:"Set" -Name:"AlastriTempWorkingDirectory" -Value:"$PathToSet" -SetUserVariableTarget:$False -SHowUI # Reset the variables $CurrentTemp = [System.IO.Path]::GetTempPath().TrimEnd('\') If ($CurrentTemp -ne "${env:LOCALAPPDATA}\Temp") { Set-MyVariable -Action:"Set" -Name:"TEMP" -Value:"${env:LOCALAPPDATA}\Temp" -SetUserVariableTarget:$False -SHowUI Set-MyVariable -Action:"Set" -Name:"TMP" -Value:"${env:LOCALAPPDATA}\Temp" -SetUserVariableTarget:$False -SHowUI } } If ($RadioButton3.Checked -AND (!($RadioButton2.Checked))) { $PathToSet = $PathToSet + "\Temp" Set-MyVariable -Action:"Set" -Name:"TEMP" -Value:"$PathToSet" -SetUserVariableTarget:$False -SHowUI Set-MyVariable -Action:"Set" -Name:"TMP" -Value:"$PathToSet" -SetUserVariableTarget:$False -SHowUI # Clear the variables Set-MyVariable -Action:"Remove" -Name:"AlastriTempWorkingDirectory" -SetUserVariableTarget:$True -SHowUI } Create-Folder -Name:"$PathToSet" -SHowUI } If ($RadioButton4.Checked -AND (!($RadioButton1.Checked))) { # Clear and/or reset the variables Set-MyVariable -Action:"Remove" -Name:"AlastriTempWorkingDirectory" -SetUserVariableTarget:$True -SHowUI $CurrentTemp = [System.IO.Path]::GetTempPath().TrimEnd('\') If ($CurrentTemp -ne "${env:LOCALAPPDATA}\Temp") { Set-MyVariable -Action:"Set" -Name:"TEMP" -Value:"${env:LOCALAPPDATA}\Temp" -SetUserVariableTarget:$False -SHowUI Set-MyVariable -Action:"Set" -Name:"TMP" -Value:"${env:LOCALAPPDATA}\Temp" -SetUserVariableTarget:$False -SHowUI } } # Refresh environment variables EnvironmentRefresh # Launching the app Launch-App -AppName:"$AppName" -AppExecutable:"$AppExecutable" -SHowUI -UICloseInSeconds:10 }) $form.AcceptButton = $action1Button $form.Controls.Add($action1Button) #------------------------------------------------------------- # Enable and start the timer $GuiIdleTimer.Enabled = $true $GuiIdleTimer.Start() $timeinstringformat = (Get-Date).AddMinutes($CloseAfterInMinutes).ToShortTimeString() $StatusMessage = "If left idle, this form will automatically close $($CloseAfterInMinutes) minutes from now at $timeinstringformat." $status1TextBox.AppendText("$StatusMessage `r`n") $status1TextBox.ScrollToCaret() write-verbose "$StatusMessage" -verbose $form.Topmost = $true $form.ShowDialog() | out-null #This starts the GUI } #------------------------------------ # Call the function to return all available RAM Disks $RamDiskResults = (Get-RamDisks) $RAMDiskFound = $RamDiskResults.RAMDiskFound $RAMDISKS = $RamDiskResults.RAMDISKS $PreferredRAMDisk = $RamDiskResults.PreferredRAMDisk #------------------------------------ # Start the appropriate function depending on whether or not the RAM Disk # software is installed and also the value of the $ShowUI variable. If ($RAMDiskFound) { If ($ShowUI) { If (![String]::IsNullOrEmpty($DevelopedBy)) { $Release = "$Release developed by $DevelopedBy" } Call-MainForm -ToolRelease:"$Release" -RAMDISKS:$RAMDISKS -PreferredRAMDisk:$PreferredRAMDisk -ChangeTEMP:$ChangeTEMP -TempChildFolder:"$TempChildFolder" -AppName:"$AppName" -AppExecutable:"$AppExecutable" -IconFile:"$IconFile" } Else { Call-Silent -PreferredRAMDisk:$PreferredRAMDisk -ChangeTEMP:$ChangeTEMP -TempChildFolder:"$TempChildFolder" -AppName:"$AppName" -AppExecutable:"$AppExecutable" } } Else { Call-NoRAMDisk -AppName:"$AppName" -AppExecutable:"$AppExecutable" } #------------------------------------ $EndDTM = (Get-Date) Write-verbose "Elapsed Time: $(($EndDTM-$StartDTM).TotalSeconds) Seconds" -Verbose Write-Verbose "Elapsed Time: $(($EndDTM-$StartDTM).TotalMinutes) Minutes" -Verbose try { Stop-Transcript } catch { Write-Verbose "$(Get-Date): This host does not support transcription" }
This is the cool stuff I build and implement to reduce operational issues and provide an improved user experience. I put a lot of personal time and research into this and feel it’s important to share for the wider community to use.
Enjoy!