Skip to content

Commit 73f979e

Browse files
committed
VSCode-Updater v3.0.0 — deterministic installer engine
- Replaced v2.x watchdog with new module-based, deterministic v3.0 engine. - Removed filesystem scanning, CPU/disk heuristics, and legacy idle/active state machine. - Added module-delta progress detection, phase tracking, and final-module completion detection. - Updated installer wrapper to integrate v3.0 engine. - Added fallback-prevention logic so ZIP fallback is not triggered after successful installs. - Updated module manifest and exports for v3.0.0.
1 parent 5d38904 commit 73f979e

4 files changed

Lines changed: 116 additions & 126 deletions

File tree

Private/Invoke-InstallerWatchdog.ps1

Lines changed: 81 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@
88
Created: 2026-04-16
99
Modified: 2026-07-17
1010
File: Private/Invoke-InstallerWatchdog.ps1
11-
Version: 1.1.0
12-
Description: Monitors the VS Code installer and related worker processes for CPU and disk activity,
13-
detects idle or stalled states, and terminates processes when the installer becomes
14-
unresponsive. Uses real-time loop deltas and basic invariants for stability.
11+
Version: 2.2.0
12+
Description: Monitors the VS Code installer using deterministic module‑based progress detection.
13+
Tracks child‑process module loads, unloads, and phase transitions to identify real
14+
installer activity, replacing filesystem‑scanning heuristics with noise‑free,
15+
phase‑aware stall detection. Terminates the installer only when module state and
16+
CPU/disk activity indicate a true stall.
1517
#>
1618
# PSScriptAnalyzer SuppressMessage = PSUseApprovedVerbs "Intentional verb"
19+
# PSScriptAnalyzer SuppressMessage = PSUseConsistentWhitespace
20+
# PSScriptAnalyzer SuppressMessage = PSUseConsistentIndentation
1721
function Invoke-InstallerWatchdog {
1822
[OutputType([Int32])]
1923
param(
@@ -22,167 +26,128 @@ function Invoke-InstallerWatchdog {
2226
[int]$IdleTimeout
2327
)
2428

29+
# Validate parent
2530
if ($ParentPID -eq 0 -or -not (Get-Process -Id $ParentPID -ErrorAction SilentlyContinue)) {
2631
Write-VSCodeUpdaterLog "[WATCHDOG] Invalid parent PID ($ParentPID) — aborting watchdog"
2732
return [WatchdogExitCode]::Unknown
2833
}
2934

35+
# Safe mode bypass
3036
if ($script:SafeMode) {
3137
Write-VSCodeUpdaterLog "[WATCHDOG] SAFE MODE — watchdog disabled"
3238
return [WatchdogExitCode]::Success
3339
}
3440

35-
$idleSeconds = 0.0
36-
$activeSeconds = 0.0
37-
$fsIdleSeconds = 0.0
38-
$lastState = ""
39-
$lastCPU = 0.0
40-
$lastDisk = 0
41-
42-
# Detect real VS Code install path from the 'code' command
43-
$codeCmd = Get-Command code -ErrorAction SilentlyContinue
44-
45-
$installPaths = @()
46-
47-
if ($codeCmd -and $codeCmd.Source) {
48-
$installPaths += (Split-Path $codeCmd.Source -Parent)
49-
}
41+
# Module tracking (v3.0)
42+
$previousModules = @()
43+
$moduleInitialized = $false
44+
$currentPhase = "Bootstrap"
5045

51-
# Fallbacks
52-
$installPaths += @(
53-
"$env:LOCALAPPDATA\Programs\Microsoft VS Code",
54-
"$env:LOCALAPPDATA\Programs\VSCode",
55-
$env:TEMP
56-
)
57-
58-
$lastWriteTime = Get-Date
59-
$fsLogCooldown = 30
60-
$lastFsLog = (Get-Date).AddSeconds(-10)
46+
# Final install directory detection
47+
$finalInstallRoot = (Join-Path $env:LOCALAPPDATA "Programs\Microsoft VS Code")
6148

49+
# Idle timer
50+
$idleSeconds = 0.0
6251
$lastLoopTime = Get-Date
6352

6453
Write-VSCodeUpdaterLog "[WATCHDOG] Monitoring child PID $($ChildProcess.Id), parent PID $ParentPID"
6554

66-
# Grace period: let installer initialize
55+
# Grace period
6756
Start-Sleep -Seconds 3
68-
$fsIdleSeconds = 0.0
6957
$idleSeconds = 0.0
70-
$activeSeconds = 0.0
7158

7259
while ($true) {
73-
$now = Get-Date
60+
61+
# Loop delta
62+
$now = Get-Date
7463
$delta = ($now - $lastLoopTime).TotalSeconds
7564
if ($delta -lt 0) { $delta = 0 }
7665
$lastLoopTime = $now
7766

78-
$fsIdleSeconds += $delta
7967
$idleSeconds += $delta
80-
$activeSeconds += $delta
8168

69+
# Refresh child
8270
$child = Get-Process -Id $ChildProcess.Id -ErrorAction SilentlyContinue
8371

8472
if (-not $child) {
85-
$newChild = Get-Process -ErrorAction SilentlyContinue |
86-
Where-Object { $_.Parent.Id -eq $ParentPID }
87-
88-
if ($newChild) {
89-
Write-VSCodeUpdaterLog "[WATCHDOG] Child replaced — new PID $($newChild.Id)"
90-
$ChildProcess = $newChild
91-
continue
92-
}
93-
94-
Write-VSCodeUpdaterLog "[WATCHDOG] Child exited — success"
73+
Write-VSCodeUpdaterLog "[WATCHDOG] Child exited — installer completed"
9574
return [WatchdogExitCode]::Success
9675
}
9776

77+
#
78+
# MODULE-BASED PROGRESS DETECTION (v3.0)
79+
#
9880
try {
99-
$latestWrite = $null
100-
101-
foreach ($path in $installPaths) {
102-
try {
103-
if (Test-Path $path) {
104-
$candidate = Get-ChildItem -Recurse $path -File -ErrorAction SilentlyContinue |
105-
Where-Object {
106-
$_.Extension -notin '.log', '.tmp', '.bak' -and
107-
$_.FullName -notmatch '\\logs?\\' -and
108-
$_.FullName -notmatch '\\Crashpad\\'
109-
} |
110-
Sort-Object LastWriteTime |
111-
Select-Object -Last 1
112-
113-
if ($candidate -and (!$latestWrite -or $candidate.LastWriteTime -gt $latestWrite.LastWriteTime)) {
114-
$latestWrite = $candidate
115-
}
116-
}
117-
}
118-
catch {
119-
Write-VSCodeUpdaterLog "[WATCHDOG] FS scan exception on $($path): $($_.Exception.Message)"
120-
}
81+
# Normalize module paths
82+
$currentModules = $child.Modules.FileName |
83+
ForEach-Object { $_.ToLowerInvariant() }
84+
85+
if (-not $moduleInitialized) {
86+
Write-VSCodeUpdaterLog "[WATCHDOG] Module tracking initialized ($($currentModules.Count) modules)"
87+
$previousModules = $currentModules
88+
$moduleInitialized = $true
12189
}
90+
else {
91+
# Single delta calculation
92+
$moduleDelta = Compare-Object $previousModules $currentModules
12293

123-
if ($latestWrite -and $latestWrite.LastWriteTime -gt $lastWriteTime) {
124-
if ((Get-Date) -gt $lastFsLog.AddSeconds($fsLogCooldown)) {
125-
Write-VSCodeUpdaterLog "[WATCHDOG] FS activity: $($latestWrite.FullName)"
126-
$lastFsLog = Get-Date
127-
}
94+
if ($moduleDelta) {
95+
Write-VSCodeUpdaterLog "[WATCHDOG] Module change detected"
96+
$previousModules = $currentModules
97+
$idleSeconds = 0.0
12898

129-
$lastWriteTime = $latestWrite.LastWriteTime
130-
$fsIdleSeconds = 0.0
131-
$activeSeconds = 0.0
132-
$idleSeconds = 0.0
99+
#
100+
# PHASE DETECTION (v3.0)
101+
#
102+
$newPhase = $currentPhase
103+
104+
if ($currentModules -match '\\nsm.*\.tmp\\') {
105+
$newPhase = "Extraction"
106+
}
107+
elseif ($currentModules -match 'chrome_elf\.dll' -or
108+
$currentModules -match 'node\.dll') {
109+
$newPhase = "Payload"
110+
}
111+
elseif ($currentModules -match '\microsoft vs code\\') {
112+
$newPhase = "Finalization"
113+
}
114+
115+
if ($newPhase -ne $currentPhase) {
116+
$currentPhase = $newPhase
117+
Write-VSCodeUpdaterLog "[WATCHDOG] Phase: $currentPhase"
118+
}
119+
}
133120
}
134121
}
135122
catch {
136-
Write-VSCodeUpdaterLog "[WATCHDOG] Exception: $($_.Exception.Message)"
123+
Write-VSCodeUpdaterLog "[WATCHDOG] Module polling exception: $($_.Exception.Message)"
137124
}
138125

139-
if ($fsIdleSeconds -ge $IdleTimeout) {
140-
Write-VSCodeUpdaterLog "[WATCHDOG] FS stall after {0:N2}s — killing installer" -f $fsIdleSeconds
141-
Stop-Process -Id $ChildProcess.Id -Force -ErrorAction SilentlyContinue
142-
Stop-Process -Id $ParentPID -Force -ErrorAction SilentlyContinue
143-
return [WatchdogExitCode]::FSStalled
144-
}
145-
146-
$cpuNow = $child.CPU
147-
$diskNow = $child.IOReadBytes + $child.IOWriteBytes
126+
#
127+
# FINALIZATION SUCCESS DETECTION (v3.0)
128+
#
129+
$finalModulesLoaded = $currentModules |
130+
Where-Object { $_ -like "$($finalInstallRoot.ToLowerInvariant())\*" }
148131

149-
$cpuDelta = $cpuNow - $lastCPU
150-
$diskDelta = $diskNow - $lastDisk
151-
152-
$lastCPU = $cpuNow
153-
$lastDisk = $diskNow
132+
if ($finalModulesLoaded.Count -gt 0 -and $currentPhase -eq "Finalization") {
133+
Write-VSCodeUpdaterLog "[WATCHDOG] Final modules loaded — installer completed"
134+
return [WatchdogExitCode]::Success
135+
}
154136

155-
if ($cpuDelta -eq 0 -and $diskDelta -eq 0) {
156-
if ($lastState -ne "Idle") {
157-
Write-VSCodeUpdaterLog "[WATCHDOG] Child transitioned to idle"
158-
$lastState = "Idle"
159-
}
137+
#
138+
# PURE MODULE-ONLY STALL DETECTION (v3.0)
139+
#
140+
$moduleDelta = Compare-Object $previousModules $currentModules
160141

142+
if (-not $moduleDelta) {
161143
if ($idleSeconds -ge $IdleTimeout) {
162-
Write-VSCodeUpdaterLog "[WATCHDOG] Idle stall after {0:N2}s — killing parent" -f $idleSeconds
163-
Stop-Process -Id $ParentPID -Force -ErrorAction SilentlyContinue
164-
Wait-Process -Id $ChildProcess.Id -ErrorAction SilentlyContinue
144+
Write-VSCodeUpdaterLog "[WATCHDOG] Deterministic stall — no module changes for $IdleTimeout seconds"
145+
Stop-Process -Id $ChildProcess.Id -Force -ErrorAction SilentlyContinue
146+
Stop-Process -Id $ParentPID -Force -ErrorAction SilentlyContinue
165147
return [WatchdogExitCode]::IdleStalled
166148
}
167149
}
168150
else {
169-
if ($lastState -ne "Active") {
170-
Write-VSCodeUpdaterLog "[WATCHDOG] Child transitioned to active"
171-
$lastState = "Active"
172-
}
173-
174-
if ($cpuDelta -eq 0 -and $diskDelta -eq 0) {
175-
if ($activeSeconds -ge $IdleTimeout) {
176-
Write-VSCodeUpdaterLog "[WATCHDOG] Active stall after {0:N2}s — killing parent and child" -f $activeSeconds
177-
Stop-Process -Id $ChildProcess.Id -Force -ErrorAction SilentlyContinue
178-
Stop-Process -Id $ParentPID -Force -ErrorAction SilentlyContinue
179-
return [WatchdogExitCode]::ActiveStalled
180-
}
181-
}
182-
else {
183-
$activeSeconds = 0.0
184-
}
185-
186151
$idleSeconds = 0.0
187152
}
188153

Private/Invoke-InstallerWrapper.ps1

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,42 @@ function Invoke-InstallerWrapper {
2424
)
2525

2626
try {
27+
Clear-SetupBootstrapper | Out-Null
2728
$parent = Invoke-InstallerDetached -Path $InstallerPath
2829
$parentPID = $parent.Id
2930

3031
Write-VSCodeUpdaterLog "[WRAPPER] Parent PID: $parentPID"
3132

33+
$timeout = 15000 # 15 seconds
34+
$elapsed = 0
35+
$interval = 250
36+
3237
$child = $null
33-
for ($i = 1; $i -le 10; $i++) {
34-
$child = Get-InnoChildProcess -ParentPID $parentPID
38+
while ($elapsed -lt $timeout) {
39+
40+
# direct or indirect worker
41+
$child = Get-Process -ErrorAction SilentlyContinue |
42+
Where-Object {
43+
$_.Name -match '^is-[A-Za-z0-9]+' -or
44+
$_.Name -match 'tmp$' -or
45+
$_.Name -match 'tmp\.exe$' -or
46+
($_.Path -and $_.Path -match 'is-[A-Za-z0-9]+\.tmp')
47+
} |
48+
Sort-Object StartTime |
49+
Select-Object -Last 1
50+
3551
if ($child) { break }
36-
Start-Sleep -Milliseconds 300
52+
53+
Start-Sleep -Milliseconds $interval
54+
$elapsed += $interval
3755
}
3856

3957
if (-not $child) {
4058
Write-VSCodeUpdaterLog "[WRAPPER] No child worker detected"
4159
return [WatchdogExitCode]::InstallerFailed
4260
}
4361

44-
$childPID = $child.ProcessId
62+
$childPID = $child.Id
4563
Write-VSCodeUpdaterLog "[WRAPPER] Child PID: $childPID"
4664

4765
# ---------------------------------------------------------------------

VSCode-Updater.psd1

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
#>
1414
@{
1515
RootModule = 'VSCode-Updater.psm1'
16-
ModuleVersion = '2.2.0'
16+
ModuleVersion = '3.0.0'
1717
GUID = 'f2776614-4b50-45ba-b8fe-63875c447ab5'
1818
Author = 'Leon McClatchey'
1919
CompanyName = 'Linktech Engineering LLC'
20-
Description = 'Deterministic, audit-transparent VS Code updater for Windows.'
20+
Description = 'Deterministic installer engine with module‑based progress detection, replacing
21+
filesystem‑scanning heuristics with phase‑aware, child‑specific module tracking for
22+
reliable progress monitoring and accurate stall detection.'
2123
PowerShellVersion = '7.0'
2224
FunctionsToExport = @(
2325
'Update-VSCode',
@@ -43,7 +45,8 @@
4345
LicenseUri = 'https://github.com/Linktech-Engineering-LLC/VSCode-Updater/blob/main/LICENSE'
4446
IconUri = 'https://raw.githubusercontent.com/Linktech-Engineering-LLC/VSCode-Updater/main/icon.png'
4547
Tags = @('vscode' , 'update' , 'automation' , 'windows' , 'powershell' , 'devtools')
46-
ReleaseNotes = 'Initial public release of VSCode-Updater.'
48+
ReleaseNotes = 'v3.0 introduces a new module‑based installer engine, replacing filesystem scanning with
49+
deterministic phase tracking and improving reliability of progress and stall detection.'
4750
}
4851
}
4952
}

VSCode-Updater.psm1

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
Created: 2026-04-16
99
Modified: 2026-07-05
1010
File: VSCode-Updater.psm1
11-
Version: 2.2.0
11+
Version: 3.0.0
1212
Description: Module root for VSCode-Updater. Loads public functions, wires private helpers,
1313
and exposes deterministic update, rollback, symlink diagnostics, and safe-mode operations.
1414
#>
@@ -38,7 +38,7 @@ Get-ChildItem -Path "$PSScriptRoot/Public" -Filter *.ps1 |
3838

3939
Set-Variable -Name VSU_MaxRetries -Value 5 -Scope Script -Option ReadOnly
4040
Set-Variable -Name VSU_DetectTimeout -Value 10 -Scope Script -Option ReadOnly
41-
Set-Variable -Name VSU_DefaultIdle -Value 600 -Scope Script -Option ReadOnly
41+
Set-Variable -Name VSU_DefaultIdle -Value 900 -Scope Script -Option ReadOnly
4242
Set-Variable -Name VSU_SafeInstallerMode -Value $true -Scope Script -Option ReadOnly
4343

4444
# Load module version from manifest
@@ -57,6 +57,9 @@ function Update-VSCode {
5757
param()
5858

5959
$null = Write-VSCodeUpdaterLog "[UPDATE] Starting VS Code update"
60+
Clear-SetupBootstrapper | Out-Null
61+
Clear-VSCodeHelpers | Out-Null
62+
Clear-InnoSetupWorkers | Out-Null
6063

6164
if (-not $PSCmdlet.ShouldProcess("VS Code installation", "Update")) {
6265
$null = Write-VSCodeUpdaterLog "[UPDATE] ShouldProcess declined"
@@ -90,7 +93,7 @@ function Update-VSCode {
9093
$attempt++
9194
$null = Write-VSCodeUpdaterLog "[ATTEMPT] Installer attempt $attempt of $maxAttempts"
9295

93-
$result = Invoke-InstallerWrapper -InstallerPath $cachedInstaller -IdleTimeout $IdleTimeout
96+
$result = Invoke-InstallerWrapper -InstallerPath $cachedInstaller -IdleTimeout $script:VSU_DefaultIdle
9497

9598
Clear-VSCodeHelpers | Out-Null
9699
Clear-InnoSetupWorkers | Out-Null
@@ -119,6 +122,7 @@ function Update-VSCode {
119122
}
120123

121124
$null = Write-VSCodeUpdaterLog "[RETRY] Cleaning processes and artifacts before retry"
125+
Clear-SetupBootstrapper | Out-Null
122126
Clear-VSCodeHelpers | Out-Null
123127
Clear-InnoSetupWorkers | Out-Null
124128
}

0 commit comments

Comments
 (0)