Repository navigation
Expand file tree
/
Copy pathInstall-WslSwitchTask.ps1
More file actions
334 lines (282 loc) · 12.4 KB
/
Copy pathInstall-WslSwitchTask.ps1
File metadata and controls
334 lines (282 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
<#
.SYNOPSIS
Installs or removes the scheduled tasks that run Update-WslSwitch.ps1, so
WSL2 bridged networking follows whichever network is connected.
.DESCRIPTION
Install-WslSwitchTask.ps1 copies Update-WslSwitch.ps1 into -InstallDir and
registers two tasks in the \WSL\ folder of Task Scheduler for the current
user. Running it again replaces both tasks and the installed script, so
use it to update the script or change settings.
Run it from an elevated PowerShell opened under your own account, from
the folder that holds Update-WslSwitch.ps1. If you elevate as a separate
admin account, the tasks, .wslconfig path and install folder all belong
to that account instead, and the script warns you.
Tasks created:
\WSL\Select Bridge Switch
Runs Update-WslSwitch.ps1 with highest privileges, because
Get-VMSwitch needs admin rights. Runs only while you are logged on,
so it runs in your session against your WSL VM. Triggers:
- at logon, after 45 seconds;
- 15 seconds after a network connect or disconnect event
(Microsoft-Windows-NetworkProfile/Operational 10000 and 10001);
- 15 seconds after a Wi-Fi connect or disconnect event
(Microsoft-Windows-WLAN-AutoConfig/Operational 8001 and 8003);
- every -PollMinutes, as a backup check. Pulling the cable or
unplugging a USB dock does not always log a network event,
because the vEthernet adapter can stay up.
A run that is triggered while another is in progress is skipped.
Runs on battery. Stopped after 10 minutes.
\WSL\Start Docker Desktop
Starts Docker Desktop without elevation. It has no triggers; the
main task starts it so that Docker Desktop never runs with admin
rights.
By default the main task starts powershell.exe through
"conhost.exe --headless", which stops a console or Windows Terminal
window flashing up on every run. Use -NoHeadlessConhost on Windows
builds where that does not work.
The install also checks that each switch in -SwitchPriority exists and is
External, and that .wslconfig exists, and warns if not. These checks do
not stop the install.
.PARAMETER SwitchPriority
Names of the Hyper-V external switches to choose from, highest priority
first. Passed to Update-WslSwitch.ps1 -SwitchPriority. A single comma-,
semicolon- or space-separated string is also accepted.
Default: ethbr0, wlanbr0
.PARAMETER InstallDir
Folder that Update-WslSwitch.ps1 is copied into and run from. Created if
missing. The copied script is unblocked (Unblock-File). This is also
where Update-WslSwitch.ps1 writes its log by default.
Default: $env:LOCALAPPDATA\WslSwitchSelector
.PARAMETER PollMinutes
Minutes between backup checks by the main task. Each check that finds
nothing to change is quick and makes no changes. Range 1 to 1440.
Default: 5
.PARAMETER DockerDesktopExe
Full path of Docker Desktop.exe. Used as the action of the
"Start Docker Desktop" task, and passed to Update-WslSwitch.ps1 as a
fallback.
Default: $env:ProgramFiles\Docker\Docker\Docker Desktop.exe
.PARAMETER ExtraArguments
Extra arguments added to the Update-WslSwitch.ps1 command line in the
main task, as one string. Only switch parameters and simple values work
here, because the script is started with powershell.exe -File.
Example: '-IgnoreGateway -NoToast -CooldownSeconds 300'
.PARAMETER NoHeadlessConhost
Start the main task with "powershell.exe -WindowStyle Hidden" instead of
"conhost.exe --headless powershell.exe". A console window may flash up
briefly on each run. Use this on Windows builds that do not support
conhost --headless.
.PARAMETER RunNow
Start the main task straight after installing it.
.PARAMETER Uninstall
Remove both tasks instead of installing them. The installed script, the
log and the cooldown state file in -InstallDir are left in place, and
the .wslconfig file is not changed.
.INPUTS
None. You cannot pipe objects to Install-WslSwitchTask.ps1.
.OUTPUTS
None. Writes progress messages to the host.
.EXAMPLE
PS> .\Install-WslSwitchTask.ps1 -RunNow
Installs with the defaults (ethbr0, then wlanbr0, checking every 5
minutes) and runs the main task straight away.
.EXAMPLE
PS> .\Install-WslSwitchTask.ps1 -SwitchPriority ethbr0,wlanbr0 -PollMinutes 2
Installs with a 2-minute backup check, so cable pulls are picked up
sooner.
.EXAMPLE
PS> .\Install-WslSwitchTask.ps1 -ExtraArguments '-NoToast -CooldownSeconds 300'
Installs with notifications turned off and at most one change every
5 minutes.
.EXAMPLE
PS> .\Install-WslSwitchTask.ps1 -NoHeadlessConhost
Installs using a hidden powershell.exe window instead of
conhost --headless.
.EXAMPLE
PS> .\Install-WslSwitchTask.ps1 -Uninstall
Removes both scheduled tasks.
.EXAMPLE
PS> Get-ScheduledTask -TaskPath '\WSL\' | Get-ScheduledTaskInfo
Shows when each task last ran and its last result (0 means success).
.NOTES
Requirements:
- Windows PowerShell 5.1 or later, run as administrator.
- Hyper-V PowerShell module (for the switch checks).
- Update-WslSwitch.ps1 in the same folder as this script.
To change the settings, run the installer again with new parameters; it
replaces the existing tasks. To stop the automation without removing it,
either disable "\WSL\Select Bridge Switch" in Task Scheduler or set
networkingMode to something other than bridged in .wslconfig.
The events and triggers can be checked in Task Scheduler under
Task Scheduler Library > WSL.
.LINK
Update-WslSwitch.ps1
.LINK
https://learn.microsoft.com/windows/wsl/wsl-config
.LINK
https://learn.microsoft.com/windows/win32/taskschd/task-scheduler-schema
#>
#Requires -Version 5.1
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[string[]] $SwitchPriority = @('ethbr0', 'wlanbr0'),
[string] $InstallDir = (Join-Path $env:LOCALAPPDATA 'WslSwitchSelector'),
[ValidateRange(1, 1440)]
[int] $PollMinutes = 5,
[string] $DockerDesktopExe = (Join-Path $env:ProgramFiles 'Docker\Docker\Docker Desktop.exe'),
# Extra arguments passed through to Update-WslSwitch.ps1, e.g. '-IgnoreGateway -NoToast'
[string] $ExtraArguments = '',
# Use powershell.exe -WindowStyle Hidden instead of conhost --headless (pre-Win11 22H2).
[switch] $NoHeadlessConhost,
[switch] $RunNow,
[switch] $Uninstall
)
$ErrorActionPreference = 'Stop'
$TaskPath = '\WSL\'
$MainTask = 'Select Bridge Switch'
$DockerTask = 'Start Docker Desktop'
if ($Uninstall) {
foreach ($n in $MainTask, $DockerTask) {
if (Get-ScheduledTask -TaskPath $TaskPath -TaskName $n -ErrorAction SilentlyContinue) {
Unregister-ScheduledTask -TaskPath $TaskPath -TaskName $n -Confirm:$false
Write-Host "Removed $TaskPath$n"
}
}
Write-Host "Script and logs left in $InstallDir"
return
}
$SwitchPriority = @($SwitchPriority | ForEach-Object { $_ -split '[,;\s]+' } | Where-Object { $_ })
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$esc = { param($s) [Security.SecurityElement]::Escape($s) }
# Sanity checks
foreach ($s in $SwitchPriority) {
$sw = Get-VMSwitch -Name $s -ErrorAction SilentlyContinue
if (-not $sw) { Write-Warning "Switch '$s' does not exist (yet)." }
elseif ($sw.SwitchType -ne 'External') { Write-Warning "Switch '$s' is $($sw.SwitchType), not External." }
}
if (-not (Test-Path -LiteralPath (Join-Path $env:USERPROFILE '.wslconfig'))) {
Write-Warning "No .wslconfig in $env:USERPROFILE. Are you running elevated as a different account?"
}
# Copy the worker script
$src = Join-Path $PSScriptRoot 'Update-WslSwitch.ps1'
if (-not (Test-Path -LiteralPath $src)) { throw "Update-WslSwitch.ps1 not found next to this installer" }
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
$dst = Join-Path $InstallDir 'Update-WslSwitch.ps1'
Copy-Item -LiteralPath $src -Destination $dst -Force
Unblock-File -LiteralPath $dst
# Build the action
$psArgs = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$dst`" -SwitchPriority $($SwitchPriority -join ',') -DockerDesktopExe `"$DockerDesktopExe`" $ExtraArguments".Trim()
if ($NoHeadlessConhost) {
$cmd = 'powershell.exe'; $args_ = $psArgs
} else {
# conhost --headless avoids a console (or Windows Terminal) window flashing up.
$cmd = 'conhost.exe'; $args_ = "--headless powershell.exe $psArgs"
}
$query = @"
<QueryList>
<Query Id="0" Path="Microsoft-Windows-NetworkProfile/Operational">
<Select Path="Microsoft-Windows-NetworkProfile/Operational">*[System[(EventID=10000 or EventID=10001)]]</Select>
<Select Path="Microsoft-Windows-WLAN-AutoConfig/Operational">*[System[(EventID=8001 or EventID=8003)]]</Select>
</Query>
</QueryList>
"@
$start = (Get-Date).Date.ToString('yyyy-MM-ddTHH:mm:ss')
$mainXml = @"
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>Selects the connected Hyper-V external switch for WSL2 bridged networking, updates .wslconfig and restarts WSL and Docker Desktop when it changes.</Description>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Enabled>true</Enabled>
<UserId>$sid</UserId>
<Delay>PT45S</Delay>
</LogonTrigger>
<EventTrigger>
<Enabled>true</Enabled>
<Subscription>$(& $esc $query)</Subscription>
<Delay>PT15S</Delay>
</EventTrigger>
<TimeTrigger>
<Repetition>
<Interval>PT$($PollMinutes)M</Interval>
<StopAtDurationEnd>false</StopAtDurationEnd>
</Repetition>
<StartBoundary>$start</StartBoundary>
<Enabled>true</Enabled>
</TimeTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>$sid</UserId>
<LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT10M</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>$(& $esc $cmd)</Command>
<Arguments>$(& $esc $args_)</Arguments>
</Exec>
</Actions>
</Task>
"@
$dockerXml = @"
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>Launches Docker Desktop non-elevated. Called by \WSL\Select Bridge Switch.</Description>
</RegistrationInfo>
<Triggers />
<Principals>
<Principal id="Author">
<UserId>$sid</UserId>
<LogonType>InteractiveToken</LogonType>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
</Settings>
<Actions Context="Author">
<Exec>
<Command>$(& $esc $DockerDesktopExe)</Command>
</Exec>
</Actions>
</Task>
"@
Register-ScheduledTask -TaskPath $TaskPath -TaskName $DockerTask -Xml $dockerXml -Force | Out-Null
Write-Host "Registered $TaskPath$DockerTask"
Register-ScheduledTask -TaskPath $TaskPath -TaskName $MainTask -Xml $mainXml -Force | Out-Null
Write-Host "Registered $TaskPath$MainTask (priority: $($SwitchPriority -join ' > '), poll every $PollMinutes min)"
Write-Host "Log: $(Join-Path $InstallDir 'WslSwitchSelector.log')"
if ($RunNow) {
Start-ScheduledTask -TaskPath $TaskPath -TaskName $MainTask
Write-Host 'Started. Check the log in a few seconds.'
}