<# This script will create a RAM Disk at either System Startup or via a User initiated Scheduled Task using the Arsenal Image Mounter command line. This was written using Arsenal Image Mounter version 3.11.x. When creating a RAM disk, it automatically takes the next available drive letter after C:, labels it as "RAM disk", and formats it as an NTFS drive. It will fail to create a RAM disk if there is not enough RAM available. The script will set a default size for the RAM disk if the Size parameter is omitted. This is based on the ammount of total physical RAM in the system: - If total physical RAM is greater than or equal to 160GB, it will create a Ram disk of size 32GB - If total physical RAM is greater than or equal to 128GB, it will create a Ram disk of size 24GB - If total physical RAM is greater than or equal to 64GB, it will create a Ram disk of size 16GB - If total physical RAM is greater than or equal to 48GB, it will create a Ram disk of size 8GB - If total physical RAM is less than 48GB, it will create a Ram disk of size 4GB After the default creation of a RAM disk permissions are pretty loose: - Administrators - Full Control - This folder, subfolders and files - SYSTEM - Full Control - This folder, subfolders and files - CREATOR OWNER - Full Control - This folder, subfolders and files - Everyone - Full Control - This folder, subfolders and files - Local Administrator (SID of S-1-5-21-1234567890-1234567890-1234567890-500) is the Owner Will look at locking that down further, specifically for the RDS servers to avoid users modifying and deleting each other's data. However, we do lock down the permissions on the "IMPORTANT Read Me.txt" file so that users cannot delete it. Each time a new RAM disk is created a new "Arsenal Virtual SCSI Disk Device" is added as a Disk drive. These are removed when the associated RAM disk is removed. This script uses the "aim_cli.exe" command line tool. I will look to re-write this using the API should the command line tool be too limiting for what needs to be achieved. I have found that either for new VMs that have not yet created a Write-Cache drive, or if a Write-Cache drive on an existing VM is deleted so it gets recreated, we must wait for the "LIC_BISF_Device_Personalize" Scheduled Task to complete at startup before creating a RAM disk. Even though the RAM disk is correctly created as E:, the BISF process will change the letter to D: and fail to create the Write-Cache drive as intended. This is intentional. So instead of changing BISF behaviour, it's allowed for in this script. Syntax: .\CreateRAMDisk.ps1 -Action: -Size: -RAMDiskStartingFrom: Where... - Action is either Create or Remove. If the action is remove, it deletes all existing RAM disks. - Size the size of the ram disk. Size in bytes, can be suffixed with for example M or G for MB or GB. - RAMDiskStartingFrom is the drive letter you want the RAM Disk to start from. This script will then reserve unused the letters between C and the starting letter. This allows for consistency with the way the RAM Disk is created. Version 1.4 - The first public release. Version 1.5 - Added the Get-UpTime function. This starts to phase out the reliance on the Get-WmiObject cmdlet, with preference to use the Get-CimInstance cmdlet. - Addressed an issue comparing System.String object to System.DateTime object caused by the way I was referencing the LastRunTime property from the Get-ScheduledTaskInfo output. - Added a check to see if sysprep is running to prevent a RAM Disk from being created during the sysprep process. Version 1.6 - Added a check to see if it is a Citrix MCS Master Image to prevent a RAM Disk from being created. The script will check the following value: Key: HKEY_LOCAL_MACHINE\SOFTWARE\Citrix\Configuration Type: DWORD Value: MasterImage Data: 1 This is important so that a RAM Disk does not interfere with the image preparation process. Script name: CreateRAMDisk.ps1 Release 1.6 Written by Jeremy Saunders (jeremy@jhouseconsulting.com) 17th February 2024 Modified by Jeremy Saunders (jeremy@jhouseconsulting.com) 13th November 2025 #> #------------------------------------------------------------- [cmdletbinding()] param( [Parameter(Mandatory = $true)] [ValidateSet("Create", "Remove")] [string]$Action, [string]$Size, [string]$RAMDiskStartingFrom="E" ) # Set Powershell Compatibility Mode Set-StrictMode -Version 2.0 # Enable verbose, warning and error mode $VerbosePreference = 'Continue' $WarningPreference = 'Continue' $ErrorPreference = 'Continue' $StartDTM = (Get-Date) #------------------------------------------------------------- $invalidChars = [io.path]::GetInvalidFileNamechars() $datestampforfilename = ((Get-Date -format s).ToString() -replace "[$invalidChars]","-") # Get the script path $ScriptPath = {Split-Path $MyInvocation.ScriptName} $ScriptPath = $(&$ScriptPath) $ScriptName = [System.IO.Path]::GetFilenameWithoutExtension($MyInvocation.MyCommand.Path.ToString()) $Logfile = "$ScriptName-$($datestampforfilename).log" $logPath = "${env:TEMP}" try { Start-Transcript "$logPath\$logFile" } catch { Write-Verbose "This host does not support transcription" } #------------------------------------------------------------- # Set the path for the Arsenal Image Mounter CLI (AIM CLI) $Executable = "${env:ProgramFiles}\Arsenal Image Mounter\aim_cli.exe" $ReadMe=@" IMPORTANT INFORMATION --------------------- - This RAM disk is to be used as a temporary scratch space ONLY. - ALL data on this drive is lost when the system reboots (application is closed) or the RAM disk is deleted. - To reduce risk and avoid data loss please ensure your data is always safely stored on a network file share. "@ #------------------------------------------------------------- Function Get-UpTime { # This function will get the uptime of the machine, returning both the LastBootUpTime in Date/Time # format, and also as a TimeSpan. param ( [switch]$UseWinRM, [int]$WinRMTimeoutSec=30 ) $ResultProps = @{ LBTime = $null TimeSpan = $null Success = $False } Try { If ($UseWinRM) { # LastBootUpTime from Get-CimInstance is already been converted to Date/Time $LBTime = (Get-CimInstance -ClassName Win32_OperatingSystem -OperationTimeoutSec $WinRMTimeoutSec -ErrorAction Stop).LastBootUpTime } Else { $LBTime = [Management.ManagementDateTimeConverter]::ToDateTime((Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop).LastBootUpTime) } If ($LBTime -ne $null) { [TimeSpan]$uptime = New-TimeSpan $LBTime $(get-date) $ResultProps.LBTime = $LBTime $ResultProps.TimeSpan = $uptime $ResultProps.Success = $True } } Catch { $($_.Exception.Message) } return $ResultProps } Function Wait-For-Scheduled-Task { param( [Parameter(Mandatory = $true)] [string]$TaskName, [datetime]$LastBootTime ) $IsComplete = $False write-verbose "Checking the current status of the `"$TaskName`" Scheduled Task..." -verbose try { $task = Get-ScheduledTask -TaskName "$TaskName" -ErrorAction Stop If ($null -ne $task) { write-verbose "- A Scheduled Task with name `"$TaskName`" was found." -verbose $exitwhencomplete = $False $Interval = 1 Do { $LastRunTime = Get-ScheduledTaskInfo -TaskName "$TaskName" -ErrorAction Continue | Select-Object -ExpandProperty LastRunTime if ($null -ne $LastRunTime) { write-verbose "- The task was last run on $($LastRunTime.DateTime)" -verbose If ($LastRunTime -ge $LastBootTime) { Write-Verbose "- Task `"$TaskName`" has run since startup." -verbose $TaskState = Get-ScheduledTask -TaskName "$TaskName" -ErrorAction Continue | Select-Object -ExpandProperty State If ($TaskState -ne $null) { If ($TaskState -eq "Ready") { $exitwhencomplete = $True Write-Verbose "- Task `"$TaskName`" is in a Ready state." -verbose $IsComplete = $True } ElseIf ($TaskState -eq "Running") { Write-Verbose "- Task `"$TaskName`" is still running." -verbose } Else { Write-Verbose "- Task `"$TaskName`" is in a $($TaskState) state" -verbose } } } else { Write-Verbose "- Task `"$TaskName`" has not run since startup." -verbose } } Else { Write-Verbose "- Task `"$TaskName`" has not yet run." -verbose } Start-Sleep -Seconds $Interval } Until ($exitwhencomplete -eq $true) } else { Write-Verbose "- Task `"$TaskName`" not found." -verbose $IsComplete = $True } } catch { if ($_.Exception -like "*No MSFT_ScheduledTask objects found*") { write-verbose "- A Scheduled Task with name `"$TaskName`" was not found." -verbose } Else { write-verbose "- $($_.Exception)" -verbose } $IsComplete = $True } return $IsComplete } Function StartProcess { param ( [string]$FilePath, [array]$Arguments ) write-verbose "Starting the process: `"$FilePath`"" -verbose write-verbose "with the arguments: $Arguments" -verbose $pinfo = New-Object System.Diagnostics.Process $pinfo.StartInfo.FileName = $FilePath # WindowStyle; 1 = hidden, 2 =maximized, 3=minimized, 4=normal $pinfo.StartInfo.WindowStyle = 1 $pinfo.StartInfo.Arguments = $Arguments $pinfo.StartInfo.RedirectStandardError = $True $pinfo.StartInfo.RedirectStandardOutput = $True $pinfo.StartInfo.UseShellExecute = $false Try { $null = $pinfo.start() | Out-Null $result = $pinfo.StandardOutput.ReadToEnd().Trim().Split("`r`n") $result += $pinfo.StandardError.ReadToEnd().Trim().Split("`r`n") } Catch { # } $pinfo.Dispose() return $result } 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-NextAvailableDriveLetter { param( [parameter(Mandatory=$False)][Switch]$Descending ) # Get all available drive letters, and store in a temporary variable. $UsedDriveLetters = @(Get-Volume | % { "$([char]$_.DriveLetter)"}) + @(Get-WmiObject -Class Win32_MappedLogicalDisk| %{$([char]$_.DeviceID.Trim(':'))}) + @(Get-WmiObject -Class Win32_LogicalDisk| %{$([char]$_.DeviceID.Trim(':'))}) | Select-Object -Unique $TempDriveLetters = @(Compare-Object -DifferenceObject $UsedDriveLetters -ReferenceObject $( 67..90 | % { "$([char]$_)" } ) | ? { $_.SideIndicator -eq '<=' } | % { $_.InputObject }) If (!$Descending) { $AvailableDriveLetter = ($TempDriveLetters | Sort-Object) } Else { $AvailableDriveLetter = ($TempDriveLetters | Sort-Object -Descending) } Return $AvailableDriveLetter[0] } $FirstAvailableDriveLetter = Get-NextAvailableDriveLetter # Create an array of drive letters to reserve $ReservedDriveLetters = @() If ($FirstAvailableDriveLetter -ne $RAMDiskStartingFrom) { # Convert uppercase letters to ASCII codes $FirstAvailableDriveLetterAscii = [int][char]$FirstAvailableDriveLetter.ToUpper() $RAMDiskStartingFromAscii = [int][char]$RAMDiskStartingFrom.ToUpper() # Loop through ASCII values and convert back to letters For ($i = $FirstAvailableDriveLetterAscii; $i -le $RAMDiskStartingFromAscii -1; $i++) { $currentLetter = [char]$i $ReservedDriveLetters += $currentLetter } } #----------------------------------- [int]$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) { [int]$TotalRam = $ComputerInformation.totalphysicalram Write-Verbose "Total RAM: $TotalRAM GB" -verbose } $DefaultSize = "4G" If ($TotalRam -ge 48 ) { $DefaultSize = "8G" } If ($TotalRam -ge 64 ) { $DefaultSize = "16G" } If ($TotalRam -ge 128 ) { $DefaultSize = "24G" } If ($TotalRam -ge 160 ) { $DefaultSize = "32G" } if ([String]::IsNullOrEmpty($Size)) { $Size = $DefaultSize } #----------------------------------- $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 } #----------------------------------- If ($IsDriverInstalled -AND (Test-Path -Path "$Executable")) { # Get all RAM Disks $AllRamDisks = StartProcess -FilePath:"$Executable" -Arguments:"--list" If ($AllRamDisks -Like "*No virtual disks*") { write-verbose "No virtual disks mounted" -verbose } $DriveLetters = @() $DeviceNumbers = @() $DeviceCount = 0 ForEach ($Line in $AllRamDisks) { $DeviceNumber = "" If ($Line -like "*Device number*") { $DeviceCount ++ $DeviceNumber = ($Line.Trim() -Split("Device number"))[1].Trim() } if (!([String]::IsNullOrEmpty($DeviceNumber))) { $DeviceNumbers += $DeviceNumber #$DeviceNumber } $DeviceIs = "" If ($Line -like "*Device is*") { $DeviceIs = ($Line.Trim() -Split("Device is"))[1].Trim() } if (!([String]::IsNullOrEmpty($DeviceIs))) { #$DeviceIs } $ContainsVolume = "" If ($Line -like "*Contains volume*") { $ContainsVolume = ($Line.Trim() -Split("Contains volume"))[1].Trim() } if (!([String]::IsNullOrEmpty($ContainsVolume))) { #$ContainsVolume } $MountedAt = "" If ($Line -like "*Mounted at*") { $MountedAt = ($Line.Trim() -Split("Mounted at"))[1].Trim() } if (!([String]::IsNullOrEmpty($MountedAt))) { $DriveLetters += $MountedAt #$MountedAt } } If ($DeviceCount -gt 0) { If ($DeviceCount -eq 1) { write-verbose "There is $DeviceCount device mounted" -verbose } Else { write-verbose "There are $DeviceCount devices mounted" -verbose } If (($DriveLetters | Measure-Object).Count -gt 0) { write-verbose "The following RAM disks are present..." -verbose ForEach ($DriveLetter in $DriveLetters) { write-verbose "- $DriveLetter" -verbose } } } Else { write-verbose "There are no devices mounted" -verbose } #------------------------------------------------------------- If ($Action -eq "Remove") { ForEach ($DeviceNumber in $DeviceNumbers) { write-verbose "Removing device $DeviceNumber" -verbose $MyArgs = "--dismount=$DeviceNumber --force" $RemoveDevices = StartProcess -FilePath:"$Executable" -Arguments:$MyArgs If ($RemoveDevices -Like "*All devices dismounted*") { write-verbose "All devices have been deleted" -verbose } If ($RemoveDevices -Like "*No mounted devices*") { write-verbose "There are no devices to delete" -verbose } } } #------------------------------------------------------------- $IsSysprepRunning = $False If (Get-Process -Name sysprep -ErrorAction SilentlyContinue) { $IsSysprepRunning = $True If ($Action -eq "Create") { write-verbose "Sysprep is running. A RAM Disk will not be created!" -verbose } } #------------------------------------------------------------- $IsMCSMasterImage = $False $ErrorActionPreference = "stop" try { If ((Get-ItemProperty -Path "HKLM:\SOFTWARE\Citrix\Configuration" | Select-Object -ExpandProperty "MasterImage") -ne $null) { [int]$MasterImageValue = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Citrix\Configuration" -Name "MasterImage").MasterImage If ($MasterImageValue -eq 1) { $IsMCSMasterImage = $True } } } catch { # } $ErrorActionPreference = "Continue" If ($IsMCSMasterImage) { If ($Action -eq "Create") { write-verbose "This Citrix VDA is the MCS Master Iamge. A RAM Disk will not be created!" -verbose } } #------------------------------------------------------------- If ($Action -eq "Create" -AND $IsSysprepRunning -eq $False -AND $IsMCSMasterImage -eq $False) { # Waiting for the BISF Personalize scheduled task to complete. # There may be circumstances when WinRM fails if is being reconfigured and or restarted at computer startup. # This may happen if there is a conflicting startup script running in your environment. So we just loop until # it's successful. $LastBootTime = $null Do { $GetUpTime = Get-UpTime -UseWinRM:$True If ($GetUpTime.Success) { $LastBootTime = $GetUpTime.LBTime } Else { write-warning "Could not retrieve the LastBootTime. Will try again in 1 second!" -verbose Start-Sleep -Milliseconds 1000 } } Until ($GetUpTime.Success) write-verbose "This host was last booted on $($LastBootTime.ToString("dddd, dd MMMM yyyy hh:mm:ss tt"))" -verbose $IsTaskComplete = Wait-For-Scheduled-Task -TaskName:"LIC_BISF_Device_Personalize" -LastBootTime:$LastBootTime write-verbose "Proceeding to create RAM disk..." -verbose $RemoveSubstDrives = @() $Reserved = $False ForEach ($ReservedDriveLetter in $ReservedDriveLetters) { If ((IsAccessAllowed -Folder "${ReservedDriveLetter}:\") -eq $False) { write-verbose "Reserving drive letter $ReservedDriveLetter" -verbose subst "${ReservedDriveLetter}:" "${env:TEMP}" $RemoveSubstDrives += $ReservedDriveLetter $Reserved = $True } } If ($Reserved) { Start-Sleep -Seconds 5 } write-verbose "Creating a new Ram disk of size $Size ..." -verbose $UnattendedArgsAdd = "--ramdisk --disksize=`"$Size`"" $CreateDevice = StartProcess -FilePath:"$Executable" -Arguments:$UnattendedArgsAdd $MountedAt = "" ForEach ($Line in $CreateDevice) { If ($Line -like "*Mounted at*") { $MountedAt = ($Line.Trim() -Split("Mounted at"))[1].Trim() } } if (!([String]::IsNullOrEmpty($MountedAt))) { write-verbose "Successfully created the $MountedAt drive as a Ram disk" -verbose write-verbose "Writing the `"${MountedAt}IMPORTANT Read Me.txt`" file" -verbose $ReadMe | Out-File -FilePath "${MountedAt}IMPORTANT Read Me.txt" -Encoding ASCII # Define the access rule to grant read and execute permissions to Users $userAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule( "Users", "ReadAndExecute", "Allow" ) # Define the access rule to grant full control to SYSTEM and Administrators $systemAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule( "SYSTEM", "FullControl", "Allow" ) # Define the access rule to grant full control to SYSTEM and Administrators $adminAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule( "Administrators", "FullControl", "Allow" ) # Get the local Administrator account $adminAccount = [System.Security.Principal.NTAccount]::new("Administrators") # Get the current access control list (ACL) for the file $acl = Get-Acl -Path "${MountedAt}IMPORTANT Read Me.txt" # Break inheritance $acl.SetAccessRuleProtection($true, $false) # Remove any inherited access rules foreach ($accessRule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) { if ($accessRule.IsInherited) { $acl.RemoveAccessRule($accessRule) } } # Remove any existing access rules $acl.Access | ForEach-Object { $acl.RemoveAccessRule($_) } # Add the new access rules to the ACL $acl.SetAccessRule($userAccessRule) $acl.SetAccessRule($systemAccessRule) $acl.SetAccessRule($adminAccessRule) # Set the owner of the file to the local Administrator account $acl.SetOwner($adminAccount) # Set the modified ACL back to the file Set-Acl -Path "${MountedAt}IMPORTANT Read Me.txt" -AclObject $acl } Else { write-warning "Failed to created a Ram disk!" -verbose } ForEach ($RemoveSubstDrive in $RemoveSubstDrives) { subst "${RemoveSubstDrive}:" /D } } #------------------------------------------------------------- } Else { If ($IsDriverInstalled -eq $False) { write-warning "The Arsenal Image Mounter SCSI device is not installed" -verbose } If (-not(Test-Path -Path "$Executable")) { write-warning "The `"$Executable`" tool cannot be found" -verbose } } #------------------------------------------------------------- $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 "This host does not support transcription" }