I previously wrote about The best free for commercial use RAM Disk that works perfectly with Desktop Virtualisation. This is a follow on from that article that will focus on:
- The installation of Arsenal Image Mounter (AIM)
- The automation for the creation of a RAM Disk
- The challenges I experienced with the creation of the RAM Disk
- How to deploy it
- The scripts themselves
Here are further articles to demonstrate the use cases:
Installation of Arsenal Image Mounter (AIM)
There is no silent installation process available, so you either need to use a combination of the AppActivate Method with the SendKeys Class or use the AutoIt PowerShell CmdLets. I personally prefer to use AutoIt, but the choice is yours. I like to deploy the AutoItX (DLL/COM control) to all images I build which allows me to easily leverage the PowerShell CmdLets as needed. On the Arsenal Image Mounter FAQs page they do state that they can provide a package that can be installed silently, but does have a caveat with regards to the installation of the drivers, which means that you may still need to use AppActivate and SendKeys. Given that I was using it for free, I didn’t want to waste their time trying this out as I was comfortable with my method of installation, which works perfectly well.
The basic steps are:
- Microsoft .NET Desktop Runtime 8.0.x is a prerequisite for Arsenal Image Mounter from version 3.11.279 to 3.11.293
- Microsoft .NET Desktop Runtime 9.0.x is a prerequisite for Arsenal Image Mounter from version 3.11.299 and above
- Download Arsenal Image Mounter, extract the zip to “C:\Program Files\Arsenal Image Mounter”
- Execute ArsenalImageMounter.exe to install.
- Once the installation is complete you can disable unnecessary services, if using it for a RAM Disk service only.
- Stop and disable the VHD image file access driver service.
- Stop and disable the AWE memory allocation driver service.
- Note that the dokan2 virtual file system driver service is required and cannot be stopped or disabled.
If using Citrix PVS, I wrote about “The Citrix PVS Target Device Driver is a SCSI Adapter Bully, which is something you need to consider pre and post installation to ensure that the Arsenal Image Mounter SCSI Adapter does not install with a SCSI ADAPTER instance of 0000.
Automation for the creation of a RAM Disk
As a RAM Disk cannot be created under the context of a Standard User, I found that the best way to implement this was via Scheduled Tasks that runs under the local System account. For my use case I create 7 Scheduled Tasks. 6 of them can be excluded via the installation script using the $CreateMainTaskOnly variable if you don’t feel the need to use them. But for me they come in handy as I will explain.
The main task will create a RAM Disk with fixed memory allocation at computer startup based on the total physical RAM installed. As it allocates the RAM dynamically, it will not consume RAM until it is used. The following logic is used to determine the size of the RAM Disk.
- 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.
There are 6 manual tasks with permissions set so that a standard user can run them if you allow them to run the Task Scheduler MMC snap-in.
- Create a 4GB RAM Disk with fixed memory allocation.
- Create a 8GB RAM Disk with fixed memory allocation.
- Create a 16GB RAM Disk with fixed memory allocation.
- Create a 24GB RAM Disk with fixed memory allocation.
- Create a 32GB RAM Disk with fixed memory allocation.
- Remove ALL existing RAM Disks.
Whilst it may seem excessive, it created a great number of options which is handy for the more advanced Geoscience users I work with. Again, you can use the $CreateMainTaskOnly variable if you don’t have a need for them.
The Challenges
- Providing a method that works under the context of a Standard User (non-Administrator) was easy to overcome using Scheduled Tasks as documented above.
- As I automate all my image builds, I didn’t want the main Scheduled Task to create a RAM Disk on each restart, which would have continually interfered with the build process. So that task is set to Disabled by default. I have a final script that runs at the end of the build process that then Enables this task in the image. However, you can also set the $EnableMainTask variable to True in the installation script, which will automatically Enable the task on creation.
- When creating a RAM Disk, it automatically takes the next available drive letter after C, formats it as an NTFS drive, and labels it as “RAM disk”. The formatting and labelling are perfect, but it was frustrating that you cannot specify a drive letter, especially to ensure consistency. For example, in a Citrix PVS deployment, drive D is typically the write-cache drive, so I wanted to ensure that the RAM Disk always started from letter E. This provided consistency so the users knew to always use E. The only way to achieve this was to reserve drive letter(s) by using the SUBST command to temporarily block lower driver letters, or in this case D. Once the RAM Disk is created, the temporary drive letters are removed.
- I then found that either for new VMs that have not yet created a Citrix PVS Write-Cache drive, or if a Write-Cache drive on an existing VM is deleted so it gets recreated on next reboot, we must wait for the Base Image Script Framework (BIS-F) “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 BIS-F process will change the letter to D and fail to create the Write-Cache drive as intended. Looking through the BIS-F code, this seems intentional. So instead of changing BIS-F behaviour, I have allowed for in my process. But don’t worry, if the BIS-F “LIC_BISF_Device_Personalize” Scheduled Task cannot be found, the process assumes that you are not using BIS-F and the RAM Disk creation proceeds.
- I found the API documentation difficult to work through for the RAM Disk functionality that I needed. That’s no reflection on Arsenal Recon. So I simply just used the “aim_cli.exe” command line tool they provide as part of the AIM package. It was perfect for what I needed. I may look to re-write this using the API in the future.
- I also noted that from v3.11.299 (released 13th February 2025) there is new PowerShell modules for more reliable CLI functionality when using PowerShell versus Command Prompt. This in itself may be worth exploring further.
- I wanted to ensure users understood it’s purpose, so once the RAM Disk is created, it writes a “IMPORTANT Read Me.txt” file to the root of the RAM Disk with contents that explains its purpose and the risk. The NTFS permissions on this file is locked down so users cannot delete it.
- I wasn’t concerned with the standard NTFS permissions on the root of the RAM Disk, as this is mostly used on single session VDI hosts. But as it also works in multisession hosts, I may revisit the NTFS permissions in the future.
How to Deploy it?
I have provided 2 PowerShell scripts as documented below. Install.ps1 and CreateRAMDisk.ps1. As per the following screen shot, download them and place them in a folder together. Extract the downloaded source into a subfolder with the corresponding version number, and set the version number in the Install.ps1 script. Then just execute the Install.ps1 script.
The Scripts
Here is the Install.ps1 (8 downloads) script that installs Arsenal Image Mounter and creates the Scheduled Tasks.
<# This script will... - Copy and then install the Arsenal Image Mounter (AIM) software - Copy the CreateRAMDisk.ps1 PowerShell script - Create the Scheduled Tasks Script variables: - Setting the $CreateTasksOnly value to True will skip the installion and only create the Scheduled Tasks. - Setting the $CreateMainTaskOnly value to True will create the main (1st) Scheduled Task only. - Setting the $EnableMainTask value to True will Enable the main (1st) Scheduled Task. It's Disabled by default. Script name: Install.ps1 Release 1.0 Written by Jeremy Saunders (jeremy@jhouseconsulting.com) 17th February 2024 #> #------------------------------------------------------------- param( [switch]$CreateTasksOnly, [switch]$CreateMainTaskOnly, [switch]$EnableMainTask ) # Set Powershell Compatibility Mode Set-StrictMode -Version 2.0 # Enable verbose, warning and error mode $VerbosePreference = 'Continue' $WarningPreference = 'Continue' $ErrorPreference = 'Continue' #------------------------------------------------------------- Write-Verbose "Setting Arguments" -Verbose $StartDTM = (Get-Date) # Get the current script path $ScriptPath = {Split-Path $MyInvocation.ScriptName} $ScriptPath = $(&$ScriptPath) $Vendor = "Arsenal Recon" $Product = "Arsenal Image Mounter" $PackageName = "ArsenalImageMounter" $Version = "3.11.300" $InstallerType = "exe" $LogPS = "${env:SystemRoot}" + "\Temp\$Vendor $Product $Version PS Wrapper.log" Start-Transcript $LogPS # Bypass the "Open File Security Warning" dialog box. # For more information refer to http://support.microsoft.com/kb/889815 $env:SEE_MASK_NOZONECHECKS = 1 $InstallFolder = "${env:ProgramFiles}\Arsenal Image Mounter" $PrerequisiteMet = $False If ($CreateTasksOnly) { $PrerequisiteMet = $True } $CreateTask = $False # Create the folder structure If (-not(Test-Path -Path "$InstallFolder")) { Write-Verbose "Creating the Folder Path: `"$InstallFolder`"" -Verbose New-Item -Path "$InstallFolder" -ItemType Directory | Out-Null } If ($CreateTasksOnly -eq $False) { # Push the current location onto a location stack and then change the current location to the location specified Push-Location "$ScriptPath\$Version" # Copy the files into place copy-item -path "$ScriptPath\$Version\*" -Destination "$InstallFolder\" -Recurse -Force -Verbose # Change the current location back to the location most recently pushed onto the stack, which will be defined by the $ScriptPath variable Pop-Location # Push the current location onto a location stack and then change the current location to the location specified Push-Location "$InstallFolder" If (Test-Path "${Env:ProgramFiles(x86)}\AutoIt3\AutoItX\AutoItX3.psd1") { Write-Verbose "Starting Installation of $Vendor $Product $Version" -Verbose (Start-Process "$PackageName.$InstallerType") # Import the module manfiest Import-Module "${Env:ProgramFiles(x86)}\AutoIt3\AutoItX\AutoItX3.psd1" $WindowCount = 0 #################################### # Wait for window and get the handle $WindowTitle = "Arsenal Image Mounter" $WindowText = "This application requires .NET Desktop Runtime" $Return = Wait-AU3Win -Title $WindowTitle -Text $WindowText -Timeout 5 If ($Return -eq 1) { $WindowCount ++ # Get the handle $winHandle = Get-AU3WinHandle -Title $WindowTitle -Text $WindowText write-verbose "Progress:" -verbose write-verbose "- Window Title: $WindowTitle" -verbose write-verbose "- Window Text: $WindowText" -verbose write-verbose "- Window Count: $WindowCount" -verbose # Activate the window Show-AU3WinActivate -WinHandle $winHandle # Send some keystrokes Send-AU3Key "!n" write-warning "Please install the .NET Desktop Runtime prerequisite" } Else { $PrerequisiteMet = $True # Wait for window and get the handle $WindowTitle = "StartupWindow" $WindowText = "" $WindowCount ++ # Wait for window and get the handle Wait-AU3Win -Title $WindowTitle -Text $WindowText $winHandle = Get-AU3WinHandle -Title $WindowTitle -Text $WindowText write-verbose "Progress:" -verbose write-verbose "- Window Title: $WindowTitle" -verbose write-verbose "- Window Text: $WindowText" -verbose write-verbose "- Window Count: $WindowCount" -verbose # Activate the window Show-AU3WinActivate -WinHandle $winHandle # Send some keystrokes Send-AU3Key "{TAB 5}" Send-AU3Key "{ENTER}" #################################### $WindowTitle = "Arsenal Image Mounter" $WindowText = "This application requires a virtual SCSI miniport driver to create virtual disks" $Return = Wait-AU3Win -Title $WindowTitle -Text $WindowText -Timeout 5 If ($Return -eq 1) { $WindowCount ++ # Wait for window and get the handle Wait-AU3Win -Title $WindowTitle -Text $WindowText $winHandle = Get-AU3WinHandle -Title $WindowTitle -Text $WindowText write-verbose "Progress:" -verbose write-verbose "- Window Title: $WindowTitle" -verbose write-verbose "- Window Text: $WindowText" -verbose write-verbose "- Window Count: $WindowCount" -verbose # Activate the window Show-AU3WinActivate -WinHandle $winHandle # Send some keystrokes Send-AU3Key "!y" #################################### # Wait for window and get the handle $WindowTitle = "Driver Setup" $WindowText = "" $WindowCount ++ # Wait for window and get the handle Wait-AU3Win -Title $WindowTitle -Text $WindowText $winHandle = Get-AU3WinHandle -Title $WindowTitle -Text $WindowText write-verbose "Progress:" -verbose write-verbose "- Window Title: $WindowTitle" -verbose write-verbose "- Window Text: $WindowText" -verbose write-verbose "- Window Count: $WindowCount" -verbose # Activate the window Show-AU3WinActivate -WinHandle $winHandle # Send some keystrokes Send-AU3Key "{ENTER}" } #################################### # Wait for window and get the handle $WindowTitle = "Arsenal Image Mounter" $WindowText = "" $WindowCount ++ # Wait for window and get the handle Wait-AU3Win -Title $WindowTitle -Text $WindowText $winHandle = Get-AU3WinHandle -Title $WindowTitle -Text $WindowText write-verbose "Progress:" -verbose write-verbose "- Window Title: $WindowTitle" -verbose write-verbose "- Window Text: $WindowText" -verbose write-verbose "- Window Count: $WindowCount" -verbose # Activate the window Show-AU3WinActivate -WinHandle $winHandle # Send some keystrokes Send-AU3Key "!f" Send-AU3Key "{UP 1}" Send-AU3Key "{ENTER}" #################################### write-verbose "Disabling unnecessary services..." -verbose write-verbose "- VHD image file access driver" -verbose Get-Service -Name vhdaccess | Stop-Service -Force Set-Service -Name vhdaccess -StartupType Disabled write-verbose "- AWE memory allocation driver" -verbose Get-Service -Name awealloc | Stop-Service -Force Set-Service -Name awealloc -StartupType Disabled } } # Change the current location back to the location most recently pushed onto the stack, which will be defined by the $ScriptPath variable Pop-Location } If ($PrerequisiteMet) { Write-Verbose "Customization" -Verbose # Copy the script in place to create the RAM Disk from a Scheduled Task If (TEST-PATH "$ScriptPath\CreateRAMDisk.ps1") { $CreateTask = $True copy-item -path "$ScriptPath\CreateRAMDisk.ps1" -Destination "$InstallFolder\" -Recurse -Force -Verbose } # Create the Scheduled Task If ($CreateTask) { # Scheduled Task 1 $ScheduledTask1 = New-Object -TypeName PSObject -Property @{ "TaskName" = "Create a RAM Disk with fixed memory allocation at Startup" "TaskDescription" = "This task will create a RAM Disk with fixed memory allocation at computer startup based on the total physical RAM installed. As it allocates the RAM dynamically, it will not consume RAM until it is used." "TaskCommand" = @(Get-Command powershell.exe)[0].Definition "TaskScript" = "$InstallFolder\CreateRAMDisk.ps1" "TaskArguments" = "-Action:Create" "EnableTask" = $False "AddTrigger" = $True "AllowUsersExecute" = $False } If ($EnableMainTask) { $ScheduledTask1.EnableTask = $True } If ($CreateMainTaskOnly -eq $False) { # Scheduled Task 2 $ScheduledTask2 = New-Object -TypeName PSObject -Property @{ "TaskName" = "Create a 4GB RAM Disk with fixed memory allocation" "TaskDescription" = "This task will create an 4GB RAM Disk with fixed memory allocation." "TaskCommand" = @(Get-Command powershell.exe)[0].Definition "TaskScript" = "$InstallFolder\CreateRAMDisk.ps1" "TaskArguments" = "-Action:Create -Size:4G" "EnableTask" = $True "AddTrigger" = $False "AllowUsersExecute" = $True } # Scheduled Task 3 $ScheduledTask3 = New-Object -TypeName PSObject -Property @{ "TaskName" = "Create a 8GB RAM Disk with fixed memory allocation" "TaskDescription" = "This task will create an 8GB RAM Disk with fixed memory allocation." "TaskCommand" = @(Get-Command powershell.exe)[0].Definition "TaskScript" = "$InstallFolder\CreateRAMDisk.ps1" "TaskArguments" = "-Action:Create -Size:8G" "EnableTask" = $True "AddTrigger" = $False "AllowUsersExecute" = $True } # Scheduled Task 4 $ScheduledTask4 = New-Object -TypeName PSObject -Property @{ "TaskName" = "Create a 16GB RAM Disk with fixed memory allocation" "TaskDescription" = "This task will create an 16GB RAM Disk with fixed memory allocation." "TaskCommand" = @(Get-Command powershell.exe)[0].Definition "TaskScript" = "$InstallFolder\CreateRAMDisk.ps1" "TaskArguments" = "-Action:Create -Size:16G" "EnableTask" = $True "AddTrigger" = $False "AllowUsersExecute" = $True } # Scheduled Task 5 $ScheduledTask5 = New-Object -TypeName PSObject -Property @{ "TaskName" = "Create a 24GB RAM Disk with fixed memory allocation" "TaskDescription" = "This task will create an 24GB RAM Disk with fixed memory allocation." "TaskCommand" = @(Get-Command powershell.exe)[0].Definition "TaskScript" = "$InstallFolder\CreateRAMDisk.ps1" "TaskArguments" = "-Action:Create -Size:24G" "EnableTask" = $True "AddTrigger" = $False "AllowUsersExecute" = $True } # Scheduled Task 6 $ScheduledTask6 = New-Object -TypeName PSObject -Property @{ "TaskName" = "Create a 32GB RAM Disk with fixed memory allocation" "TaskDescription" = "This task will create an 32GB RAM Disk with fixed memory allocation." "TaskCommand" = @(Get-Command powershell.exe)[0].Definition "TaskScript" = "$InstallFolder\CreateRAMDisk.ps1" "TaskArguments" = "-Action:Create -Size:32G" "EnableTask" = $True "AddTrigger" = $False "AllowUsersExecute" = $True } # Scheduled Task 7 $ScheduledTask7 = New-Object -TypeName PSObject -Property @{ "TaskName" = "Remove ALL existing RAM Disks" "TaskDescription" = "This task deletes ALL existing RAM Disks" "TaskCommand" = @(Get-Command powershell.exe)[0].Definition "TaskScript" = "$InstallFolder\CreateRAMDisk.ps1" "TaskArguments" = "-Action:Remove" "EnableTask" = $True "AddTrigger" = $False "AllowUsersExecute" = $False } # Create a hash table of Scheduled Tasks to be created $ScheduledTasksToCreate = @{ "Task1" = $ScheduledTask1 "Task2" = $ScheduledTask2 "Task3" = $ScheduledTask3 "Task4" = $ScheduledTask4 "Task5" = $ScheduledTask5 "Task6" = $ScheduledTask6 "Task7" = $ScheduledTask7 } } Else { # Create a hash table of Scheduled Tasks to be created $ScheduledTasksToCreate = @{ "Task1" = $ScheduledTask1 } } ForEach ($key in $ScheduledTasksToCreate.keys) { $taskName = $ScheduledTasksToCreate.$key.TaskName $taskDescription = $ScheduledTasksToCreate.$key.TaskDescription $TaskCommand = $ScheduledTasksToCreate.$key.TaskCommand $TaskScript = $ScheduledTasksToCreate.$key.TaskScript $TaskArguments = $ScheduledTasksToCreate.$key.TaskArguments $TaskArguments = '-Executionpolicy bypass -Command "& ' + " '" + $TaskScript + "' " + $TaskArguments + '"' $EnableTask = $ScheduledTasksToCreate.$key.EnableTask $AddTrigger = $ScheduledTasksToCreate.$key.AddTrigger $AllowUsersExecute = $ScheduledTasksToCreate.$key.AllowUsersExecute write-verbose "Task Name: $($taskName)" -verbose write-verbose "- Description: $($taskDescription)" -verbose write-verbose "- Command: $($TaskCommand)" -verbose write-verbose "- Script: $($TaskScript)" -verbose write-verbose "- Arguments: $($TaskArguments)" -verbose # Create the TaskService object. Try { [Object] $service = new-object -com("Schedule.Service") If (!($service.Connected)){ Try { $service.Connect() # Get a folder to create a task definition in # This is actually the %SystemRoot%\System32\Tasks folder. $rootFolder = $service.GetFolder("\") # Delete the task if already present $ScheduledTasks = $rootFolder.GetTasks(0) $Task = $ScheduledTasks | Where-Object{$_.Name -eq "$TaskName"} If ($Task -ne $Null){ Try { $rootFolder.DeleteTask($Task.Name,0) # 'Success' } Catch [System.Exception]{ # 'Exception Returned' } } Else { # "Task Not Found" } # Create the new task $taskDefinition = $service.NewTask(0) If ($AddTrigger) { # Create a registration trigger with a trigger type of (8) at startup $triggers = $taskDefinition.Triggers $trigger = $triggers.Create(8) $trigger.Id = "BootTriggerId" $trigger.Enabled = $true } # Create the action for the task to execute. $Action = $taskDefinition.Actions.Create(0) $Action.Path = $TaskCommand $Action.Arguments = $TaskArguments $Action.WorkingDirectory = "" # Register (create) the task. $Settings = $taskDefinition.Settings # Set the Task Compatibility to V2 (Windows 7/2008R2) $Settings.Compatibility = 3 $Settings.AllowDemandStart = $true $Settings.StopIfGoingOnBatteries = $false $Settings.DisallowStartIfOnBatteries = $false $regInfo = $taskDefinition.RegistrationInfo $regInfo.Description = $taskDescription $regInfo.Author = $Env:Username # Note that the task is created as an XML file under the %SystemRoot%\System32\Tasks folder # 6 == Task Create or Update # 5 == A Local System, Local Service, or Network Service account is being used as a security context to run the task. $rootFolder.RegisterTaskDefinition($taskName, $taskDefinition, 6, "System", $null , 5) | out-null "Scheduled Task Created Successfully" If (!($EnableTask)) { $rootFolder.GetTasks(0) | Where-Object{$_.Name -eq "$TaskName"} | ForEach-Object { "Disabled task" $_.Enabled = $False } } If ($AllowUsersExecute) { If (TEST-PATH "${env:SystemRoot}\System32\Tasks\$taskName") { Write-Verbose "Giving Users read and execute access to be able to lanuch the `"$taskName`" task" -verbose $result = Invoke-Expression -command "icacls.exe `"${env:SystemRoot}\System32\Tasks\$taskName`" /grant:r `"BUILTIN\Users:(RX)`" /Q /C 2>&1" $result } } } Catch [System.Exception]{ "Scheduled Task Creation Failed" $ExitCode = 1 } } } Catch [System.Exception]{ "Scheduled Task Creation Failed" $ExitCode = 1 } } } } # Enable File Security Remove-Item env:\SEE_MASK_NOZONECHECKS Write-Verbose "Stop logging" -Verbose $EndDTM = (Get-Date) Write-Verbose "Elapsed Time: $(($EndDTM-$StartDTM).TotalSeconds) Seconds" -Verbose Write-Verbose "Elapsed Time: $(($EndDTM-$StartDTM).TotalMinutes) Minutes" -Verbose Stop-Transcript
Here is the CreateRAMDisk.ps1 (6 downloads) script that is executed by the Scheduled Tasks to create/remove the RAM Disk(s).
<# 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:<action> -Size:<size> -RAMDiskStartingFrom:<letter> 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. Script name: CreateRAMDisk.ps1 Release 1.4 Written by Jeremy Saunders (jeremy@jhouseconsulting.com) 17th February 2024 Modified by Jeremy Saunders (jeremy@jhouseconsulting.com) 7th March 2024 #> #------------------------------------------------------------- [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 Wait-For-Scheduled-Task { param( [Parameter(Mandatory = $true)] [string]$TaskName ) $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 $LastBootTime = [Management.ManagementDateTimeConverter]::ToDateTime((Get-WmiObject -Class Win32_OperatingSystem).LastBootUpTime) write-verbose "- This host was last booted on $LastBootTime" -verbose 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.DateTime -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 } } } #------------------------------------------------------------- If ($Action -eq "Create") { # Waiting for the BISF Personalize scheduled task to complete. $IsTaskComplete = Wait-For-Scheduled-Task -TaskName:"LIC_BISF_Device_Personalize" 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" }
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!