197 lines
8.6 KiB
PowerShell
197 lines
8.6 KiB
PowerShell
param(
|
|
[string]$Distro,
|
|
[string]$LinuxProjectRoot,
|
|
[switch]$FromDesktop,
|
|
[switch]$InstallOnly,
|
|
[switch]$Elevated
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
$script:PlanetTargetFingerprintLength = 12
|
|
|
|
function Invoke-WslText {
|
|
param([string[]]$Arguments)
|
|
$output = & wsl.exe @Arguments
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "WSL command failed (exit $LASTEXITCODE). Check the distribution and project directory."
|
|
}
|
|
return ($output -join "`n").Trim()
|
|
}
|
|
|
|
function Get-PlanetTarget {
|
|
param([string]$WindowsRoot)
|
|
if ($WindowsRoot -match '^\\\\(?:wsl\$|wsl\.localhost)\\([^\\]+)(\\.*)?$') {
|
|
$root = if ($Matches[2]) { $Matches[2].Replace('\', '/') } else { '/' }
|
|
return @{ Distro = $Matches[1]; LinuxProjectRoot = $root.TrimEnd('/') }
|
|
}
|
|
if ($WindowsRoot -notmatch '^[a-zA-Z]:\\') {
|
|
throw 'Run planet.cmd from a WSL project folder or a local Windows drive.'
|
|
}
|
|
$distribution = Invoke-WslText -Arguments @('--exec', 'printenv', 'WSL_DISTRO_NAME')
|
|
$root = Invoke-WslText -Arguments @('-d', $distribution, '--exec', 'wslpath', '-a', '-u', $WindowsRoot)
|
|
return @{ Distro = $distribution; LinuxProjectRoot = $root.TrimEnd('/') }
|
|
}
|
|
|
|
function Get-PlanetWindowsRoot {
|
|
param([hashtable]$Target)
|
|
if ([string]::IsNullOrWhiteSpace($Target.Distro) -or
|
|
$Target.Distro -match '[\\/\r\n]' -or
|
|
$Target.LinuxProjectRoot -notmatch '^/' -or
|
|
$Target.LinuxProjectRoot -match '[\r\n]') {
|
|
throw 'Invalid WSL distribution or absolute project path.'
|
|
}
|
|
return '\\wsl$\' + $Target.Distro + $Target.LinuxProjectRoot.Replace('/', '\')
|
|
}
|
|
|
|
function Assert-PlanetTarget {
|
|
param([hashtable]$Target)
|
|
$windowsRoot = Get-PlanetWindowsRoot $Target
|
|
foreach ($relative in @('planet.sh', 'scripts\windows\planet-launcher.ps1', 'scripts\windows\restart.zsh')) {
|
|
if (-not (Test-Path -LiteralPath (Join-Path $windowsRoot $relative) -PathType Leaf)) {
|
|
throw "P_WINDOWS_LAUNCHER_TARGET_MISSING: $windowsRoot\$relative. Run planet.cmd from the current project folder to regenerate the desktop launcher."
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-PlanetBootstrap {
|
|
param([hashtable]$Target, [switch]$RunElevated)
|
|
# Only base64 crosses CMD/PowerShell command-line parsing; paths are data.
|
|
$json = $Target | ConvertTo-Json -Compress
|
|
$payload = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($json))
|
|
$elevatedFlag = if ($RunElevated) { '$true' } else { '$false' }
|
|
return @"
|
|
`$ErrorActionPreference = 'Stop'
|
|
`$ProgressPreference = 'SilentlyContinue'
|
|
try {
|
|
`$target = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('$payload')) | ConvertFrom-Json
|
|
`$root = '\\wsl$\' + `$target.Distro + `$target.LinuxProjectRoot.Replace('/', '\')
|
|
`$helper = Join-Path `$root 'scripts\windows\planet-launcher.ps1'
|
|
if (-not (Test-Path -LiteralPath `$helper -PathType Leaf)) {
|
|
throw 'P_WINDOWS_LAUNCHER_TARGET_MISSING: Project moved or unavailable. Run planet.cmd from the current project folder again.'
|
|
}
|
|
& `$helper -Distro `$target.Distro -LinuxProjectRoot `$target.LinuxProjectRoot -FromDesktop -Elevated:$elevatedFlag -InstallOnly:(`$env:PLANET_LAUNCHER_INSTALL_ONLY -eq '1')
|
|
exit `$LASTEXITCODE
|
|
} catch {
|
|
Write-Host `$_.Exception.Message -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
"@
|
|
}
|
|
|
|
function ConvertTo-PlanetEncodedCommand {
|
|
param([string]$Command)
|
|
return [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Command))
|
|
}
|
|
|
|
function Install-PlanetDesktopLauncher {
|
|
param([hashtable]$Target, [string]$Desktop = [Environment]::GetFolderPath('DesktopDirectory'))
|
|
if (-not $Desktop -or -not (Test-Path -LiteralPath $Desktop -PathType Container)) {
|
|
throw 'Windows Desktop folder is unavailable.'
|
|
}
|
|
$identity = $Target.Distro + "`n" + $Target.LinuxProjectRoot
|
|
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$identityBytes = [Text.Encoding]::UTF8.GetBytes($identity)
|
|
$fingerprint = [BitConverter]::ToString($sha256.ComputeHash($identityBytes)).Replace('-', '')
|
|
$hash = $fingerprint.Substring(0, $script:PlanetTargetFingerprintLength)
|
|
} finally {
|
|
$sha256.Dispose()
|
|
}
|
|
$marker = "rem Planet managed launcher v1 target=$hash"
|
|
$destination = Join-Path $Desktop 'Planet.cmd'
|
|
if ((Test-Path -LiteralPath $destination) -and
|
|
-not ([IO.File]::ReadAllText($destination).Contains($marker))) {
|
|
$destination = Join-Path $Desktop "Planet-$hash.cmd"
|
|
}
|
|
if ((Test-Path -LiteralPath $destination) -and
|
|
-not ([IO.File]::ReadAllText($destination).Contains($marker))) {
|
|
throw "Refusing to overwrite an unrelated file: $destination"
|
|
}
|
|
$encoded = ConvertTo-PlanetEncodedCommand (Get-PlanetBootstrap $Target)
|
|
$lines = @(
|
|
'@echo off', 'setlocal EnableExtensions DisableDelayedExpansion', $marker,
|
|
'set "PLANET_LAUNCHER_INSTALL_ONLY="',
|
|
'if /i "%~1"=="--install-only" set "PLANET_LAUNCHER_INSTALL_ONLY=1"',
|
|
'set "PLANET_POWERSHELL=powershell.exe"',
|
|
'where.exe pwsh.exe >nul 2>&1',
|
|
'if not errorlevel 1 set "PLANET_POWERSHELL=pwsh.exe"',
|
|
"%PLANET_POWERSHELL% -NoLogo -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded",
|
|
'set "planet_launcher_exit=%errorlevel%"',
|
|
'if not "%planet_launcher_exit%"=="0" pause',
|
|
'exit /b %planet_launcher_exit%'
|
|
)
|
|
[IO.File]::WriteAllText($destination, ($lines -join "`r`n") + "`r`n", [Text.Encoding]::ASCII)
|
|
return $destination
|
|
}
|
|
|
|
function Test-PlanetAdministrator {
|
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
|
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
}
|
|
|
|
function Get-PlanetTerminalLaunch {
|
|
param([string]$EncodedCommand)
|
|
$shell = Get-Command pwsh.exe -CommandType Application -ErrorAction SilentlyContinue |
|
|
Select-Object -First 1
|
|
if (-not $shell) {
|
|
$shell = Get-Command powershell.exe -CommandType Application -ErrorAction Stop |
|
|
Select-Object -First 1
|
|
}
|
|
$shellArguments = "-NoLogo -NoProfile -NoExit -ExecutionPolicy Bypass -EncodedCommand $EncodedCommand"
|
|
$launch = @{ FilePath = $shell.Source; ArgumentList = $shellArguments; WindowStyle = 'Normal' }
|
|
$terminal = Get-Command wt.exe -CommandType Application -ErrorAction SilentlyContinue |
|
|
Select-Object -First 1
|
|
if ($terminal) {
|
|
$launch.FilePath = $terminal.Source
|
|
# Start-Process joins arguments: quote the executable path explicitly.
|
|
$launch.ArgumentList = '-w new new-tab --title Planet "' + $shell.Source + '" ' + $shellArguments
|
|
}
|
|
return $launch
|
|
}
|
|
|
|
function Start-PlanetRestart {
|
|
param([hashtable]$Target, [switch]$AlreadyElevated)
|
|
$script:PlanetLauncherExitCode = 0
|
|
$isAdministrator = Test-PlanetAdministrator
|
|
if (-not $AlreadyElevated) {
|
|
$encoded = ConvertTo-PlanetEncodedCommand (Get-PlanetBootstrap $Target -RunElevated)
|
|
$launch = Get-PlanetTerminalLaunch $encoded
|
|
if (-not $isAdministrator) { $launch.Verb = 'RunAs' }
|
|
# An interactive terminal is intentional: startup logs and the WSL shell stay visible.
|
|
Start-Process @launch | Out-Null
|
|
return
|
|
}
|
|
if (-not $isAdministrator) { throw 'Administrator elevation was not granted.' }
|
|
Write-Host "Restarting Planet in $($Target.Distro):$($Target.LinuxProjectRoot)"
|
|
& wsl.exe -d $Target.Distro --cd $Target.LinuxProjectRoot --exec zsh -l -i ($Target.LinuxProjectRoot + '/scripts/windows/restart.zsh')
|
|
$script:PlanetLauncherExitCode = $LASTEXITCODE
|
|
}
|
|
|
|
if ($MyInvocation.InvocationName -ne '.') {
|
|
try {
|
|
[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
|
|
$OutputEncoding = [Console]::OutputEncoding
|
|
if ($FromDesktop) {
|
|
$target = @{ Distro = $Distro; LinuxProjectRoot = $LinuxProjectRoot }
|
|
} else {
|
|
$root = (Get-Item -LiteralPath (Join-Path $PSScriptRoot '..\..')).FullName
|
|
$target = Get-PlanetTarget $root
|
|
}
|
|
Assert-PlanetTarget $target
|
|
if (-not $FromDesktop) {
|
|
$desktopLauncher = Install-PlanetDesktopLauncher $target
|
|
Write-Host "Desktop launcher ready: $desktopLauncher"
|
|
}
|
|
if ($InstallOnly) { exit 0 }
|
|
# Do not capture the WSL output: the interactive terminal must stream live.
|
|
Start-PlanetRestart $target -AlreadyElevated:$Elevated
|
|
exit $script:PlanetLauncherExitCode
|
|
} catch {
|
|
Write-Host $_.Exception.Message -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
}
|