param( [ValidateSet("Verify", "DryRun", "Apply")] [string]$Mode = "Verify", [string]$VmrunPath = "C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe", [string]$Host110Vmx = "", [string]$Host111Vmx = "", [string]$Host188Vmx = "", [string]$Host120Vmx = "", [string]$Host121Vmx = "", [string]$Host112Vmx = "", # Host 111 is a physical MacBook/Ollama node; keep it in host probes, not VMware guest inventory. [string[]]$RequiredVmAliases = @("110", "188", "120", "121", "112"), [switch]$EnableRecursiveDiscovery, [string[]]$DiscoveryRoot = @( "D:\Documents\Virtual Machines", "D:\Downloads", "D:\VMs", "E:\VMs", "C:\VMs", "C:\Users\Public\Documents\Virtual Machines" ) ) $ErrorActionPreference = "Stop" $TaskName = "AWOOOI-Start-VMware-VMs" $ProgramDataDir = "C:\ProgramData\AWOOOI" $StartScript = Join-Path $ProgramDataDir "Start-AWOOOI-VMs.ps1" $VmOrder = @($RequiredVmAliases) $SuppliedVmx = @{ "110" = $Host110Vmx "111" = $Host111Vmx "188" = $Host188Vmx "120" = $Host120Vmx "121" = $Host121Vmx "112" = $Host112Vmx } $KnownVmxCandidates = @{ "110" = @("D:\Documents\Virtual Machines\Ubuntu 64-bit (3)\Ubuntu 64-bit (3).vmx") "111" = @("D:\Documents\Virtual Machines\192.168.0.111_Ubuntu_64-bit\192.168.0.111_Ubuntu_64-bit.vmx") "188" = @("D:\Documents\Virtual Machines\Ollama_Ubuntu_64-bit\Ollama_Ubuntu_64-bit.vmx") "120" = @("D:\Documents\Virtual Machines\192.168.0.120_Ubuntu_64-bit\192.168.0.120_Ubuntu_64-bit.vmx") "121" = @("D:\Documents\Virtual Machines\192.168.0.121_Ubuntu_64-bit\192.168.0.121_Ubuntu_64-bit.vmx") "112" = @("D:\Downloads\kali-linux-2025.4-vmware-amd64\kali-linux-2025.4-vmware-amd64.vmwarevm\kali-linux-2025.4-vmware-amd64.vmx") } function Resolve-VmxPath { param([string]$HostAlias) $supplied = $SuppliedVmx[$HostAlias] if ($supplied -and (Test-Path -LiteralPath $supplied)) { return (Resolve-Path -LiteralPath $supplied).Path } foreach ($candidate in @($KnownVmxCandidates[$HostAlias])) { if ($candidate -and (Test-Path -LiteralPath $candidate)) { return (Resolve-Path -LiteralPath $candidate).Path } } if (-not $EnableRecursiveDiscovery) { return "" } foreach ($root in $DiscoveryRoot) { if (-not (Test-Path -LiteralPath $root)) { continue } $match = Get-ChildItem -LiteralPath $root -Recurse -Filter "*.vmx" -ErrorAction SilentlyContinue | Where-Object { $displayName = "" try { $displayName = (Select-String -LiteralPath $_.FullName -Pattern "^displayName\s*=\s*" -ErrorAction SilentlyContinue | Select-Object -First 1).Line } catch { $displayName = "" } $_.FullName -match "(^|[\\_\-\s])$HostAlias([\\_\-\s]|$)" -or $displayName -match "(^|[\\_\-\s])$HostAlias([\\_\-\s]|$)" } | Select-Object -First 1 if ($match) { return $match.FullName } } return "" } function Get-VmMap { $map = [ordered]@{} foreach ($alias in $VmOrder) { $map[$alias] = Resolve-VmxPath -HostAlias $alias } return $map } function Write-StartupScript { param([hashtable]$VmMap) New-Item -ItemType Directory -Path $ProgramDataDir -Force | Out-Null $vmRows = foreach ($alias in $VmOrder) { " @{ Alias = `"$alias`"; Path = `"$($VmMap[$alias])`" }" } $body = @" `$ErrorActionPreference = "Continue" `$VmrunPath = "$VmrunPath" `$VMs = @( $($vmRows -join ",`n") ) Start-Service -Name "VMAuthdService" -ErrorAction SilentlyContinue Start-Service -Name "VMnetDHCP" -ErrorAction SilentlyContinue Start-Sleep -Seconds 30 `$running = (& `$VmrunPath list 2>`$null) -join [Environment]::NewLine `$runningProcesses = @(Get-CimInstance -ClassName Win32_Process -Filter "Name='vmware-vmx.exe'" -ErrorAction SilentlyContinue | ForEach-Object { [string]`$_.CommandLine }) foreach (`$vm in `$VMs) { if (-not (Test-Path -LiteralPath `$vm.Path)) { Write-Output "VMX_MISSING alias=`$(`$vm.Alias) path=`$(`$vm.Path)" continue } `$runningByVmrun = `$running -like "*`$(`$vm.Path)*" `$runningByProcess = @(`$runningProcesses | Where-Object { `$_ -and `$_.IndexOf(`$vm.Path, [StringComparison]::OrdinalIgnoreCase) -ge 0 }).Count -gt 0 if (`$runningByVmrun -or `$runningByProcess) { Write-Output "VM_ALREADY_RUNNING alias=`$(`$vm.Alias) path=`$(`$vm.Path)" continue } & `$VmrunPath -T ws start `$vm.Path nogui Write-Output "VM_START_REQUESTED alias=`$(`$vm.Alias) path=`$(`$vm.Path) exit=`$LASTEXITCODE" Start-Sleep -Seconds 20 } "@ Set-Content -LiteralPath $StartScript -Value $body -Encoding UTF8 } function Apply-WindowsUpdatePolicy { $auPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" New-Item -Path $auPath -Force | Out-Null New-ItemProperty -Path $auPath -Name "NoAutoRebootWithLoggedOnUsers" -Value 1 -PropertyType DWord -Force | Out-Null New-ItemProperty -Path $auPath -Name "AlwaysAutoRebootAtScheduledTime" -Value 0 -PropertyType DWord -Force | Out-Null New-ItemProperty -Path $auPath -Name "AUOptions" -Value 3 -PropertyType DWord -Force | Out-Null New-ItemProperty -Path $auPath -Name "ScheduledInstallDay" -Value 0 -PropertyType DWord -Force | Out-Null New-ItemProperty -Path $auPath -Name "ScheduledInstallTime" -Value 3 -PropertyType DWord -Force | Out-Null New-ItemProperty -Path $auPath -Name "AUPowerManagement" -Value 0 -PropertyType DWord -Force | Out-Null } function Apply-ScheduledTask { $action = New-ScheduledTaskAction ` -Execute "powershell.exe" ` -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$StartScript`"" $trigger = New-ScheduledTaskTrigger -AtStartup $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest $settings = New-ScheduledTaskSettingsSet ` -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries ` -MultipleInstances IgnoreNew ` -RestartCount 3 ` -RestartInterval (New-TimeSpan -Minutes 2) ` -StartWhenAvailable Register-ScheduledTask ` -TaskName $TaskName ` -Action $action ` -Trigger $trigger ` -Principal $principal ` -Settings $settings ` -Force | Out-Null } function Get-VmrunList { if (-not (Test-Path -LiteralPath $VmrunPath)) { return @() } try { return @(& $VmrunPath list 2>$null | Select-Object -Skip 1) } catch { return @() } } function Get-VmwareVmxProcessCommandLines { try { return @(Get-CimInstance -ClassName Win32_Process -Filter "Name='vmware-vmx.exe'" -ErrorAction SilentlyContinue | ForEach-Object { [string]$_.CommandLine } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) } catch { return @() } } function Get-VmwareVmxProcessReadback { param([hashtable]$VmMap) $processes = @() try { $processes = @(Get-CimInstance -ClassName Win32_Process -Filter "Name='vmware-vmx.exe'" -ErrorAction SilentlyContinue) } catch { $processes = @() } foreach ($alias in $VmOrder) { $path = "$($VmMap[$alias])" $matches = @($processes | Where-Object { $commandLine = [string]$_.CommandLine $path -and $commandLine -and $commandLine.IndexOf($path, [StringComparison]::OrdinalIgnoreCase) -ge 0 }) if ($matches.Count -eq 0) { Write-Output "VM_PROCESS alias=$alias present=0 process_id=unknown session_id=unknown owner=unknown interactive_session=0 ui_truth=not_running" continue } foreach ($process in $matches) { $owner = "unknown" try { $ownerResult = Invoke-CimMethod -InputObject $process -MethodName GetOwner -ErrorAction SilentlyContinue if ($ownerResult -and $ownerResult.ReturnValue -eq 0 -and $ownerResult.User) { $owner = if ($ownerResult.Domain) { "$($ownerResult.Domain)\$($ownerResult.User)" } else { "$($ownerResult.User)" } } } catch { $owner = "unknown" } $sessionId = if ($null -ne $process.SessionId) { [int]$process.SessionId } else { -1 } $interactiveSession = [int]($sessionId -gt 0) $uiTruth = if ($interactiveSession -eq 1) { "runtime_running_interactive_session" } else { "runtime_running_gui_session_isolated" } Write-Output "VM_PROCESS alias=$alias present=1 process_id=$($process.ProcessId) session_id=$sessionId owner=$(Format-ReadbackValue $owner) interactive_session=$interactiveSession ui_truth=$uiTruth" } } } function Get-VmxRunningSource { param( [string]$VmxPath, [string[]]$VmrunList, [string[]]$ProcessCommandLines ) if (-not $VmxPath) { return "none" } $byVmrun = [bool]($VmrunList -contains $VmxPath) $byProcess = [bool](@($ProcessCommandLines | Where-Object { $_ -and $_.IndexOf($VmxPath, [StringComparison]::OrdinalIgnoreCase) -ge 0 }).Count -gt 0) if ($byVmrun -and $byProcess) { return "vmrun_and_process" } if ($byVmrun) { return "vmrun" } if ($byProcess) { return "vmware_vmx_process" } return "none" } function Get-VmwareServiceReadback { $serviceNames = @("VMAuthdService", "VMnetDHCP") foreach ($name in $serviceNames) { $service = Get-Service -Name $name -ErrorAction SilentlyContinue $startupType = "unknown" try { $cim = Get-CimInstance -ClassName Win32_Service -Filter "Name='$name'" -ErrorAction SilentlyContinue if ($cim) { $startupType = $cim.StartMode } } catch { $startupType = "unknown" } $present = [int][bool]$service $status = if ($service) { "$($service.Status)" } else { "missing" } $ok = [int]($service -and $service.Status -eq "Running" -and $startupType -in @("Auto", "Automatic")) Write-Output "VMWARE_SERVICE name=$name present=$present status=$status startup_type=$startupType ok=$ok" } } function Format-ReadbackValue { param([object]$Value) if ($null -eq $Value) { return "unknown" } $text = "$Value" if ([string]::IsNullOrWhiteSpace($text)) { return "unknown" } return ($text -replace "\s+", "_") } function Get-AutostartTaskReadback { $task = $null $taskInfo = $null try { $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue } catch { $task = $null } try { $taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName -ErrorAction SilentlyContinue } catch { $taskInfo = $null } $present = [int][bool]$task $enabled = 0 $state = "missing" $triggerCount = 0 $actionCount = 0 $principalUser = "missing" $runLevel = "missing" $logonType = "missing" $lastRunTime = "never" $lastTaskResult = "unknown" $nextRunTime = "unknown" $actionExecute = "missing" $actionArgument = "missing" if ($task) { $enabled = [int]($task.State -ne "Disabled") $state = "$($task.State)" $triggerCount = @($task.Triggers).Count $actionCount = @($task.Actions).Count if ($task.Principal) { $principalUser = "$($task.Principal.UserId)" $runLevel = "$($task.Principal.RunLevel)" $logonType = "$($task.Principal.LogonType)" } $actionExecute = (@($task.Actions) | ForEach-Object { "$($_.Execute)" }) -join "|" $actionArgument = (@($task.Actions) | ForEach-Object { "$($_.Arguments)" }) -join "|" } if ($taskInfo) { if ($taskInfo.LastRunTime) { $lastRunTime = $taskInfo.LastRunTime.ToString("s") } $lastTaskResult = "$($taskInfo.LastTaskResult)" if ($taskInfo.NextRunTime) { $nextRunTime = $taskInfo.NextRunTime.ToString("s") } } $startScriptPresent = [int](Test-Path -LiteralPath $StartScript) $ok = [int]($task -and $enabled -eq 1 -and $triggerCount -gt 0 -and $actionCount -gt 0) Write-Output "VMWARE_AUTOSTART_TASK name=$TaskName present=$present enabled=$enabled state=$state trigger_count=$triggerCount action_count=$actionCount ok=$ok" Write-Output "VMWARE_AUTOSTART_TASK_DETAIL name=$TaskName principal_user=$(Format-ReadbackValue $principalUser) run_level=$(Format-ReadbackValue $runLevel) logon_type=$(Format-ReadbackValue $logonType) start_script_present=$startScriptPresent last_run_time=$(Format-ReadbackValue $lastRunTime) last_task_result=$(Format-ReadbackValue $lastTaskResult) next_run_time=$(Format-ReadbackValue $nextRunTime)" Write-Output "VMWARE_AUTOSTART_TASK_ACTION name=$TaskName execute=$(Format-ReadbackValue $actionExecute) argument=$(Format-ReadbackValue $actionArgument)" } function Get-WindowsUpdatePolicyReadback { $auPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" $expected = [ordered]@{ "NoAutoRebootWithLoggedOnUsers" = 1 "AlwaysAutoRebootAtScheduledTime" = 0 "AUOptions" = 3 "ScheduledInstallDay" = 0 "ScheduledInstallTime" = 3 "AUPowerManagement" = 0 } $props = $null if (Test-Path -LiteralPath $auPath) { $props = Get-ItemProperty -LiteralPath $auPath -ErrorAction SilentlyContinue } foreach ($name in $expected.Keys) { $value = "missing" if ($props -and $null -ne $props.$name) { $value = "$($props.$name)" } $ok = [int]($value -eq "$($expected[$name])") Write-Output "WINDOWS_UPDATE_POLICY name=$name value=$value expected=$($expected[$name]) ok=$ok" } } function Get-VmPowerReadback { param([hashtable]$VmMap) $running = Get-VmrunList $processCommands = Get-VmwareVmxProcessCommandLines foreach ($alias in $VmOrder) { $path = "$($VmMap[$alias])" $present = [int][bool]$path $runningSource = Get-VmxRunningSource -VmxPath $path -VmrunList $running -ProcessCommandLines $processCommands $runningMatch = [int]($runningSource -ne "none") Write-Output "VM_POWER alias=$alias vmx_present=$present running=$runningMatch source=$runningSource" } } $vmMap = Get-VmMap $missing = @($VmOrder | Where-Object { -not $vmMap[$_] }) $vmrunPresent = Test-Path -LiteralPath $VmrunPath Write-Output "AWOOOI_WINDOWS99_VMWARE_AUTOSTART=1" Write-Output "MODE=$Mode" Write-Output "VMRUN_PRESENT=$([int]$vmrunPresent)" if ($SuppliedVmx["111"] -or $VmOrder -contains "111") { Write-Output "HOST111_VMWARE_TARGET=explicit_or_required" } else { Write-Output "HOST111_VMWARE_TARGET=not_required_by_default_missing_no_vmx_found_on_99" } foreach ($alias in $VmOrder) { Write-Output "VMX alias=$alias path=$($vmMap[$alias]) present=$([int][bool]$vmMap[$alias])" } Get-VmwareServiceReadback Get-AutostartTaskReadback Get-WindowsUpdatePolicyReadback Get-VmPowerReadback -VmMap $vmMap Get-VmwareVmxProcessReadback -VmMap $vmMap $serviceReady = @("VMAuthdService", "VMnetDHCP") | ForEach-Object { $svc = Get-Service -Name $_ -ErrorAction SilentlyContinue $svc -and $svc.Status -eq "Running" } $task = $null try { $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue } catch { $task = $null } $policyValues = @{} try { $policy = Get-ItemProperty -LiteralPath "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -ErrorAction SilentlyContinue if ($policy) { $policyValues["NoAutoRebootWithLoggedOnUsers"] = $policy.NoAutoRebootWithLoggedOnUsers $policyValues["AlwaysAutoRebootAtScheduledTime"] = $policy.AlwaysAutoRebootAtScheduledTime $policyValues["AUOptions"] = $policy.AUOptions $policyValues["ScheduledInstallDay"] = $policy.ScheduledInstallDay $policyValues["ScheduledInstallTime"] = $policy.ScheduledInstallTime $policyValues["AUPowerManagement"] = $policy.AUPowerManagement } } catch { $policyValues = @{} } $windowsUpdateReady = ( "$($policyValues["NoAutoRebootWithLoggedOnUsers"])" -eq "1" -and "$($policyValues["AlwaysAutoRebootAtScheduledTime"])" -eq "0" -and "$($policyValues["AUOptions"])" -eq "3" -and "$($policyValues["ScheduledInstallDay"])" -eq "0" -and "$($policyValues["ScheduledInstallTime"])" -eq "3" -and "$($policyValues["AUPowerManagement"])" -eq "0" ) $runningVmList = Get-VmrunList $runningVmxProcessCommandLines = Get-VmwareVmxProcessCommandLines $vmPowerReady = $true foreach ($alias in $VmOrder) { $path = "$($vmMap[$alias])" $runningSource = Get-VmxRunningSource -VmxPath $path -VmrunList $runningVmList -ProcessCommandLines $runningVmxProcessCommandLines if (-not $path -or $runningSource -eq "none") { $vmPowerReady = $false } } $configReady = ( $vmrunPresent -and $missing.Count -eq 0 -and ($serviceReady -notcontains $false) -and $task -and $task.State -ne "Disabled" -and @($task.Triggers).Count -gt 0 -and @($task.Actions).Count -gt 0 -and $windowsUpdateReady ) Write-Output "VMWARE_AUTOSTART_CONFIG_READY=$([int]$configReady)" Write-Output "VMWARE_AUTOSTART_POWER_READY=$([int]$vmPowerReady)" Write-Output "WINDOWS_UPDATE_NO_AUTO_REBOOT_READY=$([int]$windowsUpdateReady)" Write-Output "VMWARE_AUTOSTART_VERIFY_READY=$([int]($configReady -and $vmPowerReady))" if (-not $vmrunPresent) { Write-Error "vmrun.exe not found at $VmrunPath" } if ($missing.Count -gt 0) { Write-Output "MISSING_VMX_ALIASES=$($missing -join ',')" if ($Mode -eq "Apply") { throw "Apply requires explicit or discoverable VMX paths for every required VM." } } if ($Mode -eq "Verify") { exit 0 } if ($Mode -eq "DryRun") { Write-Output "DRY_RUN would_write_start_script=$StartScript" Write-Output "DRY_RUN would_register_task=$TaskName" Write-Output "DRY_RUN would_apply_windows_update_no_auto_reboot_policy=1" exit 0 } Write-StartupScript -VmMap $vmMap Set-Service -Name "VMAuthdService" -StartupType Automatic -ErrorAction SilentlyContinue Set-Service -Name "VMnetDHCP" -StartupType Automatic -ErrorAction SilentlyContinue Apply-ScheduledTask Apply-WindowsUpdatePolicy Write-Output "APPLIED scheduled_task=$TaskName start_script=$StartScript windows_update_no_auto_reboot_policy=1"