windowsweep 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-INTEGRATION-GUIDE.md +16 -12
- package/CHANGELOG.md +82 -0
- package/LICENSE +21 -21
- package/README.md +104 -69
- package/VERSION +1 -1
- package/lib/actions.ps1 +212 -212
- package/lib/config.ps1 +108 -99
- package/lib/constants.ps1 +103 -93
- package/lib/external.ps1 +87 -0
- package/lib/fs.ps1 +189 -180
- package/lib/log.ps1 +140 -140
- package/lib/safety.ps1 +467 -493
- package/lib/scan.ps1 +109 -100
- package/lib/ui.ps1 +275 -275
- package/modules/android_avd.ps1 +36 -36
- package/modules/app_caches.ps1 +58 -58
- package/modules/browsers.ps1 +33 -33
- package/modules/build_tools.ps1 +35 -35
- package/modules/crash_trap.ps1 +89 -89
- package/modules/disk_usage.ps1 +49 -49
- package/modules/docker.ps1 +93 -93
- package/modules/editors.ps1 +150 -150
- package/modules/globals_audit.ps1 +236 -236
- package/modules/health.ps1 +101 -101
- package/modules/installed_programs.ps1 +118 -118
- package/modules/menu.ps1 +63 -63
- package/modules/notify.ps1 +60 -60
- package/modules/orphaned_appdata.ps1 +189 -189
- package/modules/personal.ps1 +126 -126
- package/modules/pkg_managers.ps1 +73 -73
- package/modules/projects.ps1 +160 -160
- package/modules/release_helpers.ps1 +438 -438
- package/modules/reports.ps1 +212 -212
- package/modules/runner.ps1 +246 -239
- package/modules/self_test_contract.ps1 +195 -0
- package/modules/self_test_extra.ps1 +366 -360
- package/modules/startup_audit.ps1 +126 -126
- package/modules/system_admin.ps1 +220 -220
- package/modules/test_runners.ps1 +24 -24
- package/modules/walkthrough.ps1 +80 -79
- package/modules/windows_user.ps1 +118 -118
- package/package.json +3 -3
- package/windowsweep.ps1 +322 -316
package/modules/runner.ps1
CHANGED
|
@@ -1,239 +1,246 @@
|
|
|
1
|
-
# runner.ps1 - section dispatch, batch policy enforcement, the read-only scan mode and the summary.
|
|
2
|
-
|
|
3
|
-
function Get-SectionIdList {
|
|
4
|
-
<# .SYNOPSIS Parse "1,3,5-7" into section ids; validates against the catalogue. #>
|
|
5
|
-
param([string] $Text)
|
|
6
|
-
$ids = New-Object System.Collections.Generic.SortedSet[int]
|
|
7
|
-
foreach ($part in ($Text -split ',')) {
|
|
8
|
-
$part = $part.Trim()
|
|
9
|
-
if (-not $part) { continue }
|
|
10
|
-
if ($part -match '^(\d+)-(\d+)$') {
|
|
11
|
-
$a = [int]$Matches[1]; $b = [int]$Matches[2]
|
|
12
|
-
if ($a -gt $b) { $t = $a; $a = $b; $b = $t }
|
|
13
|
-
for ($i = $a; $i -le $b; $i++) { if (Get-Section $i) { $null = $ids.Add($i) } else { Write-Warn "no section $i - ignored" } }
|
|
14
|
-
} elseif ($part -match '^\d+$') {
|
|
15
|
-
$i = [int]$part
|
|
16
|
-
if (Get-Section $i) { $null = $ids.Add($i) } else { Write-Warn "no section $i - ignored" }
|
|
17
|
-
} else {
|
|
18
|
-
Write-Warn "cannot parse section '$part' - ignored"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return @($ids)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function Get-RequestedSections {
|
|
25
|
-
<# .SYNOPSIS Resolve --all / --only / --profile / --exclude into the ordered list to run. #>
|
|
26
|
-
$ws = $Script:WS
|
|
27
|
-
$ids = @()
|
|
28
|
-
if ($ws.Mode -eq 'all') {
|
|
29
|
-
$ids = @($Script:WS_SAFE_BATCH)
|
|
30
|
-
if ($ws.IsAdmin) { $ids += $Script:WS_SAFE_BATCH_ADMIN }
|
|
31
|
-
} elseif ($ws.OnlyList) {
|
|
32
|
-
$ids = Get-SectionIdList $ws.OnlyList
|
|
33
|
-
} elseif ($ws.Profile) {
|
|
34
|
-
$ids = @($Script:WS_PROFILES[$ws.Profile])
|
|
35
|
-
Write-Info "profile '$($ws.Profile)' = sections $($ids -join ',')"
|
|
36
|
-
}
|
|
37
|
-
if ($ws.Exclude) {
|
|
38
|
-
$ex = Get-SectionIdList $ws.Exclude
|
|
39
|
-
$ids = @($ids | Where-Object { $ex -notcontains $_ })
|
|
40
|
-
Write-Info "after --exclude: sections $($ids -join ',')"
|
|
41
|
-
}
|
|
42
|
-
return @($ids | Sort-Object -Unique)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function Get-MachineProgressLine {
|
|
46
|
-
<# .SYNOPSIS The progress-line format a --json caller parses. Kept pure so the self-test can round-trip it. #>
|
|
47
|
-
param([int] $Section, [string] $Stage, [string] $Status = '', [long] $Freed = 0)
|
|
48
|
-
$line = "##$Script:WS_NAME section=$Section event=$Stage"
|
|
49
|
-
if ($Stage -eq 'end') { $line += " status=$Status freed_bytes=$Freed" }
|
|
50
|
-
return $line
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function Write-MachineProgress {
|
|
54
|
-
<# .SYNOPSIS One machine-readable progress line on stderr, --json mode only, so a GUI need not parse the log.
|
|
55
|
-
Never on stdout: --json promises exactly one stdout line. #>
|
|
56
|
-
param([int] $Section, [string] $Stage, [string] $Status = '', [long] $Freed = 0)
|
|
57
|
-
if (-not $Script:WS.JsonMode) { return }
|
|
58
|
-
[Console]::Error.WriteLine((Get-MachineProgressLine -Section $Section -Stage $Stage -Status $Status -Freed $Freed))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function Add-JsonCandidate {
|
|
62
|
-
<# .SYNOPSIS Record one item an interactive section offered, so --json callers can present the same list. #>
|
|
63
|
-
param([int] $Section, [int] $Index, [string] $Path, [long] $Bytes, [int] $IdleDays, [string] $Project = '')
|
|
64
|
-
if (-not $Script:WS.JsonMode) { return }
|
|
65
|
-
$Script:WS.Candidates += [ordered]@{ section = $Section; index = $Index; path = $Path; bytes = $Bytes; idle_days = $IdleDays; project = $Project }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function Invoke-SectionById {
|
|
69
|
-
<# .SYNOPSIS Run one section under the batch, admin and developer policies; records the report step. #>
|
|
70
|
-
param([int] $Id)
|
|
71
|
-
$ws = $Script:WS
|
|
72
|
-
$sec = Get-Section $Id
|
|
73
|
-
if (-not $sec) { Write-Warn "no section $Id"; return }
|
|
74
|
-
$title = "[{0:00}] {1}" -f $sec.Id, $sec.Title
|
|
75
|
-
Write-Box $title
|
|
76
|
-
Write-MachineProgress -Section $Id -Stage 'start'
|
|
77
|
-
# Batch policy: deep and interactive-only sections never run unattended without an explicit opt-in.
|
|
78
|
-
if ($ws.BatchMode -and -not $ws.DryRun) {
|
|
79
|
-
if ($sec.Batch -eq 'interactive' -and -not $ws.SelectActive) {
|
|
80
|
-
Write-Warn "section $Id is interactive-only: it needs a person at the keyboard, or a selection passed with --select / --select-file."
|
|
81
|
-
$ws.Refusals += "section $Id (interactive-only)"
|
|
82
|
-
Add-ReportStep -Section $Id -Title $sec.Title -Status 'refused' -Note 'interactive-only section in batch mode'
|
|
83
|
-
Write-MachineProgress -Section $Id -Stage 'end' -Status 'refused'
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
if ($sec.Batch -eq 'interactive') {
|
|
87
|
-
Write-Note "section $Id runs unattended because a selection was supplied (--select / --select-file): a person chose these."
|
|
88
|
-
}
|
|
89
|
-
if ($sec.Batch -eq 'deep' -and -not $ws.Deep) {
|
|
90
|
-
Write-Warn "section $Id is deep (irreversible or system-changing): refused in batch mode without --i-understand-deep."
|
|
91
|
-
$ws.Refusals += "section $Id (deep, add --i-understand-deep)"
|
|
92
|
-
Add-ReportStep -Section $Id -Title $sec.Title -Status 'refused' -Note 'deep section without --i-understand-deep'
|
|
93
|
-
Write-MachineProgress -Section $Id -Stage 'end' -Status 'refused'
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if ($sec.Admin -and -not $ws.IsAdmin) {
|
|
98
|
-
Write-Warn "section $Id needs Administrator rights - skipped."
|
|
99
|
-
Write-Note "run it elevated: windowsweep --only $Id --yes --elevate"
|
|
100
|
-
$ws.Hints += "Admin section $Id skipped: windowsweep --only $Id --yes --elevate"
|
|
101
|
-
Add-ReportStep -Section $Id -Title $sec.Title -Status 'skipped' -Note 'needs Administrator'
|
|
102
|
-
Write-MachineProgress -Section $Id -Stage 'end' -Status 'skipped'
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
if ($sec.Dev -and $ws.Developer -eq $false -and $Id -in @(4, 17, 20)) {
|
|
106
|
-
Write-Info "developer mode is off - section $Id (developer-only data) skipped."
|
|
107
|
-
Add-ReportStep -Section $Id -Title $sec.Title -Status 'skipped' -Note 'developer mode off'
|
|
108
|
-
Write-MachineProgress -Section $Id -Stage 'end' -Status 'skipped'
|
|
109
|
-
return
|
|
110
|
-
}
|
|
111
|
-
$ws.SectionFreed = [long]0
|
|
112
|
-
$before = Get-SystemDriveFree
|
|
113
|
-
$status = 'ran'
|
|
114
|
-
if ($ws.DryRun) { $status = 'dry-run' }
|
|
115
|
-
try {
|
|
116
|
-
& $sec.Fn
|
|
117
|
-
} catch {
|
|
118
|
-
$status = 'failed'
|
|
119
|
-
Write-Err "section $Id failed: $($_.Exception.Message)"
|
|
120
|
-
Write-LogLine ("section $Id error: " + $_.ScriptStackTrace)
|
|
121
|
-
$ws.ExitCode = $Script:WS_EXIT_FAIL
|
|
122
|
-
}
|
|
123
|
-
$after = Get-SystemDriveFree
|
|
124
|
-
$delta = $after - $before
|
|
125
|
-
if ($delta -lt 0) { $delta = 0 }
|
|
126
|
-
$freed = $ws.SectionFreed
|
|
127
|
-
if ($ws.DryRun) {
|
|
128
|
-
if ($freed -gt 0) { Write-UiLine (" [dry-run] this section would
|
|
129
|
-
} elseif ($freed -gt 0 -or $delta -gt 0) {
|
|
130
|
-
Write-UiLine (" {0} this section
|
|
131
|
-
} else {
|
|
132
|
-
Write-Note 'no measurable change in this section'
|
|
133
|
-
}
|
|
134
|
-
Add-ReportStep -Section $Id -Title $sec.Title -Status $status -Freed $freed
|
|
135
|
-
Write-MachineProgress -Section $Id -Stage 'end' -Status $status -Freed $freed
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function Invoke-BatchMode {
|
|
139
|
-
<# .SYNOPSIS --all / --only / --profile #>
|
|
140
|
-
$ws = $Script:WS
|
|
141
|
-
$ids = @(Get-RequestedSections)
|
|
142
|
-
if ($ids.Count -eq 0) { Write-Err 'no sections to run'; $ws.ExitCode = $Script:WS_EXIT_USAGE; return }
|
|
143
|
-
Write-Banner
|
|
144
|
-
Show-RunHeader
|
|
145
|
-
Write-Info "running sections: $($ids -join ',')"
|
|
146
|
-
foreach ($id in $ids) { Invoke-SectionById -Id $id }
|
|
147
|
-
Show-SessionSummary
|
|
148
|
-
if ($ws.Refusals.Count -gt 0 -and $ws.Mode -eq 'only' -and $ws.ExitCode -eq 0) { $ws.ExitCode = $Script:WS_EXIT_REFUSED }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function Show-RunHeader {
|
|
152
|
-
$ws = $Script:WS
|
|
153
|
-
$strategy = "prune files idle $($ws.Days)+ days (temp: $($ws.TempDays)+ days)"
|
|
154
|
-
if ($ws.PurgeAll) { $strategy = 'FULL PURGE of cache targets' }
|
|
155
|
-
$dev = 'off'
|
|
156
|
-
if ($ws.Developer) { $dev = "on ($($ws.DeveloperSource))" }
|
|
157
|
-
$adm = 'no'
|
|
158
|
-
if ($ws.IsAdmin) { $adm = 'yes' }
|
|
159
|
-
Write-Info "mode: $($ws.Mode) strategy: $strategy developer: $dev elevated: $adm auto-yes: $($ws.Yes)"
|
|
160
|
-
if ($ws.DryRun) { Write-UiLine ' DRY-RUN: nothing will be deleted; sizes below are estimates of what a real run would remove.' 'Magenta' }
|
|
161
|
-
Write-Info "log: $($ws.LogFile)"
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function Invoke-ScanMode {
|
|
165
|
-
<# .SYNOPSIS --scan: health report + every target's size + the read-only personal scanners. Deletes nothing. #>
|
|
166
|
-
$ws = $Script:WS
|
|
167
|
-
Write-Banner
|
|
168
|
-
Write-Info '
|
|
169
|
-
if (Get-Command Invoke-Section00 -ErrorAction SilentlyContinue) { Invoke-Section00 }
|
|
170
|
-
Write-Box 'Targets on disk' 'What each section can reach, and how much it currently holds'
|
|
171
|
-
$null = Show-ScanTable
|
|
172
|
-
if (Get-Command Show-PersonalScan -ErrorAction SilentlyContinue) { Show-PersonalScan }
|
|
173
|
-
Add-ReportStep -Section -1 -Title '
|
|
174
|
-
Show-SessionSummary
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function Show-SessionSummary {
|
|
178
|
-
$ws = $Script:WS
|
|
179
|
-
Write-Box 'Session summary'
|
|
180
|
-
if ($ws.DryRun) {
|
|
181
|
-
Write-Kv 'Mode:' 'DRY-RUN - nothing was deleted'
|
|
182
|
-
Write-Kv 'Would
|
|
183
|
-
} else {
|
|
184
|
-
Write-Kv 'Reclaimed:' (Format-Bytes $ws.TotalFreed)
|
|
185
|
-
}
|
|
186
|
-
$secs = [int][math]::Round(((Get-Date).Ticks - $ws.Report.startedTicks) / 10000000)
|
|
187
|
-
Write-Kv 'Duration:' (Format-Duration $secs)
|
|
188
|
-
$ran = @($ws.Report.steps | Where-Object { $_.status -in 'ran', 'dry-run' }).Count
|
|
189
|
-
$skipped = @($ws.Report.steps | Where-Object { $_.status -notin 'ran', 'dry-run' }).Count
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
Write-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
Write-
|
|
203
|
-
Write-
|
|
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
|
-
}
|
|
1
|
+
# runner.ps1 - section dispatch, batch policy enforcement, the read-only scan mode and the summary.
|
|
2
|
+
|
|
3
|
+
function Get-SectionIdList {
|
|
4
|
+
<# .SYNOPSIS Parse "1,3,5-7" into section ids; validates against the catalogue. #>
|
|
5
|
+
param([string] $Text)
|
|
6
|
+
$ids = New-Object System.Collections.Generic.SortedSet[int]
|
|
7
|
+
foreach ($part in ($Text -split ',')) {
|
|
8
|
+
$part = $part.Trim()
|
|
9
|
+
if (-not $part) { continue }
|
|
10
|
+
if ($part -match '^(\d+)-(\d+)$') {
|
|
11
|
+
$a = [int]$Matches[1]; $b = [int]$Matches[2]
|
|
12
|
+
if ($a -gt $b) { $t = $a; $a = $b; $b = $t }
|
|
13
|
+
for ($i = $a; $i -le $b; $i++) { if (Get-Section $i) { $null = $ids.Add($i) } else { Write-Warn "no section $i - ignored" } }
|
|
14
|
+
} elseif ($part -match '^\d+$') {
|
|
15
|
+
$i = [int]$part
|
|
16
|
+
if (Get-Section $i) { $null = $ids.Add($i) } else { Write-Warn "no section $i - ignored" }
|
|
17
|
+
} else {
|
|
18
|
+
Write-Warn "cannot parse section '$part' - ignored"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return @($ids)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function Get-RequestedSections {
|
|
25
|
+
<# .SYNOPSIS Resolve --all / --only / --profile / --exclude into the ordered list to run. #>
|
|
26
|
+
$ws = $Script:WS
|
|
27
|
+
$ids = @()
|
|
28
|
+
if ($ws.Mode -eq 'all') {
|
|
29
|
+
$ids = @($Script:WS_SAFE_BATCH)
|
|
30
|
+
if ($ws.IsAdmin) { $ids += $Script:WS_SAFE_BATCH_ADMIN }
|
|
31
|
+
} elseif ($ws.OnlyList) {
|
|
32
|
+
$ids = Get-SectionIdList $ws.OnlyList
|
|
33
|
+
} elseif ($ws.Profile) {
|
|
34
|
+
$ids = @($Script:WS_PROFILES[$ws.Profile])
|
|
35
|
+
Write-Info "profile '$($ws.Profile)' = sections $($ids -join ',')"
|
|
36
|
+
}
|
|
37
|
+
if ($ws.Exclude) {
|
|
38
|
+
$ex = Get-SectionIdList $ws.Exclude
|
|
39
|
+
$ids = @($ids | Where-Object { $ex -notcontains $_ })
|
|
40
|
+
Write-Info "after --exclude: sections $($ids -join ',')"
|
|
41
|
+
}
|
|
42
|
+
return @($ids | Sort-Object -Unique)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Get-MachineProgressLine {
|
|
46
|
+
<# .SYNOPSIS The progress-line format a --json caller parses. Kept pure so the self-test can round-trip it. #>
|
|
47
|
+
param([int] $Section, [string] $Stage, [string] $Status = '', [long] $Freed = 0)
|
|
48
|
+
$line = "##$Script:WS_NAME section=$Section event=$Stage"
|
|
49
|
+
if ($Stage -eq 'end') { $line += " status=$Status freed_bytes=$Freed" }
|
|
50
|
+
return $line
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Write-MachineProgress {
|
|
54
|
+
<# .SYNOPSIS One machine-readable progress line on stderr, --json mode only, so a GUI need not parse the log.
|
|
55
|
+
Never on stdout: --json promises exactly one stdout line. #>
|
|
56
|
+
param([int] $Section, [string] $Stage, [string] $Status = '', [long] $Freed = 0)
|
|
57
|
+
if (-not $Script:WS.JsonMode) { return }
|
|
58
|
+
[Console]::Error.WriteLine((Get-MachineProgressLine -Section $Section -Stage $Stage -Status $Status -Freed $Freed))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function Add-JsonCandidate {
|
|
62
|
+
<# .SYNOPSIS Record one item an interactive section offered, so --json callers can present the same list. #>
|
|
63
|
+
param([int] $Section, [int] $Index, [string] $Path, [long] $Bytes, [int] $IdleDays, [string] $Project = '')
|
|
64
|
+
if (-not $Script:WS.JsonMode) { return }
|
|
65
|
+
$Script:WS.Candidates += [ordered]@{ section = $Section; index = $Index; path = $Path; bytes = $Bytes; idle_days = $IdleDays; project = $Project }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Invoke-SectionById {
|
|
69
|
+
<# .SYNOPSIS Run one section under the batch, admin and developer policies; records the report step. #>
|
|
70
|
+
param([int] $Id)
|
|
71
|
+
$ws = $Script:WS
|
|
72
|
+
$sec = Get-Section $Id
|
|
73
|
+
if (-not $sec) { Write-Warn "no section $Id"; return }
|
|
74
|
+
$title = "[{0:00}] {1}" -f $sec.Id, $sec.Title
|
|
75
|
+
Write-Box $title
|
|
76
|
+
Write-MachineProgress -Section $Id -Stage 'start'
|
|
77
|
+
# Batch policy: deep and interactive-only sections never run unattended without an explicit opt-in.
|
|
78
|
+
if ($ws.BatchMode -and -not $ws.DryRun) {
|
|
79
|
+
if ($sec.Batch -eq 'interactive' -and -not $ws.SelectActive) {
|
|
80
|
+
Write-Warn "section $Id is interactive-only: it needs a person at the keyboard, or a selection passed with --select / --select-file."
|
|
81
|
+
$ws.Refusals += "section $Id (interactive-only)"
|
|
82
|
+
Add-ReportStep -Section $Id -Title $sec.Title -Status 'refused' -Note 'interactive-only section in batch mode'
|
|
83
|
+
Write-MachineProgress -Section $Id -Stage 'end' -Status 'refused'
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
if ($sec.Batch -eq 'interactive') {
|
|
87
|
+
Write-Note "section $Id runs unattended because a selection was supplied (--select / --select-file): a person chose these."
|
|
88
|
+
}
|
|
89
|
+
if ($sec.Batch -eq 'deep' -and -not $ws.Deep) {
|
|
90
|
+
Write-Warn "section $Id is deep (irreversible or system-changing): refused in batch mode without --i-understand-deep."
|
|
91
|
+
$ws.Refusals += "section $Id (deep, add --i-understand-deep)"
|
|
92
|
+
Add-ReportStep -Section $Id -Title $sec.Title -Status 'refused' -Note 'deep section without --i-understand-deep'
|
|
93
|
+
Write-MachineProgress -Section $Id -Stage 'end' -Status 'refused'
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if ($sec.Admin -and -not $ws.IsAdmin) {
|
|
98
|
+
Write-Warn "section $Id needs Administrator rights - skipped."
|
|
99
|
+
Write-Note "run it elevated: windowsweep --only $Id --yes --elevate"
|
|
100
|
+
$ws.Hints += "Admin section $Id skipped: windowsweep --only $Id --yes --elevate"
|
|
101
|
+
Add-ReportStep -Section $Id -Title $sec.Title -Status 'skipped' -Note 'needs Administrator'
|
|
102
|
+
Write-MachineProgress -Section $Id -Stage 'end' -Status 'skipped'
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if ($sec.Dev -and $ws.Developer -eq $false -and $Id -in @(4, 17, 20)) {
|
|
106
|
+
Write-Info "developer mode is off - section $Id (developer-only data) skipped."
|
|
107
|
+
Add-ReportStep -Section $Id -Title $sec.Title -Status 'skipped' -Note 'developer mode off'
|
|
108
|
+
Write-MachineProgress -Section $Id -Stage 'end' -Status 'skipped'
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
$ws.SectionFreed = [long]0
|
|
112
|
+
$before = Get-SystemDriveFree
|
|
113
|
+
$status = 'ran'
|
|
114
|
+
if ($ws.DryRun) { $status = 'dry-run' }
|
|
115
|
+
try {
|
|
116
|
+
& $sec.Fn
|
|
117
|
+
} catch {
|
|
118
|
+
$status = 'failed'
|
|
119
|
+
Write-Err "section $Id failed: $($_.Exception.Message)"
|
|
120
|
+
Write-LogLine ("section $Id error: " + $_.ScriptStackTrace)
|
|
121
|
+
$ws.ExitCode = $Script:WS_EXIT_FAIL
|
|
122
|
+
}
|
|
123
|
+
$after = Get-SystemDriveFree
|
|
124
|
+
$delta = $after - $before
|
|
125
|
+
if ($delta -lt 0) { $delta = 0 }
|
|
126
|
+
$freed = $ws.SectionFreed
|
|
127
|
+
if ($ws.DryRun) {
|
|
128
|
+
if ($freed -gt 0) { Write-UiLine (" [dry-run] this section would reclaim about {0}" -f (Format-Bytes $freed)) 'Magenta' }
|
|
129
|
+
} elseif ($freed -gt 0 -or $delta -gt 0) {
|
|
130
|
+
Write-UiLine (" {0} this section reclaimed {1} (system drive gained {2}) running total: {3}" -f $ws.Glyph.ok, (Format-Bytes $freed), (Format-Bytes $delta), (Format-Bytes $ws.TotalFreed)) 'Green'
|
|
131
|
+
} else {
|
|
132
|
+
Write-Note 'no measurable change in this section'
|
|
133
|
+
}
|
|
134
|
+
Add-ReportStep -Section $Id -Title $sec.Title -Status $status -Freed $freed
|
|
135
|
+
Write-MachineProgress -Section $Id -Stage 'end' -Status $status -Freed $freed
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function Invoke-BatchMode {
|
|
139
|
+
<# .SYNOPSIS --all / --only / --profile #>
|
|
140
|
+
$ws = $Script:WS
|
|
141
|
+
$ids = @(Get-RequestedSections)
|
|
142
|
+
if ($ids.Count -eq 0) { Write-Err 'no sections to run - check --only / --profile against windowsweep --list'; $ws.ExitCode = $Script:WS_EXIT_USAGE; return }
|
|
143
|
+
Write-Banner
|
|
144
|
+
Show-RunHeader
|
|
145
|
+
Write-Info "running sections: $($ids -join ',')"
|
|
146
|
+
foreach ($id in $ids) { Invoke-SectionById -Id $id }
|
|
147
|
+
Show-SessionSummary
|
|
148
|
+
if ($ws.Refusals.Count -gt 0 -and $ws.Mode -eq 'only' -and $ws.ExitCode -eq 0) { $ws.ExitCode = $Script:WS_EXIT_REFUSED }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function Show-RunHeader {
|
|
152
|
+
$ws = $Script:WS
|
|
153
|
+
$strategy = "prune files idle $($ws.Days)+ days (temp: $($ws.TempDays)+ days)"
|
|
154
|
+
if ($ws.PurgeAll) { $strategy = 'FULL PURGE of cache targets' }
|
|
155
|
+
$dev = 'off'
|
|
156
|
+
if ($ws.Developer) { $dev = "on ($($ws.DeveloperSource))" }
|
|
157
|
+
$adm = 'no'
|
|
158
|
+
if ($ws.IsAdmin) { $adm = 'yes' }
|
|
159
|
+
Write-Info "mode: $($ws.Mode) strategy: $strategy developer: $dev elevated: $adm auto-yes: $($ws.Yes)"
|
|
160
|
+
if ($ws.DryRun) { Write-UiLine ' DRY-RUN: nothing will be deleted; sizes below are estimates of what a real run would remove.' 'Magenta' }
|
|
161
|
+
Write-Info "log: $($ws.LogFile)"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function Invoke-ScanMode {
|
|
165
|
+
<# .SYNOPSIS --scan: health report + every target's size + the read-only personal scanners. Deletes nothing. #>
|
|
166
|
+
$ws = $Script:WS
|
|
167
|
+
Write-Banner
|
|
168
|
+
Write-Info 'scan only - nothing is deleted; a log and a report are still written'
|
|
169
|
+
if (Get-Command Invoke-Section00 -ErrorAction SilentlyContinue) { Invoke-Section00 }
|
|
170
|
+
Write-Box 'Targets on disk' 'What each section can reach, and how much it currently holds'
|
|
171
|
+
$null = Show-ScanTable
|
|
172
|
+
if (Get-Command Show-PersonalScan -ErrorAction SilentlyContinue) { Show-PersonalScan }
|
|
173
|
+
Add-ReportStep -Section -1 -Title 'Scan' -Status 'ran'
|
|
174
|
+
Show-SessionSummary
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function Show-SessionSummary {
|
|
178
|
+
$ws = $Script:WS
|
|
179
|
+
Write-Box 'Session summary'
|
|
180
|
+
if ($ws.DryRun) {
|
|
181
|
+
Write-Kv 'Mode:' 'DRY-RUN - nothing was deleted'
|
|
182
|
+
Write-Kv 'Would reclaim (est.):' (Format-Bytes $ws.TotalEstimated)
|
|
183
|
+
} else {
|
|
184
|
+
Write-Kv 'Reclaimed:' (Format-Bytes $ws.TotalFreed)
|
|
185
|
+
}
|
|
186
|
+
$secs = [int][math]::Round(((Get-Date).Ticks - $ws.Report.startedTicks) / 10000000)
|
|
187
|
+
Write-Kv 'Duration:' (Format-Duration $secs)
|
|
188
|
+
$ran = @($ws.Report.steps | Where-Object { $_.status -in 'ran', 'dry-run' }).Count
|
|
189
|
+
$skipped = @($ws.Report.steps | Where-Object { $_.status -notin 'ran', 'dry-run' }).Count
|
|
190
|
+
# C-091: 'skipped' is every step that did not run, so it folds in REFUSED and FAILED as well as skipped.
|
|
191
|
+
# The label is kept deliberately - the exported report was aligned to this exact wording on purpose, and
|
|
192
|
+
# renaming both costs two surfaces and a stored artefact's vocabulary to buy a word. The fact lives here,
|
|
193
|
+
# beside the slot, so a reader of `9 / 2` can find out which.
|
|
194
|
+
Write-Kv 'Sections run / skipped:' "$ran / $skipped"
|
|
195
|
+
Write-Kv 'Log:' $ws.LogFile
|
|
196
|
+
$rep = Save-Report
|
|
197
|
+
if ($rep) { Write-Kv 'Report (JSON):' $rep } elseif ($ws.NoReport) { Write-Kv 'Report:' '(disabled via --no-report)' }
|
|
198
|
+
Write-UiLine '' 'Gray'
|
|
199
|
+
Show-DriveTable
|
|
200
|
+
if ($ws.Refusals.Count -gt 0) {
|
|
201
|
+
Write-UiLine '' 'Gray'
|
|
202
|
+
Write-Warn 'refused in batch mode:'
|
|
203
|
+
foreach ($r in $ws.Refusals) { Write-Note $r }
|
|
204
|
+
}
|
|
205
|
+
if ($ws.Hints.Count -gt 0) {
|
|
206
|
+
Write-UiLine '' 'Gray'
|
|
207
|
+
Write-Info 'next steps:'
|
|
208
|
+
foreach ($h in ($ws.Hints | Sort-Object -Unique)) { Write-Note $h }
|
|
209
|
+
}
|
|
210
|
+
Write-UiLine '' 'Gray'
|
|
211
|
+
Write-Note "$Script:WS_NAME v$(Get-ToolVersion) by $Script:WS_AUTHOR - $Script:WS_REPO"
|
|
212
|
+
Send-RunNotification
|
|
213
|
+
if ($ws.JsonMode) { Write-JsonSummary }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function Get-JsonSummary {
|
|
217
|
+
<# .SYNOPSIS The --json document (ordered), kept apart from the printer so the self-test can parse it. #>
|
|
218
|
+
$ws = $Script:WS
|
|
219
|
+
return [ordered]@{
|
|
220
|
+
tool = $Script:WS_NAME; version = (Get-ToolVersion); mode = $ws.Mode; dry_run = [bool]$ws.DryRun
|
|
221
|
+
elevated = [bool]$ws.IsAdmin; developer = $ws.Developer
|
|
222
|
+
freed_bytes = [long]$ws.TotalFreed; estimated_bytes = [long]$ws.TotalEstimated
|
|
223
|
+
sections = @($ws.Report.steps | ForEach-Object { [ordered]@{ section = $_.section; status = $_.status; freed_bytes = $_.freed_bytes } })
|
|
224
|
+
# candidates and targets are ALWAYS present, empty when nothing was collected: a caller can rely on the shape.
|
|
225
|
+
candidates = @($ws.Candidates); targets = @($ws.ScanTargets)
|
|
226
|
+
refusals = @($ws.Refusals); excluded = @($ws.Excluded); log_file = $ws.LogFile; report_file = $ws.ReportFile
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function Get-CatalogueJson {
|
|
231
|
+
<# .SYNOPSIS --list --json: the section catalogue, so a front end reads it instead of hard-coding it. #>
|
|
232
|
+
return [ordered]@{
|
|
233
|
+
tool = $Script:WS_NAME; version = (Get-ToolVersion)
|
|
234
|
+
sections = @($Script:WS_SECTIONS | ForEach-Object { [ordered]@{ id = $_.Id; key = $_.Key; title = $_.Title; tier = $_.Tier; admin = [bool]$_.Admin; batch = $_.Batch; dev = [bool]$_.Dev } })
|
|
235
|
+
safe_batch = @($Script:WS_SAFE_BATCH); safe_batch_admin = @($Script:WS_SAFE_BATCH_ADMIN)
|
|
236
|
+
# The same two lists --list-targets prints, so a front end shows the engine's own promise rather than
|
|
237
|
+
# a copy of it that can age. Subtrees is built by Initialize-Safety from this machine's real folders.
|
|
238
|
+
protected = [ordered]@{ subtrees = @($Script:WS_PROTECT.Subtrees); categories = @($Script:WS_PROTECT_CATEGORIES) }
|
|
239
|
+
profiles = ([ordered]@{} + $Script:WS_PROFILES); walkthrough = @($Script:WS_WALKTHROUGH); walkthrough_admin = @($Script:WS_WALKTHROUGH_ADMIN)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function Write-JsonSummary {
|
|
244
|
+
<# .SYNOPSIS --json: the only line written to stdout. #>
|
|
245
|
+
[Console]::Out.WriteLine(((Get-JsonSummary) | ConvertTo-Json -Depth 5 -Compress))
|
|
246
|
+
}
|