Silently Installing and Automating the Arsenal Image Mounter (AIM) RAM Disk Feature

by Jeremy Saunders on February 13, 2025

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.

RAM Disk Scheduled Tasks

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.

RAM Disk Read Me

  • 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.

Arsenal Image Mounter Installation Source Folder

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!

Jeremy Saunders

Jeremy Saunders

Technical Architect | DevOps Evangelist | Software Developer | Microsoft, NVIDIA, Citrix and Desktop Virtualisation (VDI) Specialist/Expert | Rapper | Improvisor | Comedian | Property Investor | Kayaking enthusiast at J House Consulting
Jeremy Saunders is the Problem Terminator. He is a highly respected IT Professional with over 35 years’ experience in the industry. Using his exceptional design and problem solving skills with precise methodologies applied at both technical and business levels he is always focused on achieving the best business outcomes. He worked as an independent consultant until September 2017, when he took up a full time role at BHP, one of the largest and most innovative global mining companies. With a diverse skill set, high ethical standards, and attention to detail, coupled with a friendly nature and great sense of humour, Jeremy aligns to industry and vendor best practices, which puts him amongst the leaders of his field. He is intensely passionate about solving technology problems for his organisation, their customers and the tech community, to improve the user experience, reliability and operational support. Views and IP shared on this site belong to Jeremy.
Jeremy Saunders
Jeremy Saunders

Previous post:

Next post: