|
param( |
|
[Parameter(Mandatory = $true)] |
|
[ValidateSet('install', 'uninstall')] |
|
[string]$Mode, |
|
|
|
[string]$Distribution, |
|
[string]$User, |
|
[string]$PhysicalDrive, |
|
[int]$Partition = 1 |
|
) |
|
|
|
# ------------------------------- |
|
# Helpers |
|
# ------------------------------- |
|
|
|
function Assert-Admin { |
|
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() |
|
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser) |
|
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { |
|
Write-Error "This script must be run as Administrator." |
|
exit 1 |
|
} |
|
} |
|
|
|
function Select-Distribution { |
|
$distros = wsl -l -q 2>$null |
|
if (-not $distros) { |
|
Write-Error "No WSL distributions found." |
|
exit 1 |
|
} |
|
|
|
Write-Host "Available WSL distributions:" -ForegroundColor Cyan |
|
$i = 1 |
|
$map = @{} |
|
foreach ($d in $distros) { |
|
Write-Host " [$i] $d" |
|
$map[$i] = $d |
|
$i++ |
|
} |
|
|
|
do { |
|
$choice = Read-Host "Select distribution number" |
|
[int]$idx = 0 |
|
[int]::TryParse($choice, [ref]$idx) | Out-Null |
|
} while (-not $map.ContainsKey($idx)) |
|
|
|
return $map[$idx] |
|
} |
|
|
|
function Invoke-WslRoot { |
|
param( |
|
[string]$Distribution, |
|
[string]$Command |
|
) |
|
|
|
& wsl -d $Distribution -u root -- /bin/sh -lc $Command |
|
if ($LASTEXITCODE -ne 0) { |
|
Write-Warning "Command failed in WSL ($Distribution): $Command" |
|
} |
|
} |
|
|
|
function Invoke-WslUser { |
|
param( |
|
[string]$Distribution, |
|
[string]$User, |
|
[string]$Command |
|
) |
|
|
|
& wsl -d $Distribution -u $User -- /bin/sh -lc $Command |
|
if ($LASTEXITCODE -ne 0) { |
|
Write-Warning "Command failed in WSL ($Distribution, user=$User): $Command" |
|
} |
|
} |
|
|
|
function Test-SystemdActive { |
|
param( |
|
[string]$Distribution |
|
) |
|
|
|
# This will start the distro if not already running |
|
$checkCmd = @' |
|
if [ -d /run/systemd/system ] && [ "$(ps -p 1 -o comm=)" = "systemd" ]; then |
|
echo systemd |
|
else |
|
echo nosystemd |
|
fi |
|
'@ |
|
|
|
$out = & wsl -d $Distribution -- /bin/sh -lc $checkCmd 2>$null |
|
if ($LASTEXITCODE -ne 0) { |
|
return $false |
|
} |
|
|
|
return ($out.Trim() -eq 'systemd') |
|
} |
|
|
|
# ------------------------------- |
|
# Validate + fill parameters |
|
# ------------------------------- |
|
Assert-Admin |
|
|
|
if (-not $Distribution) { |
|
$Distribution = Select-Distribution |
|
} |
|
|
|
if ($Mode -eq 'install' -and -not $PhysicalDrive) { |
|
$PhysicalDrive = Read-Host "Enter physical drive path (e.g. \\.\PHYSICALDRIVE1)" |
|
} |
|
|
|
# Normalize PhysicalDrive for Linux mount path |
|
$driveId = $null |
|
if ($PhysicalDrive) { |
|
# Expect form \\.\PHYSICALDRIVE1 |
|
if ($PhysicalDrive -match 'PHYSICALDRIVE(\d+)') { |
|
$driveId = "PHYSICALDRIVE$($Matches[1])" |
|
} else { |
|
Write-Error "PhysicalDrive path '$PhysicalDrive' is not in expected form (\\.\PHYSICALDRIVE<N>)." |
|
exit 1 |
|
} |
|
} |
|
|
|
if (-not $User -and $Mode -eq 'install') { |
|
# Try to autodetect default user |
|
$defaultUser = (& wsl -d $Distribution -- /bin/sh -lc 'id -un' 2>$null) |
|
if ($LASTEXITCODE -eq 0 -and $defaultUser) { |
|
Write-Host "Detected default user in distro '$Distribution': $defaultUser" -ForegroundColor Cyan |
|
$useDetected = Read-Host "Use this user? (Y/n)" |
|
if ($useDetected -match '^(n|no)$') { |
|
$User = Read-Host "Enter Linux username to configure" |
|
} else { |
|
$User = $defaultUser.Trim() |
|
} |
|
} else { |
|
$User = Read-Host "Enter Linux username to configure" |
|
} |
|
} |
|
|
|
if ($Mode -eq 'install') { |
|
if (-not $User) { |
|
Write-Error "Linux user name is required for install." |
|
exit 1 |
|
} |
|
} |
|
|
|
# Derived names/paths |
|
$TaskName = "WSL-HomeMount-$Distribution" |
|
$BaseDir = Join-Path $env:ProgramData "WslHomeMount\$Distribution" |
|
$MountScriptPath = Join-Path $BaseDir "mount-ext4.ps1" |
|
$LinuxRealHomeRoot = "/mnt/wsl/${driveId}p$Partition" |
|
$LinuxMountScript = "/usr/local/sbin/mount-home-ssd.sh" |
|
$HomeServiceName = "home-$User-ssd.service" |
|
$HomeServicePath = "/etc/systemd/system/$HomeServiceName" |
|
$WslMountServicePath = "/etc/systemd/system/wsl-mount-ext4.service" |
|
|
|
Write-Host "Mode : $Mode" |
|
Write-Host "Distribution : $Distribution" |
|
if ($Mode -eq 'install') { |
|
Write-Host "Linux user : $User" |
|
Write-Host "Physical drive : $PhysicalDrive" |
|
Write-Host "Partition : $Partition" |
|
Write-Host "Linux SSD root : $LinuxRealHomeRoot" |
|
} |
|
Write-Host "" |
|
|
|
# For install: require systemd to be active |
|
if ($Mode -eq 'install') { |
|
Write-Host "Checking if systemd is active in distro '$Distribution'..." -ForegroundColor Cyan |
|
$systemdOk = Test-SystemdActive -Distribution $Distribution |
|
if (-not $systemdOk) { |
|
Write-Error @" |
|
Systemd does NOT appear to be active in distro '$Distribution'. |
|
|
|
Ensure ALL of the following before running this installer: |
|
1) /etc/wsl.conf inside the distro contains: |
|
[boot] |
|
systemd=true |
|
|
|
2) You run: wsl --shutdown |
|
|
|
3) Start the distro again, and verify inside: |
|
ps -p 1 -o comm= |
|
prints: systemd |
|
|
|
Once systemd is active, re-run this script. |
|
"@ |
|
exit 1 |
|
} else { |
|
Write-Host "Systemd is active (PID 1 is systemd). Continuing..." -ForegroundColor Green |
|
} |
|
} |
|
|
|
# ------------------------------- |
|
# INSTALL LOGIC |
|
# ------------------------------- |
|
if ($Mode -eq 'install') { |
|
# 1. Ensure base directory |
|
if (-not (Test-Path $BaseDir)) { |
|
New-Item -ItemType Directory -Path $BaseDir | Out-Null |
|
} |
|
|
|
# 2. Create mount-ext4.ps1 for the scheduled task |
|
$mountScriptContent = @" |
|
param( |
|
[string]\$Drive = "$PhysicalDrive", |
|
[int]\$Partition = $Partition |
|
) |
|
|
|
\$EventLogName = "Application" |
|
\$EventSourceName = "WSL-Disk-Tasks-$Distribution" |
|
|
|
function Ensure-EventSource { |
|
try { |
|
if (-not [System.Diagnostics.EventLog]::SourceExists(\$EventSourceName)) { |
|
New-EventLog -LogName \$EventLogName -Source \$EventSourceName |
|
} |
|
} |
|
catch { |
|
Write-Host "Warning: Could not create/find event source '\$EventSourceName': \$($_.Exception.Message)" -ForegroundColor DarkYellow |
|
} |
|
} |
|
|
|
function Write-Log { |
|
param( |
|
[string]\$Message, |
|
[System.Diagnostics.EventLogEntryType]\$EntryType = [System.Diagnostics.EventLogEntryType]::Information, |
|
[int]\$EventId = 2000 |
|
) |
|
|
|
Write-Host \$Message |
|
|
|
try { |
|
if ([System.Diagnostics.EventLog]::SourceExists(\$EventSourceName)) { |
|
Write-EventLog -LogName \$EventLogName -Source \$EventSourceName -EntryType \$EntryType -EventId \$EventId -Message \$Message |
|
} |
|
} |
|
catch { |
|
Write-Host "Warning: Failed to write to Event Log: \$($_.Exception.Message)" -ForegroundColor DarkYellow |
|
} |
|
} |
|
|
|
Ensure-EventSource |
|
|
|
Write-Log -Message "[WSL] mount-ext4.ps1 started (drive=\$Drive, partition=\$Partition, distro=$Distribution)." -EventId 2000 |
|
Write-Log -Message "[WSL] Running: wsl --mount \$Drive --partition \$Partition --type ext4 --options rw" -EventId 2001 |
|
|
|
wsl --mount \$Drive --partition \$Partition --type ext4 --options "rw" |
|
\$code = \$LASTEXITCODE |
|
|
|
if (\$code -ne 0) { |
|
Write-Log -Message "[WSL] ERROR: wsl --mount failed with exit code \$code." -EntryType Error -EventId 9006 |
|
exit \$code |
|
} |
|
|
|
try { |
|
\$verifyOutput = wsl -e sh -c "mount | grep '$driveId' || df -h | grep '$driveId'" 2>\$null |
|
if (\$LASTEXITCODE -eq 0 -and \$verifyOutput) { |
|
Write-Log -Message "[WSL] Verification OK: disk appears mounted:`n\$verifyOutput" -EventId 2008 |
|
} |
|
else { |
|
Write-Log -Message "[WSL] WARNING: Disk mounted but could not verify via 'mount/df'." -EntryType Warning -EventId 9007 |
|
} |
|
} |
|
catch { |
|
Write-Log -Message "[WSL] WARNING: Exception during mount verification: \$($_.Exception.Message)" -EntryType Warning -EventId 9008 |
|
} |
|
|
|
Write-Log -Message "[WSL] mount-ext4.ps1 finished." -EventId 2010 |
|
"@ |
|
|
|
Set-Content -Path $MountScriptPath -Value $mountScriptContent -Encoding UTF8 |
|
|
|
Write-Host "Created mount-ext4 script at: $MountScriptPath" |
|
|
|
# 3. Register scheduled task (manual trigger only; systemd will call it) |
|
Write-Host "Creating scheduled task '$TaskName'..." -ForegroundColor Cyan |
|
|
|
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$MountScriptPath`"" |
|
$task = New-ScheduledTask -Action $action |
|
Register-ScheduledTask -TaskName $TaskName -InputObject $task -Force | Out-Null |
|
|
|
Write-Host "Scheduled task '$TaskName' created." |
|
|
|
# 4. Create mount-home-ssd.sh inside distro |
|
Write-Host "Creating $LinuxMountScript inside distro..." -ForegroundColor Cyan |
|
|
|
$linuxScriptEscaped = @" |
|
cat > $LinuxMountScript << 'EOF' |
|
#!/bin/bash |
|
set -e |
|
|
|
USER_NAME="$User" |
|
HOME_DIR="/home/\$USER_NAME" |
|
|
|
REAL_HOME_ROOT="$LinuxRealHomeRoot" |
|
REAL_HOME="\$REAL_HOME_ROOT" |
|
|
|
# Runs fsck fix once per boot |
|
MAX_WAIT_LOOPS=40 # 40 * 0.5s = 20s |
|
SLEEP_MS=500 # Sleep duration in milliseconds |
|
STATE_FILE="/run/wsl_fsck_done" |
|
LOG_FILE="/var/log/wsl_fsck.log" |
|
|
|
# Derived total wait time in seconds (integer math: 40 * 500 / 1000 = 20) |
|
TOTAL_WAIT_SECONDS=\$((MAX_WAIT_LOOPS * SLEEP_MS / 1000)) |
|
|
|
if [[ -f "\$STATE_FILE" ]]; then |
|
echo "wsl-fsck: already performed this boot; skipping." |
|
else |
|
# Make sure log dir exists |
|
echo "wsl-fsck: ensure log dir exists" |
|
mkdir -p "\$(dirname "\$LOG_FILE")" |
|
|
|
# Wait up to TOTAL_WAIT_SECONDS for the SSD mount to appear |
|
for ((i=1; i<=MAX_WAIT_LOOPS; i++)); do |
|
echo "wsl-fsck: attempting mount point in \$REAL_HOME_ROOT (iteration \$i/\$MAX_WAIT_LOOPS)" |
|
if mountpoint -q "\$REAL_HOME_ROOT"; then |
|
# 1. Identify the physical device (e.g., /dev/sdd1) |
|
DEVICE_PATH=\$(findmnt -n -o SOURCE -- "\$REAL_HOME_ROOT" 2>/dev/null || true) |
|
if [[ -z "\$DEVICE_PATH" ]]; then |
|
echo "wsl-fsck: could not determine device for \$REAL_HOME_ROOT" >&2 |
|
break |
|
fi |
|
|
|
echo "wsl-fsck: found device \$DEVICE_PATH at \$REAL_HOME_ROOT. Preparing to fsck..." |
|
|
|
# 2. Aggressively unmount the device, but with a retry limit |
|
UNMOUNT_RETRIES=10 |
|
while grep -qsE "^\$DEVICE_PATH " /proc/mounts || \ |
|
grep -qsE "^[^ ]+ \$REAL_HOME_ROOT " /proc/mounts; do |
|
echo "wsl-fsck: unmounting \$DEVICE_PATH..." |
|
umount "\$DEVICE_PATH" 2>/dev/null || umount -l "\$DEVICE_PATH" 2>/dev/null || true |
|
sleep 0.5 |
|
((UNMOUNT_RETRIES--)) |
|
if (( UNMOUNT_RETRIES <= 0 )); then |
|
echo "wsl-fsck: failed to fully unmount \$DEVICE_PATH after multiple attempts" >&2 |
|
break |
|
fi |
|
done |
|
|
|
# 3. Check filesystem type: only fsck ext4 (or unknown) |
|
FSTYPE=\$(blkid -o value -s TYPE "\$DEVICE_PATH" 2>/dev/null || echo "") |
|
if [[ -n "\$FSTYPE" && "\$FSTYPE" != "ext4" ]]; then |
|
echo "wsl-fsck: \$DEVICE_PATH is type '\$FSTYPE', skipping fsck." >&2 |
|
else |
|
echo "--- fsck run at \$(date) ---" >> "\$LOG_FILE" |
|
echo "wsl-fsck: running filesystem check on \$DEVICE_PATH..." | tee -a "\$LOG_FILE" |
|
if ! fsck -y "\$DEVICE_PATH" >> "\$LOG_FILE" 2>&1; then |
|
# fsck non-zero doesn’t always mean failure, but we log it anyway |
|
echo "wsl-fsck: fsck reported non-zero status for \$DEVICE_PATH, see \$LOG_FILE" >&2 |
|
fi |
|
fi |
|
|
|
# 4. Remount the drive to the original location |
|
echo "wsl-fsck: remounting \$DEVICE_PATH to \$REAL_HOME_ROOT..." |
|
if mount "\$DEVICE_PATH" "\$REAL_HOME_ROOT"; then |
|
echo "wsl-fsck: remount successful." |
|
else |
|
echo "wsl-fsck: remount FAILED for \$DEVICE_PATH -> \$REAL_HOME_ROOT" >&2 |
|
fi |
|
|
|
# Mark that we ran once this boot |
|
touch "\$STATE_FILE" |
|
echo "wsl-fsck: filesystem repair sequence complete — exiting loop." |
|
break # <<< important: don’t re-run on the remounted drive |
|
else |
|
echo "wsl-fsck: no mountpoint available yet" |
|
fi |
|
|
|
echo "wsl-fsck: waiting ..." |
|
# Convert ms -> fractional seconds for sleep (500 -> 0.50) |
|
sleep "0.\$((SLEEP_MS/10))" |
|
done |
|
|
|
# If we never saw the mount, you might want a log line: |
|
if ! mountpoint -q "\$REAL_HOME_ROOT"; then |
|
echo "wsl-fsck: \$REAL_HOME_ROOT never appeared within timeout (\${TOTAL_WAIT_SECONDS}s)." >&2 |
|
fi |
|
fi |
|
|
|
# If still not mounted after waiting, fallback to VHD home |
|
if ! mountpoint -q "\$REAL_HOME_ROOT"; then |
|
exit 0 |
|
fi |
|
|
|
# Ensure fallback home exists |
|
if [ ! -d "\$HOME_DIR" ]; then |
|
mkdir -p "\$HOME_DIR" |
|
chown "\$USER_NAME:\$USER_NAME" "\$HOME_DIR" |
|
fi |
|
|
|
# Skip if already mounted |
|
if mountpoint -q "\$HOME_DIR"; then |
|
exit 0 |
|
fi |
|
|
|
# Ensure real home exists |
|
if [ ! -d "\$REAL_HOME" ]; then |
|
mkdir -p "\$REAL_HOME" |
|
chown "\$USER_NAME:\$USER_NAME" "\$REAL_HOME" |
|
fi |
|
|
|
# Perform bind mount |
|
mount --bind "\$REAL_HOME" "\$HOME_DIR" |
|
EOF |
|
|
|
chmod +x $LinuxMountScript |
|
"@ |
|
|
|
Invoke-WslRoot -Distribution $Distribution -Command $linuxScriptEscaped |
|
|
|
# 5. Ensure fallback home + minimal .zshrc |
|
Write-Host "Ensuring fallback /home/$User and minimal .zshrc..." -ForegroundColor Cyan |
|
|
|
$fallbackHomeCmd = @" |
|
if [ ! -d /home/$User ]; then |
|
mkdir -p /home/$User |
|
chown $User:$User /home/$User |
|
fi |
|
|
|
# Sentinel README |
|
if [ ! -f /home/$User/.FALLBACK_HOME_README ]; then |
|
cat << 'TXT' > /home/$User/.FALLBACK_HOME_README |
|
This is the fallback home directory on the WSL virtual disk. |
|
If your SSD is not mounted, this directory is used temporarily. |
|
TXT |
|
chown $User:$User /home/$User/.FALLBACK_HOME_README |
|
fi |
|
"@ |
|
|
|
Invoke-WslRoot -Distribution $Distribution -Command $fallbackHomeCmd |
|
|
|
# Setup minimal .zshrc only if missing |
|
$zshrcCmd = @" |
|
if [ -d /home/$User ]; then |
|
if [ ! -f /home/$User/.zshrc ]; then |
|
cat << 'EOF' > /home/$User/.zshrc |
|
# Minimal zshrc created by WSL SSD home installer |
|
# You can safely customize this file. |
|
EOF |
|
chown $User:$User /home/$User/.zshrc |
|
fi |
|
fi |
|
"@ |
|
Invoke-WslRoot -Distribution $Distribution -Command $zshrcCmd |
|
|
|
# 6. systemd service: wsl-mount-ext4.service |
|
Write-Host "Creating systemd units in distro..." -ForegroundColor Cyan |
|
|
|
$wslMountService = @" |
|
cat > $WslMountServicePath << 'EOF' |
|
[Unit] |
|
Description=Trigger Windows task to mount PHYSICALDRIVE into WSL |
|
|
|
[Service] |
|
Type=oneshot |
|
ExecStart=/mnt/c/Windows/System32/schtasks.exe /run /TN "$TaskName" |
|
RemainAfterExit=yes |
|
|
|
[Install] |
|
WantedBy=multi-user.target |
|
EOF |
|
"@ |
|
|
|
Invoke-WslRoot -Distribution $Distribution -Command $wslMountService |
|
|
|
# 7. systemd service: home-<user>-ssd.service |
|
$homeService = @" |
|
cat > $HomeServicePath << 'EOF' |
|
[Unit] |
|
Description=Bind-mount SSD-backed home for user $User |
|
After=wsl-mount-ext4.service |
|
Requires=wsl-mount-ext4.service |
|
|
|
[Service] |
|
Type=oneshot |
|
ExecStart=$LinuxMountScript |
|
RemainAfterExit=yes |
|
|
|
[Install] |
|
WantedBy=multi-user.target |
|
EOF |
|
"@ |
|
|
|
Invoke-WslRoot -Distribution $Distribution -Command $homeService |
|
|
|
# 8. Enable the services |
|
$enableServicesCmd = @" |
|
systemctl daemon-reload |
|
systemctl enable wsl-mount-ext4.service |
|
systemctl enable $HomeServiceName |
|
"@ |
|
|
|
Invoke-WslRoot -Distribution $Distribution -Command $enableServicesCmd |
|
|
|
Write-Host "" |
|
Write-Host "INSTALL COMPLETE." -ForegroundColor Green |
|
Write-Host "Next steps:" -ForegroundColor Yellow |
|
Write-Host " 1) Run: wsl --shutdown" |
|
Write-Host " 2) Start your distro '$Distribution' again." |
|
Write-Host "WSL boot will:" |
|
Write-Host " - trigger the scheduled task '$TaskName' to run mount-ext4.ps1" |
|
Write-Host " - mount $PhysicalDrive partition $Partition into WSL" |
|
Write-Host " - systemd will bind-mount $LinuxRealHomeRoot onto /home/$User" |
|
} |
|
|
|
# ------------------------------- |
|
# UNINSTALL LOGIC |
|
# ------------------------------- |
|
if ($Mode -eq 'uninstall') { |
|
Write-Host "Uninstalling WSL SSD home integration for distro '$Distribution'..." -ForegroundColor Cyan |
|
|
|
# 1. Remove systemd units + helper script |
|
$removeLinuxCmd = @" |
|
if [ -f $HomeServicePath ]; then |
|
systemctl disable $(basename $HomeServicePath) 2>/dev/null || true |
|
rm -f $HomeServicePath |
|
fi |
|
|
|
if [ -f $WslMountServicePath ]; then |
|
systemctl disable $(basename $WslMountServicePath) 2>/dev/null || true |
|
rm -f $WslMountServicePath |
|
fi |
|
|
|
if [ -f $LinuxMountScript ]; then |
|
rm -f $LinuxMountScript |
|
fi |
|
|
|
systemctl daemon-reload |
|
"@ |
|
|
|
Invoke-WslRoot -Distribution $Distribution -Command $removeLinuxCmd |
|
|
|
# 2. Remove scheduled task |
|
if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) { |
|
Write-Host "Removing scheduled task '$TaskName'..." -ForegroundColor Cyan |
|
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false |
|
} |
|
|
|
# 3. Remove script directory |
|
if (Test-Path $BaseDir) { |
|
Write-Host "Removing base directory: $BaseDir" -ForegroundColor Cyan |
|
Remove-Item -Path $BaseDir -Recurse -Force |
|
} |
|
|
|
Write-Host "UNINSTALL COMPLETE." -ForegroundColor Green |
|
Write-Host "You may want to run: wsl --shutdown before restarting the distro." |
|
} |