Script for installing WSL on Windows 10 AME
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

545 lines
19 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. # https://stackoverflow.com/a/34559554/
  2. function New-TemporaryDirectory {
  3. $parent = [System.IO.Path]::GetTempPath()
  4. $name = [System.IO.Path]::GetRandomFileName()
  5. New-Item -ItemType Directory -Path (Join-Path $parent $name)
  6. }
  7. Workflow Install-WSL {
  8. [CmdletBinding(DefaultParameterSetName='Installation')]
  9. param(
  10. [Parameter(Mandatory=$True,ParameterSetName='Installation',Position=0)]
  11. [ValidateSet(
  12. 'wslubuntu2004',
  13. 'wslubuntu2004arm',
  14. 'wsl-ubuntu-1804',
  15. 'wsl-ubuntu-1804-arm',
  16. 'wsl-ubuntu-1604',
  17. 'wsl-debian-gnulinux',
  18. 'wsl-kali-linux-new',
  19. 'wsl-opensuse-42',
  20. 'wsl-sles-12'
  21. )]
  22. [string]$LinuxDistribution,
  23. [Parameter(Mandatory=$False,ParameterSetName='Installation')]
  24. [switch]$FeatureInstalled,
  25. [Parameter(Mandatory=$False,ParameterSetName='Installation')]
  26. [switch]$OmitWindowsTerminal,
  27. [Parameter(Mandatory=$True,ParameterSetName='Cancelation')]
  28. [switch]$Cancel,
  29. [Parameter(Mandatory=$False,ParameterSetName='WindowsTerminal')]
  30. [switch]$InstallWindowsTerminal
  31. )
  32. # The task scheduler is unreliable in AME
  33. $ShortcutPath = Join-Path $env:AppData 'Microsoft\Windows\Start Menu\Programs\Startup\Install WSL.lnk'
  34. if ($Cancel) {
  35. $Removed = Remove-Item -LiteralPath $ShortcutPath -ErrorAction SilentlyContinue
  36. $Removed = Get-Job -Command 'Install-WSL' | Where-Object {$_.State -eq 'Suspended'} | Remove-Job -Force
  37. Write-Information 'All pending WSL installations have been canceled.'
  38. return 'done'
  39. } elseif ($InstallWindowsTerminal) {
  40. InlineScript {
  41. $ExecutionPolicy = Get-ExecutionPolicy -Scope Process
  42. Set-ExecutionPolicy RemoteSigned -Scope Process
  43. Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
  44. Set-ExecutionPolicy $ExecutionPolicy -Scope Process
  45. scoop bucket add extras
  46. scoop install windows-terminal
  47. }
  48. return 'done'
  49. }
  50. # establish directory for WSL installations
  51. $AppDataFolder = Join-Path $env:LocalAppData 'WSL'
  52. $DistrosFolder = New-Item -ItemType Directory -Force -Path $AppDataFolder
  53. $DistroFolder = Join-Path $DistrosFolder $LinuxDistribution
  54. if (Test-Path -Path $DistroFolder -PathType Container) {
  55. return Write-Error 'Cannot install a distro twice! This will waste your internet data. Uninstall the existing version first.' -Category ResourceExists
  56. }
  57. Write-Information 'Creating startup item'
  58. InlineScript {
  59. $shell = New-Object -ComObject ('WScript.Shell')
  60. $shortcut = $shell.CreateShortcut($Using:ShortcutPath)
  61. $shortcut.TargetPath = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
  62. $shortcut.Arguments = "-WindowStyle Normal -NoLogo -NoProfile -Command `"& { Write-Output \`"Resuming installation...\`"; Get-Job -Command `'Install-WSL`' | Resume-Job | Receive-Job -Wait -InformationAction Continue; pause; exit }`""
  63. $shortcut.Save()
  64. }
  65. Write-Information ''
  66. Write-Information 'There will be a "Windows PowerShell" shortcut in your startup items until this'
  67. Write-Information 'script is complete. Please do not be alarmed, it will remove itself once the'
  68. Write-Information 'installation is complete.'
  69. Write-Information ''
  70. Write-Information 'Ensuring required features are enabled...'
  71. # using a named pipe to communicate between elevated process and not elevated one
  72. if ($FeatureInstalled) {
  73. $RestartNeeded = $False
  74. } else {
  75. try {
  76. # For various reasons this needs to be duplicated twice.
  77. # I hate it as much as you, but for some reason I can't put it in a function
  78. # It just refuses to work when I try to call it in the loop below
  79. $RestartNeeded = InlineScript {
  80. $PipeName = -join (((48..57)+(65..90)+(97..122)) * 80 |Get-Random -Count 12 |%{[char]$_})
  81. $Enabled = Start-Process powershell -ArgumentList "`
  82. `$Enabled = Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart -WarningAction SilentlyContinue`
  83. `$RestartNeeded = `$Enabled.RestartNeeded`
  84. `
  85. `$pipe = New-Object System.IO.Pipes.NamedPipeServerStream `'$PipeName`',`'Out`'`
  86. `$pipe.WaitForConnection()`
  87. `$sw = New-Object System.IO.StreamWriter `$pipe`
  88. `$sw.AutoFlush = `$True`
  89. `$sw.WriteLine([string]`$RestartNeeded)`
  90. `$sw.Dispose()`
  91. `$pipe.Dispose()`
  92. " -Verb RunAs -WindowStyle Hidden -ErrorAction Stop
  93. $pipe = New-Object System.IO.Pipes.NamedPipeClientStream '.',$Using:PipeName,'In'
  94. $pipe.Connect()
  95. $sr = New-Object System.IO.StreamReader $pipe
  96. $data = $sr.ReadLine()
  97. $sr.Dispose()
  98. $pipe.Dispose()
  99. $data -eq [string]$True
  100. } -ErrorAction Stop
  101. } catch {
  102. return Write-Error 'Please accept the UAC prompt so that the WSL feature can be installed, or specify the -FeatureInstalled flag to skip'
  103. }
  104. }
  105. if ($RestartNeeded) {
  106. # TODO detect if we're already waiting for a reboot specifically
  107. # Maybe this can be done by checking for the scheduled task instead?
  108. # This feels messy which is why it's disabled, and it would also detect
  109. # the currently running task
  110. # Future Logan from the future!: I think the shortcut is more easily
  111. # detected, but there are reasons you might want to run this more than
  112. # once in a row. For example if you are installing multiple distros
  113. # Should work okay...
  114. Write-Information 'Restart your computer in 30 seconds or it will explode'
  115. 'restart-needed'
  116. Suspend-Workflow
  117. # Wait for a logon where the feature is installed. This will be after at
  118. # least 1 reboot, but for various reasons (grumble grumble...) it might
  119. # be later. Every Suspend-Workflow is virtually guaranteed to be resumed
  120. # by a logon, or a manual resume (which is harmless in this case).
  121. $waiting = $True
  122. while ($waiting) {
  123. if ($FeatureInstalled) {
  124. $RestartNeeded = $False
  125. } else {
  126. try {
  127. $RestartNeeded = InlineScript {
  128. $PipeName = -join (((48..57)+(65..90)+(97..122)) * 80 |Get-Random -Count 12 |%{[char]$_})
  129. $Enabled = Start-Process powershell -ArgumentList "`
  130. `$Enabled = Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart -WarningAction SilentlyContinue`
  131. `$RestartNeeded = `$Enabled.RestartNeeded`
  132. `
  133. `$pipe = New-Object System.IO.Pipes.NamedPipeServerStream `'$PipeName`',`'Out`'`
  134. `$pipe.WaitForConnection()`
  135. `$sw = New-Object System.IO.StreamWriter `$pipe`
  136. `$sw.AutoFlush = `$True`
  137. `$sw.WriteLine([string]`$RestartNeeded)`
  138. `$sw.Dispose()`
  139. `$pipe.Dispose()`
  140. " -Verb RunAs -WindowStyle Hidden -ErrorAction Stop
  141. $pipe = New-Object System.IO.Pipes.NamedPipeClientStream '.',$Using:PipeName,'In'
  142. $pipe.Connect()
  143. $sr = New-Object System.IO.StreamReader $pipe
  144. $data = $sr.ReadLine()
  145. $sr.Dispose()
  146. $pipe.Dispose()
  147. $data -eq [string]$True
  148. } -ErrorAction Stop
  149. } catch {
  150. # I decided that this is not always true and it would be
  151. # rude to assume that. So I give the user a choice and allow
  152. # them to continue without UAC
  153. ## The user accepted the UAC prompt the first time, so they
  154. ## can do it again. They cannot specify the -FeatureInstalled
  155. ## flag at this point, unfortunately.
  156. #Write-Output 'Please accept the UAC prompt to continue installation.'
  157. # Try to get input from the user as a fallback
  158. $response = InlineScript {
  159. [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
  160. [System.Windows.Forms.Messagebox]::Show("Admin access is required to check the status of the WSL feature. If you can no longer grant admin access via UAC:`n`nIs the WSL feature installed and enabled?", 'WSL Installer', [System.Windows.Forms.MessageBoxButtons]::YesNo)
  161. }
  162. $RestartNeeded = $response -eq 7 # 7 is DialogResult.No
  163. }
  164. }
  165. if ($RestartNeeded) {
  166. Write-Information 'Looks like the WSL component is still not installed.'
  167. 'still-waiting'
  168. Suspend-Workflow
  169. } else {
  170. $waiting = $False
  171. }
  172. }
  173. }
  174. Write-Information "`n`n`n`n`n`n`n"
  175. Write-Information 'It will take a few minutes to download the distribution. Most WSL distros are'
  176. Write-Information 'at or around 200 MB in size. Depending on your internet connection, you could be'
  177. Write-Information 'staring at this screen for 10 minutes. Sit back and relax, grab a cup of tea...'
  178. Write-Information ''
  179. $retrying = $True
  180. while ($retrying) {
  181. $tempFile = InlineScript { New-TemporaryFile }
  182. Remove-Item -LiteralPath $tempFile
  183. $tempFile = $tempFile.FullName -replace '$','.zip'
  184. try {
  185. Write-Information "Attempting to download distribution to $tempFile..."
  186. $data = InlineScript {
  187. $PipeName = -join (((48..57)+(65..90)+(97..122)) * 80 |Get-Random -Count 12 |%{[char]$_})
  188. Start-Process powershell -ArgumentList "`
  189. Try {`
  190. Invoke-WebRequest -Uri `"https://aka.ms/$Using:LinuxDistribution`" -OutFile `"$Using:tempFile`" -ErrorAction Stop -UseBasicParsing`
  191. `$Result = 'Success'`
  192. } Catch {`
  193. `$Result = `"Failed to download file: `$(`$PSItem.Message)`"`
  194. }`
  195. `
  196. `$pipe = New-Object System.IO.Pipes.NamedPipeServerStream `'$PipeName`',`'Out`'`
  197. `$pipe.WaitForConnection()`
  198. `$sw = New-Object System.IO.StreamWriter `$pipe`
  199. `$sw.AutoFlush = `$True`
  200. `$sw.WriteLine([string]`$Result)`
  201. `$sw.Dispose()`
  202. `$pipe.Dispose()`
  203. " -WindowStyle Hidden -ErrorAction Stop
  204. $pipe = New-Object System.IO.Pipes.NamedPipeClientStream '.',$PipeName,'In'
  205. $pipe.Connect()
  206. $sr = New-Object System.IO.StreamReader $pipe
  207. $data = $sr.ReadLine()
  208. $sr.Dispose()
  209. $pipe.Dispose()
  210. $data
  211. } -ErrorAction Stop
  212. if ($data -ne 'Success') {
  213. Write-Error $data -ErrorAction Stop
  214. }
  215. $retrying = $False
  216. Write-Information 'Done!'
  217. } catch {
  218. Remove-Item -LiteralPath $tempFile -ErrorAction SilentlyContinue
  219. # PSItem is contextual and can't be read from the InlineScript
  220. $theError = $PSItem
  221. Write-Information "Error: $theError"
  222. $response = InlineScript {
  223. [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
  224. [System.Windows.Forms.Messagebox]::Show("The WSL package '$Using:LinuxDistribution' could not be downloaded from Microsoft's servers.`n`nError: $Using:theError`n`nYou may abort the install, and restart it at any time using the wizard. Clicking Ignore will cause a retry the next time you log in.", 'Could not download WSL package', [System.Windows.Forms.MessageBoxButtons]::AbortRetryIgnore)
  225. }
  226. if ($response -eq 3) { # Abort
  227. Write-Information 'Aborting'
  228. $retrying = $False
  229. Write-Information 'Removing startup item...'
  230. Remove-Item -LiteralPath $ShortcutPath -ErrorAction SilentlyContinue
  231. return 'aborted'
  232. } elseif ($response -eq 5) { # Ignore
  233. Write-Information 'Ignoring'
  234. 'still-waiting'
  235. Suspend-Workflow # Wait for next logon
  236. }
  237. Write-Information 'Retrying'
  238. # If retry just loop again /shrug
  239. }
  240. }
  241. Write-Information 'Removing startup item...'
  242. Remove-Item -LiteralPath $ShortcutPath -ErrorAction SilentlyContinue
  243. $tempDir = New-TemporaryDirectory
  244. Expand-Archive -LiteralPath $tempFile -DestinationPath $tempDir -ErrorAction Stop
  245. Remove-Item -LiteralPath $tempFile -ErrorAction SilentlyContinue
  246. Write-Information 'Distribution bundle extracted'
  247. $theDir = $tempDir
  248. $Executable = Get-ChildItem $tempDir | Where-Object {$_.Name -match '.exe$'} | Select-Object -First 1
  249. if ($Executable -eq $null) {
  250. $Package = Get-ChildItem $tempDir | Where-Object {$_.Name -match '_x64.appx$'} | Select-Object -First 1
  251. if ($Package -eq $null) {
  252. return Write-Error 'Could not find the package containing the installer :(' -Category NotImplemented
  253. }
  254. $Package = Rename-Item -LiteralPath ($Package.FullName) -NewName ($Package.Name -replace '.appx$','.zip') -PassThru
  255. Write-Information "Distribution package: $($Package.Name)"
  256. $InnerPackageTemp = New-TemporaryDirectory
  257. Expand-Archive -LiteralPath $Package -DestinationPath $InnerPackageTemp
  258. Remove-Item -LiteralPath $tempDir -Recurse
  259. $Executable = Get-ChildItem $InnerPackageTemp | Where-Object {$_.Name -match '.exe$'} | Select-Object -First 1
  260. $theDir = $InnerPackageTemp
  261. if ($Executable -eq $null) {
  262. return Write-Error 'Could not find an executable inside the x64 package :(' -Category NotImplemented
  263. }
  264. } else {
  265. Write-Information 'Root package contains the installer'
  266. }
  267. # this is going to have to stick around forever if the wsl install is going to stay intact
  268. $theDir = Move-Item -LiteralPath $theDir -Destination $DistroFolder -PassThru
  269. $Executable = Get-ChildItem $theDir | Where-Object {$_.Name -match '.exe$'} | Select-Object -First 1
  270. Write-Information "Executing installer: $($Executable.Name)"
  271. InlineScript { wsl --set-default-version 1 }
  272. Start-Process -FilePath ($Executable.FullName) -Wait
  273. if (!$OmitWindowsTerminal) {
  274. Write-Information 'Installing Windows Terminal...'
  275. InlineScript {
  276. $ExecutionPolicy = Get-ExecutionPolicy -Scope Process
  277. Set-ExecutionPolicy RemoteSigned -Scope Process
  278. Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
  279. Set-ExecutionPolicy $ExecutionPolicy -Scope Process
  280. scoop bucket add extras
  281. scoop install windows-terminal
  282. }
  283. }
  284. Write-Information 'Everything should be in order now. Enjoy!'
  285. # We done
  286. return 'done'
  287. }
  288. function Install-WSLInteractive {
  289. $Distros = @(
  290. [PSCustomObject]@{Slug = 'wslubuntu2004'; Name = 'Ubuntu 20.04'; Arch = 'x64'}
  291. [PSCustomObject]@{Slug = 'wsl-ubuntu-1804'; Name = 'Ubuntu 18.04'; Arch = 'x64'}
  292. [PSCustomObject]@{Slug = 'wsl-ubuntu-1604'; Name = 'Ubuntu 16.04'; Arch = 'x64'}
  293. [PSCustomObject]@{Slug = 'wsl-debian-gnulinux'; Name = 'Debian Stable'; Arch = 'x64'}
  294. [PSCustomObject]@{Slug = 'wsl-kali-linux-new'; Name = 'Kali Linux'; Arch = 'x64'}
  295. [PSCustomObject]@{Slug = 'wsl-opensuse-42'; Name = 'OpenSUSE 4.2'; Arch = 'x64'}
  296. [PSCustomObject]@{Slug = 'wsl-sles-12'; Name = 'SLES 12'; Arch = 'x64'}
  297. )
  298. $Menu = 'main'
  299. if ([Security.Principal.WindowsIdentity]::GetCurrent().Groups -contains 'S-1-5-32-544') {
  300. $Menu = 'admin'
  301. }
  302. while ($Menu -ne 'exit') {
  303. Clear-Host
  304. # 80 chars: ' '
  305. Write-Host ' :: WSL INSTALL SCRIPT FOR WINDOWS 10 AME'
  306. Write-Host ''
  307. Write-Host ' This script will help you install Windows Subsystem for Linux on your'
  308. Write-Host ' ameliorated installation of Windows 10'
  309. Write-Host ''
  310. Write-Host ' :: NOTE: Tested on Windows 10 1909, and Windows 10 AME 20H2'
  311. switch ($menu) {
  312. 'main' {
  313. Write-Host ''
  314. Write-Host ' :: Please enter a number from 1-3 to select an option from the list below'
  315. Write-Host ''
  316. Write-Host ' 1) Install a new WSL distro'
  317. Write-Host ' 2) Cancel a pending WSL installation'
  318. Write-Host ' 3) Exit'
  319. Write-Host ''
  320. Write-Host ' >> ' -NoNewLine
  321. $Input = $Host.UI.ReadLine()
  322. switch ($Input) {
  323. '1' {
  324. $Menu = 'select-distro'
  325. }
  326. '2' {
  327. $Menu = 'cancel'
  328. }
  329. '3' {
  330. $Menu = 'exit'
  331. }
  332. default {
  333. Write-Host ''
  334. Write-Host ' !! Invalid option selected' -ForegroundColor red
  335. Write-Host ''
  336. Write-Host ' Press enter to continue...' -NoNewLine
  337. $Host.UI.ReadLine()
  338. }
  339. }
  340. }
  341. 'select-distro' {
  342. Write-Host ''
  343. Write-Host ' :: Please enter a number from the list to select a distro to install'
  344. Write-Host ''
  345. $Max = 1
  346. $Distros | ForEach-Object {
  347. Add-Member -InputObject $_ -NotePropertyName Option -NotePropertyValue ([string]$Max) -Force
  348. Write-Host " $Max) $($_.Name)"
  349. $Max += 1
  350. }
  351. Write-Host " $Max) Return to main menu"
  352. Write-Host ''
  353. Write-Host ' >> ' -NoNewLine
  354. $Input = $Host.UI.ReadLine()
  355. if ($Input -eq ([string]$Max)) {
  356. $Menu = 'main'
  357. } else {
  358. $Distro = $Distros | Where-Object -Property Option -eq -Value $Input
  359. if ($Distro -eq $null) {
  360. Write-Host ''
  361. Write-Host ' !! Invalid option selected' -ForegroundColor Red
  362. Write-Host ''
  363. Write-Host ' Press enter to continue...' -NoNewLine
  364. $Host.UI.ReadLine()
  365. } else {
  366. $Menu = 'install-distro-confirm'
  367. }
  368. }
  369. }
  370. 'install-distro-confirm' {
  371. Write-Host ''
  372. Write-Host " :: WARNING: Are you sure you want to install $($Distro.Name)? (yes/no) " -NoNewLine
  373. $Input = $Host.UI.ReadLine()
  374. switch ($Input) {
  375. 'yes' {
  376. $Menu = 'install-distro'
  377. }
  378. 'no' {
  379. $Menu = 'select-distro'
  380. }
  381. default {
  382. Write-Host ''
  383. Write-Host ' !! Invalid input' -ForegroundColor Red
  384. Write-Host ''
  385. Write-Host ' Press enter to continue...' -NoNewLine
  386. $Host.UI.ReadLine()
  387. $Menu = 'select-distro'
  388. }
  389. }
  390. }
  391. 'install-distro' {
  392. Write-Host ''
  393. Write-Host "Installing $($Distro.Name)..."
  394. try {
  395. $Menu = ('result-' + (Install-WSL -LinuxDistribution ($Distro.Slug) -InformationAction Continue -ErrorAction Stop | Select-Object -First 1 -Wait))
  396. } catch {
  397. Write-Host ''
  398. Write-Host ' !! An error occurred during the installation' -ForegroundColor Red
  399. Write-Host " !! The error is: $PSItem" -ForegroundColor Red
  400. Write-Host ''
  401. Write-Host ' Your chosen distro could not be installed.'
  402. Write-Host ''
  403. Write-Host ' Press enter to continue...' -NoNewLine
  404. $Host.UI.ReadLine()
  405. $Menu = 'select-distro'
  406. }
  407. }
  408. 'cancel' {
  409. Write-Host ''
  410. Write-Host ' :: WARNING: Are you sure you want to cancel all pending installs? (yes/no) ' -NoNewLine
  411. $Input = $Host.UI.ReadLine()
  412. switch ($Input) {
  413. 'yes' {
  414. Write-Host ''
  415. Install-WSL -Cancel
  416. }
  417. 'no' {
  418. Write-Host ''
  419. Write-Host ' Returning to main menu.'
  420. }
  421. default {
  422. Write-Host ''
  423. Write-Host ' !! Invalid input' -ForegroundColor Red
  424. }
  425. }
  426. Write-Host ''
  427. Write-Host ' Press enter to continue...' -NoNewLine
  428. $Host.UI.ReadLine()
  429. $Menu = 'main'
  430. }
  431. 'admin' {
  432. Write-Host ''
  433. Write-Host ' !! This script should NOT be run as Administrator' -ForegroundColor Red
  434. Write-Host ' !! Please close this window and run the script normally' -ForegroundColor Red
  435. Write-Host ''
  436. Write-Host ' Press enter to continue...' -NoNewLine
  437. $Host.UI.ReadLine()
  438. $Menu = 'exit'
  439. }
  440. 'result-restart-needed' {
  441. Clear-Host
  442. Write-Host ' !! WSL installation will resume once you restart Windows'
  443. Write-Host ''
  444. Write-Host ' Please ensure you stay connected to the Internet.'
  445. Write-Host ''
  446. Write-Host ' Press enter to continue...' -NoNewLine
  447. $Host.UI.ReadLine()
  448. $Menu = 'exit'
  449. }
  450. 'result-done' {
  451. Clear-Host
  452. Write-Host ' :: Installation done!'
  453. Write-Host ''
  454. Write-Host ' The WSL feature was already installed and enabled on your system, so we were'
  455. Write-Host ' able to install your distro right away.'
  456. Write-Host ''
  457. Write-Host ' Enjoy!'
  458. Write-Host ''
  459. Write-Host ' Press enter to continue...' -NoNewLine
  460. $Host.UI.ReadLine()
  461. $Menu = 'exit'
  462. }
  463. default {
  464. Write-Host ''
  465. Write-Host " !! Invalid menu encountered ($Menu). Exiting" -ForegroundColor Red
  466. Write-Host ' !! THIS IS A BUG, PLEASE REPORT IT TO THE AME DEVS' -ForegroundColor Red
  467. $Menu = 'exit'
  468. }
  469. }
  470. }
  471. }