#Requires -Version 5.1 <# .SYNOPSIS Launches multiple instances of Google Chrome, each with a separate, temporary profile. Optionally downloads and installs a predefined bookmarks file into each new profile. .DESCRIPTION This script prompts the user for the number of Chrome instances, a target URL, the delay between launches, and whether to import bookmarks from a specified URL. It then creates unique profile directories, optionally downloads and copies a bookmarks file, and launches Chrome for each profile. Finally, it offers to clean up the created profiles. .PARAMETER DefaultUrl The default URL to open in each Chrome instance if the user doesn't provide one. .PARAMETER DefaultCount The default number of Chrome instances to launch if the user doesn't provide a number. .PARAMETER DefaultDelaySeconds The default delay in seconds between launching Chrome instances. .PARAMETER DefaultImportBookmarks Specifies whether to import bookmarks by default ($true) or not ($false). .PARAMETER BookmarksFileName The filename Chrome uses for its bookmarks file (typically 'Bookmarks'). .PARAMETER BookmarksFileUrl The URL from which to download the bookmarks file if import is enabled. .EXAMPLE .\Chrome.ps1 Runs the script with default settings, prompting the user for configuration. .NOTES Author: Your Name/Org Date: YYYY-MM-DD Requires Google Chrome to be installed in a standard location or be available in PATH. Uses $env:LocalAppData\Google\Chrome\ScriptProfiles for temporary profiles. Uses $env:TEMP for the downloaded bookmarks file. #> param ( [string]$DefaultUrl = 'https://glastonbury.seetickets.com', [int]$DefaultCount = 10, [double]$DefaultDelaySeconds = 10.0, [bool]$DefaultImportBookmarks = $true, [string]$BookmarksFileName = 'Bookmarks', [string]$BookmarksFileUrl = 'https://chrome.prentice.dev/Bookmarks' ) # --- Script Configuration --- $Script:Configuration = @{ Count = $DefaultCount TargetUrl = $DefaultUrl DelaySeconds = $DefaultDelaySeconds ImportBookmarks = $DefaultImportBookmarks SecondsToWaitCount = 10 SecondsToWaitUrl = 20 SecondsToWaitDelay = 15 SecondsToWaitBookmarks = 15 ProfilesBasePath = Join-Path $env:LocalAppData "Google\Chrome\ScriptProfiles" TempBookmarksPath = Join-Path $env:TEMP "downloaded_$BookmarksFileName" ChromeExePath = $null BookmarksDownloadedSuccessfully = $false } # --- Helper Functions --- function Get-RandomString { param ( [int]$Length = 8 ) $chars = "abcdefghijklmnopqrstuvwxyz0123456789" $result = -join ((1..$Length) | ForEach-Object { $chars[(Get-Random -Minimum 0 -Maximum $chars.Length)] }) return $result } function Find-ChromeExecutable { $potentialPaths = @( "$env:ProgramFiles\Google\Chrome\Application\chrome.exe", "$env:ProgramFiles (x86)\Google\Chrome\Application\chrome.exe", "$env:LocalAppData\Google\Chrome\Application\chrome.exe" ) foreach ($path in $potentialPaths) { if (Test-Path $path -PathType Leaf) { return $path } } $chromeInPath = Get-Command "chrome.exe" -ErrorAction SilentlyContinue if ($chromeInPath) { return $chromeInPath.Source } return $null } function Invoke-TimedInput { param ( [Parameter(Mandatory = $true)] [string]$PromptMessage, [Parameter(Mandatory = $true)] [string]$DefaultValue, [Parameter(Mandatory = $true)] [int]$TimeoutSeconds, [Parameter(Mandatory = $false)] [ValidateScript({ $_ -is [scriptblock] })] $ValidationScript = { $true }, # Default validation always passes [Parameter(Mandatory = $false)] [string]$AllowedCharsRegex = '.' # Default allows any character ) # Removed the check for cursor position at the bottom of the buffer $cursorTop = [System.Console]::CursorTop $cursorLeft = [System.Console]::CursorLeft $timer = [System.Diagnostics.Stopwatch]::StartNew() $inputText = "" $inputValid = $false $windowWidth = [System.Console]::WindowWidth try { [System.Console]::CursorVisible = $false while ($timer.Elapsed.TotalSeconds -lt $TimeoutSeconds) { $remainingTime = $TimeoutSeconds - [math]::Floor($timer.Elapsed.TotalSeconds) $fullPrompt = "$PromptMessage (default $DefaultValue, timeout: $($remainingTime)s) " $inputLine = "Your input: [$inputText]" # Position cursor and write prompt line, padding to clear previous content [System.Console]::SetCursorPosition($cursorLeft, $cursorTop) [System.Console]::Write($fullPrompt.PadRight($windowWidth - 1)) # Position cursor and write input line, padding to clear previous content [System.Console]::SetCursorPosition($cursorLeft, $cursorTop + 1) [System.Console]::Write($inputLine.PadRight($windowWidth - 1)) if ([System.Console]::KeyAvailable) { $key = [System.Console]::ReadKey($true) if ($key.Key -eq [System.ConsoleKey]::Enter) { break } elseif ($key.Key -eq [System.ConsoleKey]::Backspace -and $inputText.Length -gt 0) { $inputText = $inputText.Substring(0, $inputText.Length - 1) } elseif ($key.KeyChar -match $AllowedCharsRegex) { $inputText += $key.KeyChar } } Start-Sleep -Milliseconds 50 } } finally { # Clear the lines used for the prompt and input [System.Console]::SetCursorPosition($cursorLeft, $cursorTop) [System.Console]::Write("".PadRight($windowWidth)) [System.Console]::SetCursorPosition($cursorLeft, $cursorTop + 1) [System.Console]::Write("".PadRight($windowWidth)) # Move cursor back to the start of the prompt line for subsequent Write-Host [System.Console]::SetCursorPosition($cursorLeft, $cursorTop) # Visibility restored after all prompts are done in Get-UserConfiguration } if (-not [string]::IsNullOrWhiteSpace($inputText)) { # Use Invoke() method instead of call operator & if ($ValidationScript.Invoke($inputText)) { Write-Host "Using custom value: $inputText" return $inputText } else { Write-Host "Invalid input format. Using default value: $DefaultValue" return $DefaultValue } } else { Write-Host "Timeout or no input. Using default value: $DefaultValue" return $DefaultValue } } function Get-UserConfiguration { Write-Host "`nConfiguring launch settings..." $countInput = Invoke-TimedInput -PromptMessage "Enter number of profiles [1-99]" -DefaultValue $Script:Configuration.Count -TimeoutSeconds $Script:Configuration.SecondsToWaitCount -ValidationScript { param($input) return $input -match '^\d+$' -and [int]$input -gt 0 -and [int]$input -le 99 } -AllowedCharsRegex '\d' $Script:Configuration.Count = [int]$countInput Write-Host "" # Line break $urlInput = Invoke-TimedInput -PromptMessage "Enter URL" -DefaultValue $Script:Configuration.TargetUrl -TimeoutSeconds $Script:Configuration.SecondsToWaitUrl -ValidationScript { param($input) # Basic check for common URL patterns return $input -like '*://*' -or $input -like 'www.*' -or $input -like 'http://localhost*' -or $input -like 'https://localhost*' } $Script:Configuration.TargetUrl = $urlInput Write-Host "" # Line break $delayInput = Invoke-TimedInput -PromptMessage "Enter delay between launches in seconds (e.g., 0.5)" -DefaultValue $Script:Configuration.DelaySeconds -TimeoutSeconds $Script:Configuration.SecondsToWaitDelay -ValidationScript { param($input) try { $parsed = [double]::Parse($input, [System.Globalization.CultureInfo]::InvariantCulture) return $parsed -ge 0 } catch { return $false } } -AllowedCharsRegex '[\d\.]' $Script:Configuration.DelaySeconds = [double]::Parse($delayInput, [System.Globalization.CultureInfo]::InvariantCulture) Write-Host "" # Line break # Determine default bookmark value using if/else for PS 5.1 compatibility $defaultBookmarkValue = '' if ($Script:Configuration.ImportBookmarks) { $defaultBookmarkValue = 'Y' } else { $defaultBookmarkValue = 'N' } $bookmarkInput = Invoke-TimedInput -PromptMessage "Download and install bookmarks from '$BookmarksFileUrl'? [Y/N]" -DefaultValue $defaultBookmarkValue -TimeoutSeconds $Script:Configuration.SecondsToWaitBookmarks -ValidationScript { param($input) # Use case-insensitive match for Y/N return $input -imatch '^[YN]$' } -AllowedCharsRegex '[YNyn]' # Normalize input to uppercase before comparison $Script:Configuration.ImportBookmarks = ($bookmarkInput.ToUpperInvariant() -eq 'Y') if ($Script:Configuration.ImportBookmarks) { Write-Host "Will attempt to download and install bookmarks." } else { Write-Host "Skipping bookmark download and installation." } # Restore cursor visibility after all prompts [System.Console]::CursorVisible = $true } function Initialize-Environment { Write-Host "`nPerforming pre-launch checks..." $Script:Configuration.ChromeExePath = Find-ChromeExecutable if (-not $Script:Configuration.ChromeExePath) { Write-Error "Chrome is not installed or not found in common locations or PATH." return $false } Write-Host "Found Chrome executable: $($Script:Configuration.ChromeExePath)" if (-not (Test-Path $Script:Configuration.ProfilesBasePath)) { try { New-Item -ItemType Directory -Path $Script:Configuration.ProfilesBasePath -Force -ErrorAction Stop | Out-Null Write-Host "Created profile base directory: $($Script:Configuration.ProfilesBasePath)" } catch { Write-Error "Failed to create profile base directory at '$($Script:Configuration.ProfilesBasePath)'. Please check permissions." return $false } } else { Write-Host "Using existing profile base directory: $($Script:Configuration.ProfilesBasePath)" } return $true } function Download-BookmarksFile { if (-not $Script:Configuration.ImportBookmarks) { return # Skip if user opted out } Write-Host "`nAttempting to download bookmarks file from $BookmarksFileUrl..." try { if (-not (Test-Path $env:TEMP -PathType Container)) { New-Item -ItemType Directory -Path $env:TEMP -Force -ErrorAction Stop | Out-Null } Invoke-WebRequest -Uri $BookmarksFileUrl -OutFile $Script:Configuration.TempBookmarksPath -UseBasicParsing -ErrorAction Stop if (Test-Path $Script:Configuration.TempBookmarksPath -PathType Leaf) { Write-Host "Bookmarks file successfully downloaded to $($Script:Configuration.TempBookmarksPath)" $Script:Configuration.BookmarksDownloadedSuccessfully = $true } else { Write-Warning "Bookmark download reported success, but file not found at $($Script:Configuration.TempBookmarksPath)." $Script:Configuration.BookmarksDownloadedSuccessfully = $false } } catch { Write-Warning "Failed to download bookmarks file from '$BookmarksFileUrl'. Error: $($_.Exception.Message). Bookmarks will NOT be copied." $Script:Configuration.BookmarksDownloadedSuccessfully = $false } } function New-ChromeProfile { param ( [Parameter(Mandatory = $true)] [int]$ProfileIndex ) $profileName = Get-RandomString $profilePath = Join-Path $Script:Configuration.ProfilesBasePath $profileName $defaultProfileDir = Join-Path $profilePath "Default" $prefsFile = Join-Path $defaultProfileDir "Preferences" $bookmarksDestPath = Join-Path $defaultProfileDir $BookmarksFileName try { New-Item -ItemType Directory -Path $profilePath -Force -ErrorAction Stop | Out-Null New-Item -ItemType Directory -Path $defaultProfileDir -Force -ErrorAction Stop | Out-Null @{ profile = @{ name = $profileName }; bookmark_bar = @{ show_on_all_tabs = $true }; browser = @{ check_default_browser = $false } } | ConvertTo-Json -Depth 4 -Compress | Set-Content -Path $prefsFile -Encoding UTF8 -Force -ErrorAction Stop if ($Script:Configuration.BookmarksDownloadedSuccessfully) { Copy-Item -Path $Script:Configuration.TempBookmarksPath -Destination $bookmarksDestPath -Force -ErrorAction Stop Write-Host "Copied downloaded bookmarks to profile $ProfileIndex ($profileName)." return @{ Path = $profilePath; Name = $profileName; BookmarksCopied = $true } } else { return @{ Path = $profilePath; Name = $profileName; BookmarksCopied = $false } } } catch { Write-Warning "Failed to create profile $ProfileIndex ($profileName) or copy bookmarks. Error: $($_.Exception.Message)" # Attempt to clean up partially created directory if creation failed midway if (Test-Path $profilePath) { Remove-Item -Path $profilePath -Recurse -Force -ErrorAction SilentlyContinue } return $null } } function Start-ChromeInstance { param ( [Parameter(Mandatory = $true)] [hashtable]$ProfileInfo, [Parameter(Mandatory = $true)] [int]$ProfileIndex ) Write-Host "Launching profile $ProfileIndex ($($ProfileInfo.Name)) from $($ProfileInfo.Path)..." try { $arguments = @( "--user-data-dir=`"$($ProfileInfo.Path)`"", "--no-first-run", "--disable-backgrounding-occluded-windows", $Script:Configuration.TargetUrl ) $process = Start-Process $Script:Configuration.ChromeExePath -ArgumentList $arguments -PassThru -ErrorAction Stop return $process } catch { Write-Warning "Failed to launch Chrome for profile $ProfileIndex ($($ProfileInfo.Name)). Error: $($_.Exception.Message)" return $null } } function Remove-ScriptProfiles { param ( [Parameter(Mandatory = $true)] [array]$CreatedProfiles, # Array of profile hashtables [Parameter(Mandatory = $true)] [array]$LaunchedProcesses # Array of process objects ) Write-Host "`nDeleting profiles from $($Script:Configuration.ProfilesBasePath)..." foreach ($profile in $CreatedProfiles) { $profilePath = $profile.Path if (Test-Path $profilePath -PathType Container) { Write-Host "Removing $profilePath" try { $procToKill = $LaunchedProcesses | Where-Object { $_.StartInfo.Arguments -like "*--user-data-dir=`"$profilePath`"" } if ($procToKill) { Stop-Process -Id $procToKill.Id -Force -ErrorAction SilentlyContinue Start-Sleep -Milliseconds 500 # Give process time to exit } } catch { Write-Warning "Could not find or stop process for profile $profilePath. Manual closure might be needed if deletion fails." } Remove-Item $profilePath -Recurse -Force -ErrorAction SilentlyContinue } } if (Test-Path $Script:Configuration.ProfilesBasePath -PathType Container) { if ((Get-ChildItem -Path $Script:Configuration.ProfilesBasePath -ErrorAction SilentlyContinue).Count -eq 0) { Write-Host "Removing empty base directory: $($Script:Configuration.ProfilesBasePath)" Remove-Item $Script:Configuration.ProfilesBasePath -Force -ErrorAction SilentlyContinue } } Write-Host "Profile directories deleted." } function CleanUp-TemporaryFiles { if (Test-Path $Script:Configuration.TempBookmarksPath -PathType Leaf) { Write-Host "`nCleaning up temporary bookmarks file: $($Script:Configuration.TempBookmarksPath)" Remove-Item $Script:Configuration.TempBookmarksPath -Force -ErrorAction SilentlyContinue } } # --- Main Script Execution --- Clear-Host # Clear the console screen at the start Get-UserConfiguration if (-not (Initialize-Environment)) { Write-Warning "Initialization failed. Exiting." Start-Sleep -Seconds 5 exit 1 } Download-BookmarksFile $createdProfiles = @() $launchedProcesses = @() $bookmarksActuallyCopiedCount = 0 $delayMilliseconds = [int]($Script:Configuration.DelaySeconds * 1000) Write-Host "`nStarting $($Script:Configuration.Count) Chrome instances..." try { for ($i = 1; $i -le $Script:Configuration.Count; $i++) { $profileInfo = New-ChromeProfile -ProfileIndex $i if ($null -eq $profileInfo) { Write-Warning "Skipping launch for profile $i due to creation error." continue # Move to the next profile } $createdProfiles += $profileInfo if ($profileInfo.BookmarksCopied) { $bookmarksActuallyCopiedCount++ } $process = Start-ChromeInstance -ProfileInfo $profileInfo -ProfileIndex $i if ($null -ne $process) { $launchedProcesses += $process } if ($i -lt $Script:Configuration.Count) { Write-Host "Waiting $($Script:Configuration.DelaySeconds) seconds..." Start-Sleep -Milliseconds $delayMilliseconds Start-Sleep -Milliseconds (Get-Random -Minimum 200 -Maximum 1500) # Random jitter } } } finally { CleanUp-TemporaryFiles } # --- Final Summary and Cleanup Prompt --- $bookmarkStatusMessage = "" if ($Script:Configuration.ImportBookmarks) { if ($Script:Configuration.BookmarksDownloadedSuccessfully) { $bookmarkStatusMessage = "and attempted to copy downloaded bookmarks to $bookmarksActuallyCopiedCount profile(s)" } else { $bookmarkStatusMessage = "(bookmark download failed from '$BookmarksFileUrl', bookmarks not copied)" } } else { $bookmarkStatusMessage = "(bookmark download skipped by user)" } Write-Host "`nLaunched $($launchedProcesses.Count) of $($Script:Configuration.Count) Chrome windows visiting $($Script:Configuration.TargetUrl) $bookmarkStatusMessage." Write-Host "Profiles created in: $($Script:Configuration.ProfilesBasePath)" if ($createdProfiles.Count -gt 0) { Write-Host "Press Y to delete the created profiles from '$($Script:Configuration.ProfilesBasePath)', or any other key to exit..." $key = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") if ($key.Character -in @('y', 'Y')) { Remove-ScriptProfiles -CreatedProfiles $createdProfiles -LaunchedProcesses $launchedProcesses } else { Write-Host "`nExiting without deleting profiles in '$($Script:Configuration.ProfilesBasePath)'." } } else { Write-Host "`nNo profiles were successfully created to delete." } Write-Host "Script ended."