vanspod 0.1.2 → 0.1.3
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/README.md +81 -75
- package/bin/register.cjs +11 -11
- package/bin/vanspod.cjs +2 -2
- package/dist/classroomUpdate.js +142 -142
- package/dist/connectionMascot.js +215 -215
- package/dist/installProgress.js +167 -167
- package/dist/lifecyclePage.js +33 -33
- package/dist/modelStatus.js +38 -38
- package/dist/version.js +1 -1
- package/package.json +30 -29
- package/scripts/uninstall.ps1 +133 -133
package/scripts/uninstall.ps1
CHANGED
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
param([string]$Plan = (Join-Path $PSScriptRoot 'uninstall-plan.json'))
|
|
2
|
-
$ErrorActionPreference = 'Stop'
|
|
3
|
-
$utf8 = New-Object System.Text.UTF8Encoding($false)
|
|
4
|
-
$planSource = [System.IO.File]::ReadAllText($Plan, $utf8)
|
|
5
|
-
$toolSource = [System.IO.File]::ReadAllText($PSCommandPath, $utf8)
|
|
6
|
-
$planData = $planSource | ConvertFrom-Json
|
|
7
|
-
$localRoot = if ($env:LOCALAPPDATA) { $env:LOCALAPPDATA } else { $env:USERPROFILE }
|
|
8
|
-
$expectedRoot = [System.IO.Path]::GetFullPath((Join-Path $localRoot 'VansPod'))
|
|
9
|
-
$root = [System.IO.Path]::GetFullPath($planData.root)
|
|
10
|
-
$dataRoot = [System.IO.Path]::GetFullPath($planData.dataRoot)
|
|
11
|
-
if ($planData.version -ne 1 -or $root -ne $expectedRoot -or $dataRoot -ne [System.IO.Path]::GetFullPath((Join-Path $env:USERPROFILE '.vanspod')) -or [System.IO.Path]::GetFullPath($Plan) -ne (Join-Path $root 'uninstall-plan.json')) { throw 'Invalid removal plan' }
|
|
12
|
-
[System.IO.File]::WriteAllText((Join-Path $root 'uninstall-ready'), (@{pid = $PID} | ConvertTo-Json -Compress), $utf8)
|
|
13
|
-
|
|
14
|
-
function Assert-NoLinks([string]$Target, [bool]$InspectChildren = $true) {
|
|
15
|
-
# A regular leaf can still resolve through a junction in any ancestor.
|
|
16
|
-
$ancestor = [System.IO.Path]::GetFullPath($Target)
|
|
17
|
-
while ($ancestor) {
|
|
18
|
-
if ((Test-Path -LiteralPath $ancestor) -and ((Get-Item -LiteralPath $ancestor -Force).Attributes -band [System.IO.FileAttributes]::ReparsePoint)) { throw "拒絕移除連結:$ancestor" }
|
|
19
|
-
$ancestor = [System.IO.Path]::GetDirectoryName($ancestor)
|
|
20
|
-
}
|
|
21
|
-
if (Test-Path -LiteralPath $Target) {
|
|
22
|
-
$item = Get-Item -LiteralPath $Target -Force
|
|
23
|
-
if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw "拒絕移除連結:$Target" }
|
|
24
|
-
if ($InspectChildren -and $item.PSIsContainer) { foreach ($child in Get-ChildItem -LiteralPath $Target -Force) { Assert-NoLinks $child.FullName } }
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function Remove-Owned([string]$Name, [string]$Directory = $root) {
|
|
29
|
-
$target = [System.IO.Path]::GetFullPath((Join-Path $Directory $Name))
|
|
30
|
-
if (-not $target.StartsWith($Directory + [System.IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw 'Removal target escaped VansPod data directory' }
|
|
31
|
-
Assert-NoLinks $target
|
|
32
|
-
if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Recurse -Force }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
$failedTarget = $root
|
|
36
|
-
try {
|
|
37
|
-
# Validate roots and ancestors now, but the worker still owns mutable runtime
|
|
38
|
-
# entries until it exits. Inspect their final tree only after that handoff.
|
|
39
|
-
Assert-NoLinks $root $false
|
|
40
|
-
Assert-NoLinks $dataRoot $false
|
|
41
|
-
foreach ($processId in @($planData.pid, $planData.waitForPid)) {
|
|
42
|
-
if ($processId) {
|
|
43
|
-
for ($attempt = 0; $attempt -lt 300 -and (Get-Process -Id $processId -ErrorAction SilentlyContinue); $attempt++) { Start-Sleep -Milliseconds 100 }
|
|
44
|
-
if (Get-Process -Id $processId -ErrorAction SilentlyContinue) { throw 'VansPod 尚未退出,請結束後重試。' }
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
Assert-NoLinks $root
|
|
48
|
-
Assert-NoLinks $dataRoot
|
|
49
|
-
$currentStateFile = Join-Path $root 'runtime/instance.json'
|
|
50
|
-
if (Test-Path -LiteralPath (Join-Path $root 'runtime')) {
|
|
51
|
-
if (-not (Test-Path -LiteralPath $currentStateFile)) { throw 'VansPod 正在啟動,請從畫面或 vanspod uninstall 重試完整移除。' }
|
|
52
|
-
$currentState = [System.IO.File]::ReadAllText($currentStateFile, $utf8) | ConvertFrom-Json
|
|
53
|
-
if ($currentState.id -ne $planData.instanceId) { throw 'VansPod 已有新的執行紀錄,請從畫面或 vanspod uninstall 重試完整移除,以先恢復客戶端設定。' }
|
|
54
|
-
if (Get-Process -Id $currentState.pid -ErrorAction SilentlyContinue) { throw 'VansPod 已重新啟動,請從畫面或 vanspod uninstall 重試完整移除。' }
|
|
55
|
-
}
|
|
56
|
-
foreach ($name in @('connection.json', 'settings.json', 'vscode-route-journal.json')) { $failedTarget = Join-Path $dataRoot $name; Remove-Owned $name $dataRoot }
|
|
57
|
-
$legacyRoot = [System.IO.Path]::GetFullPath((Join-Path $localRoot 'classroom-app'))
|
|
58
|
-
$failedTarget = Join-Path $legacyRoot 'connection.json'
|
|
59
|
-
if (Test-Path -LiteralPath $legacyRoot) {
|
|
60
|
-
if ((Get-Item -LiteralPath $legacyRoot -Force).Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw '拒絕清理舊憑證資料夾連結。' }
|
|
61
|
-
Remove-Owned 'connection.json' $legacyRoot
|
|
62
|
-
if (-not @(Get-ChildItem -LiteralPath $legacyRoot -Force).Count) { Remove-Item -LiteralPath $legacyRoot -Force }
|
|
63
|
-
}
|
|
64
|
-
if (Test-Path -LiteralPath $dataRoot) {
|
|
65
|
-
foreach ($file in Get-ChildItem -LiteralPath $dataRoot -File -Force) {
|
|
66
|
-
if ($file.Name -match '^(connection\.json|settings\.json|vscode-route-journal\.json)\..*tmp$') { $failedTarget = $file.FullName; Remove-Owned $file.Name $dataRoot }
|
|
67
|
-
}
|
|
68
|
-
if (-not @(Get-ChildItem -LiteralPath $dataRoot -Force).Count) { Remove-Item -LiteralPath $dataRoot -Force }
|
|
69
|
-
}
|
|
70
|
-
foreach ($name in @('runtime', 'desktop-assets', 'logs', 'installations.lock', 'installations.json.tmp', 'removal-result.json', 'startup-error.json')) {
|
|
71
|
-
$failedTarget = Join-Path $root $name
|
|
72
|
-
Remove-Owned $name
|
|
73
|
-
}
|
|
74
|
-
foreach ($installation in $planData.installations) {
|
|
75
|
-
if ($installation.kind -eq 'exe') {
|
|
76
|
-
$failedTarget = $installation.file
|
|
77
|
-
$target = [System.IO.Path]::GetFullPath($installation.file)
|
|
78
|
-
if ([System.IO.Path]::GetExtension($target) -ne '.exe' -or $installation.sha256 -notmatch '^[a-f0-9]{64}$') { throw 'Invalid executable registration' }
|
|
79
|
-
Assert-NoLinks $target
|
|
80
|
-
if (Test-Path -LiteralPath $target) {
|
|
81
|
-
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
82
|
-
try { $actualHash = [BitConverter]::ToString($sha.ComputeHash([System.IO.File]::ReadAllBytes($target))).Replace('-', '').ToLowerInvariant() }
|
|
83
|
-
finally { $sha.Dispose() }
|
|
84
|
-
if ($actualHash -ne $installation.sha256) { throw '檔案內容已改變,請重新開啟該 VansPod 後重試移除。' }
|
|
85
|
-
Remove-Item -LiteralPath $target -Force
|
|
86
|
-
}
|
|
87
|
-
} elseif ($installation.kind -eq 'npm') {
|
|
88
|
-
$failedTarget = $installation.root
|
|
89
|
-
$prefix = [System.IO.Path]::GetFullPath($installation.prefix)
|
|
90
|
-
$packageRoot = [System.IO.Path]::GetFullPath($installation.root)
|
|
91
|
-
if ($packageRoot -ne (Join-Path $prefix 'node_modules/vanspod')) { throw 'Invalid npm registration' }
|
|
92
|
-
Assert-NoLinks $packageRoot
|
|
93
|
-
if (Test-Path -LiteralPath $packageRoot) {
|
|
94
|
-
$package = [System.IO.File]::ReadAllText((Join-Path $packageRoot 'package.json'), $utf8) | ConvertFrom-Json
|
|
95
|
-
if ($package.name -ne 'vanspod' -or $package.bin.vanspod -ne 'bin/vanspod.cjs') { throw 'Registered package is not VansPod' }
|
|
96
|
-
& $installation.node $installation.npmCli uninstall --global --prefix $prefix vanspod --ignore-scripts 2>&1 | Out-Null
|
|
97
|
-
if ($LASTEXITCODE -ne 0 -or (Test-Path -LiteralPath $packageRoot)) { throw 'npm 移除失敗。' }
|
|
98
|
-
}
|
|
99
|
-
} else { throw 'Unknown installation kind' }
|
|
100
|
-
}
|
|
101
|
-
foreach ($name in @('installations.json', 'uninstall-plan.json', 'uninstall-ready')) { $failedTarget = Join-Path $root $name; Remove-Owned $name }
|
|
102
|
-
$failedTarget = $PSCommandPath
|
|
103
|
-
Remove-Owned 'uninstall-tool.ps1'
|
|
104
|
-
if (-not @(Get-ChildItem -LiteralPath $root -Force).Count) { Remove-Item -LiteralPath $root -Force }
|
|
105
|
-
if (-not $planData.headless) { Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('VansPod 已完整移除。學生專案與獨立工具已保留;未登記的其他 exe 副本請自行刪除。', 'VansPod') | Out-Null }
|
|
106
|
-
} catch {
|
|
107
|
-
# A late cleanup error may have already removed a control file. Re-create
|
|
108
|
-
# the exact plan and tool so partial removal always remains retryable.
|
|
109
|
-
if (-not (Test-Path -LiteralPath $root)) { New-Item -ItemType Directory -Path $root | Out-Null }
|
|
110
|
-
Assert-NoLinks $root
|
|
111
|
-
[System.IO.File]::WriteAllText((Join-Path $root 'uninstall-plan.json'), $planSource, $utf8)
|
|
112
|
-
[System.IO.File]::WriteAllText((Join-Path $root 'uninstall-tool.ps1'), $toolSource, (New-Object System.Text.UTF8Encoding($true)))
|
|
113
|
-
$reason = $_.Exception.Message
|
|
114
|
-
$remaining = @($failedTarget)
|
|
115
|
-
foreach ($installation in $planData.installations) {
|
|
116
|
-
$location = if ($installation.kind -eq 'exe') { $installation.file } else { $installation.root }
|
|
117
|
-
if (Test-Path -LiteralPath $location) { $remaining += $location }
|
|
118
|
-
}
|
|
119
|
-
foreach ($name in @('runtime', 'desktop-assets', 'logs', 'installations.json', 'installations.lock', 'installations.json.tmp', 'uninstall-plan.json', 'uninstall-tool.ps1', 'uninstall-ready', 'startup-error.json', 'removal-result.json')) {
|
|
120
|
-
$location = Join-Path $root $name
|
|
121
|
-
if (Test-Path -LiteralPath $location) { $remaining += $location }
|
|
122
|
-
}
|
|
123
|
-
$legacyFile = Join-Path $localRoot 'classroom-app/connection.json'
|
|
124
|
-
if (Test-Path -LiteralPath $legacyFile) { $remaining += $legacyFile }
|
|
125
|
-
if (Test-Path -LiteralPath $dataRoot) {
|
|
126
|
-
foreach ($file in Get-ChildItem -LiteralPath $dataRoot -File -Force) { if ($file.Name -match '^(connection\.json|settings\.json|vscode-route-journal\.json)(\..*tmp)?$') { $remaining += $file.FullName } }
|
|
127
|
-
}
|
|
128
|
-
$remaining = @($remaining | Select-Object -Unique)
|
|
129
|
-
$message = "移除未完成:$reason`n剩餘項目:`n$($remaining -join "`n")`n請重試完整移除;若 VansPod 已重新啟動,請從畫面或 vanspod uninstall 重試。保留的 uninstall-tool.ps1 僅在服務已結束時可直接執行。"
|
|
130
|
-
[System.IO.File]::WriteAllText((Join-Path $root 'removal-result.json'), (@{ok = $false; message = $message; remaining = $remaining} | ConvertTo-Json), $utf8)
|
|
131
|
-
if (-not $planData.headless) { Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show($message, 'VansPod') | Out-Null }
|
|
132
|
-
exit 1
|
|
133
|
-
}
|
|
1
|
+
param([string]$Plan = (Join-Path $PSScriptRoot 'uninstall-plan.json'))
|
|
2
|
+
$ErrorActionPreference = 'Stop'
|
|
3
|
+
$utf8 = New-Object System.Text.UTF8Encoding($false)
|
|
4
|
+
$planSource = [System.IO.File]::ReadAllText($Plan, $utf8)
|
|
5
|
+
$toolSource = [System.IO.File]::ReadAllText($PSCommandPath, $utf8)
|
|
6
|
+
$planData = $planSource | ConvertFrom-Json
|
|
7
|
+
$localRoot = if ($env:LOCALAPPDATA) { $env:LOCALAPPDATA } else { $env:USERPROFILE }
|
|
8
|
+
$expectedRoot = [System.IO.Path]::GetFullPath((Join-Path $localRoot 'VansPod'))
|
|
9
|
+
$root = [System.IO.Path]::GetFullPath($planData.root)
|
|
10
|
+
$dataRoot = [System.IO.Path]::GetFullPath($planData.dataRoot)
|
|
11
|
+
if ($planData.version -ne 1 -or $root -ne $expectedRoot -or $dataRoot -ne [System.IO.Path]::GetFullPath((Join-Path $env:USERPROFILE '.vanspod')) -or [System.IO.Path]::GetFullPath($Plan) -ne (Join-Path $root 'uninstall-plan.json')) { throw 'Invalid removal plan' }
|
|
12
|
+
[System.IO.File]::WriteAllText((Join-Path $root 'uninstall-ready'), (@{pid = $PID} | ConvertTo-Json -Compress), $utf8)
|
|
13
|
+
|
|
14
|
+
function Assert-NoLinks([string]$Target, [bool]$InspectChildren = $true) {
|
|
15
|
+
# A regular leaf can still resolve through a junction in any ancestor.
|
|
16
|
+
$ancestor = [System.IO.Path]::GetFullPath($Target)
|
|
17
|
+
while ($ancestor) {
|
|
18
|
+
if ((Test-Path -LiteralPath $ancestor) -and ((Get-Item -LiteralPath $ancestor -Force).Attributes -band [System.IO.FileAttributes]::ReparsePoint)) { throw "拒絕移除連結:$ancestor" }
|
|
19
|
+
$ancestor = [System.IO.Path]::GetDirectoryName($ancestor)
|
|
20
|
+
}
|
|
21
|
+
if (Test-Path -LiteralPath $Target) {
|
|
22
|
+
$item = Get-Item -LiteralPath $Target -Force
|
|
23
|
+
if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw "拒絕移除連結:$Target" }
|
|
24
|
+
if ($InspectChildren -and $item.PSIsContainer) { foreach ($child in Get-ChildItem -LiteralPath $Target -Force) { Assert-NoLinks $child.FullName } }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Remove-Owned([string]$Name, [string]$Directory = $root) {
|
|
29
|
+
$target = [System.IO.Path]::GetFullPath((Join-Path $Directory $Name))
|
|
30
|
+
if (-not $target.StartsWith($Directory + [System.IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw 'Removal target escaped VansPod data directory' }
|
|
31
|
+
Assert-NoLinks $target
|
|
32
|
+
if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Recurse -Force }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$failedTarget = $root
|
|
36
|
+
try {
|
|
37
|
+
# Validate roots and ancestors now, but the worker still owns mutable runtime
|
|
38
|
+
# entries until it exits. Inspect their final tree only after that handoff.
|
|
39
|
+
Assert-NoLinks $root $false
|
|
40
|
+
Assert-NoLinks $dataRoot $false
|
|
41
|
+
foreach ($processId in @($planData.pid, $planData.waitForPid)) {
|
|
42
|
+
if ($processId) {
|
|
43
|
+
for ($attempt = 0; $attempt -lt 300 -and (Get-Process -Id $processId -ErrorAction SilentlyContinue); $attempt++) { Start-Sleep -Milliseconds 100 }
|
|
44
|
+
if (Get-Process -Id $processId -ErrorAction SilentlyContinue) { throw 'VansPod 尚未退出,請結束後重試。' }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
Assert-NoLinks $root
|
|
48
|
+
Assert-NoLinks $dataRoot
|
|
49
|
+
$currentStateFile = Join-Path $root 'runtime/instance.json'
|
|
50
|
+
if (Test-Path -LiteralPath (Join-Path $root 'runtime')) {
|
|
51
|
+
if (-not (Test-Path -LiteralPath $currentStateFile)) { throw 'VansPod 正在啟動,請從畫面或 vanspod uninstall 重試完整移除。' }
|
|
52
|
+
$currentState = [System.IO.File]::ReadAllText($currentStateFile, $utf8) | ConvertFrom-Json
|
|
53
|
+
if ($currentState.id -ne $planData.instanceId) { throw 'VansPod 已有新的執行紀錄,請從畫面或 vanspod uninstall 重試完整移除,以先恢復客戶端設定。' }
|
|
54
|
+
if (Get-Process -Id $currentState.pid -ErrorAction SilentlyContinue) { throw 'VansPod 已重新啟動,請從畫面或 vanspod uninstall 重試完整移除。' }
|
|
55
|
+
}
|
|
56
|
+
foreach ($name in @('connection.json', 'settings.json', 'vscode-route-journal.json')) { $failedTarget = Join-Path $dataRoot $name; Remove-Owned $name $dataRoot }
|
|
57
|
+
$legacyRoot = [System.IO.Path]::GetFullPath((Join-Path $localRoot 'classroom-app'))
|
|
58
|
+
$failedTarget = Join-Path $legacyRoot 'connection.json'
|
|
59
|
+
if (Test-Path -LiteralPath $legacyRoot) {
|
|
60
|
+
if ((Get-Item -LiteralPath $legacyRoot -Force).Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw '拒絕清理舊憑證資料夾連結。' }
|
|
61
|
+
Remove-Owned 'connection.json' $legacyRoot
|
|
62
|
+
if (-not @(Get-ChildItem -LiteralPath $legacyRoot -Force).Count) { Remove-Item -LiteralPath $legacyRoot -Force }
|
|
63
|
+
}
|
|
64
|
+
if (Test-Path -LiteralPath $dataRoot) {
|
|
65
|
+
foreach ($file in Get-ChildItem -LiteralPath $dataRoot -File -Force) {
|
|
66
|
+
if ($file.Name -match '^(connection\.json|settings\.json|vscode-route-journal\.json)\..*tmp$') { $failedTarget = $file.FullName; Remove-Owned $file.Name $dataRoot }
|
|
67
|
+
}
|
|
68
|
+
if (-not @(Get-ChildItem -LiteralPath $dataRoot -Force).Count) { Remove-Item -LiteralPath $dataRoot -Force }
|
|
69
|
+
}
|
|
70
|
+
foreach ($name in @('runtime', 'desktop-assets', 'logs', 'installations.lock', 'installations.json.tmp', 'removal-result.json', 'startup-error.json')) {
|
|
71
|
+
$failedTarget = Join-Path $root $name
|
|
72
|
+
Remove-Owned $name
|
|
73
|
+
}
|
|
74
|
+
foreach ($installation in $planData.installations) {
|
|
75
|
+
if ($installation.kind -eq 'exe') {
|
|
76
|
+
$failedTarget = $installation.file
|
|
77
|
+
$target = [System.IO.Path]::GetFullPath($installation.file)
|
|
78
|
+
if ([System.IO.Path]::GetExtension($target) -ne '.exe' -or $installation.sha256 -notmatch '^[a-f0-9]{64}$') { throw 'Invalid executable registration' }
|
|
79
|
+
Assert-NoLinks $target
|
|
80
|
+
if (Test-Path -LiteralPath $target) {
|
|
81
|
+
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
82
|
+
try { $actualHash = [BitConverter]::ToString($sha.ComputeHash([System.IO.File]::ReadAllBytes($target))).Replace('-', '').ToLowerInvariant() }
|
|
83
|
+
finally { $sha.Dispose() }
|
|
84
|
+
if ($actualHash -ne $installation.sha256) { throw '檔案內容已改變,請重新開啟該 VansPod 後重試移除。' }
|
|
85
|
+
Remove-Item -LiteralPath $target -Force
|
|
86
|
+
}
|
|
87
|
+
} elseif ($installation.kind -eq 'npm') {
|
|
88
|
+
$failedTarget = $installation.root
|
|
89
|
+
$prefix = [System.IO.Path]::GetFullPath($installation.prefix)
|
|
90
|
+
$packageRoot = [System.IO.Path]::GetFullPath($installation.root)
|
|
91
|
+
if ($packageRoot -ne (Join-Path $prefix 'node_modules/vanspod')) { throw 'Invalid npm registration' }
|
|
92
|
+
Assert-NoLinks $packageRoot
|
|
93
|
+
if (Test-Path -LiteralPath $packageRoot) {
|
|
94
|
+
$package = [System.IO.File]::ReadAllText((Join-Path $packageRoot 'package.json'), $utf8) | ConvertFrom-Json
|
|
95
|
+
if ($package.name -ne 'vanspod' -or $package.bin.vanspod -ne 'bin/vanspod.cjs') { throw 'Registered package is not VansPod' }
|
|
96
|
+
& $installation.node $installation.npmCli uninstall --global --prefix $prefix vanspod --ignore-scripts 2>&1 | Out-Null
|
|
97
|
+
if ($LASTEXITCODE -ne 0 -or (Test-Path -LiteralPath $packageRoot)) { throw 'npm 移除失敗。' }
|
|
98
|
+
}
|
|
99
|
+
} else { throw 'Unknown installation kind' }
|
|
100
|
+
}
|
|
101
|
+
foreach ($name in @('installations.json', 'uninstall-plan.json', 'uninstall-ready')) { $failedTarget = Join-Path $root $name; Remove-Owned $name }
|
|
102
|
+
$failedTarget = $PSCommandPath
|
|
103
|
+
Remove-Owned 'uninstall-tool.ps1'
|
|
104
|
+
if (-not @(Get-ChildItem -LiteralPath $root -Force).Count) { Remove-Item -LiteralPath $root -Force }
|
|
105
|
+
if (-not $planData.headless) { Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('VansPod 已完整移除。學生專案與獨立工具已保留;未登記的其他 exe 副本請自行刪除。', 'VansPod') | Out-Null }
|
|
106
|
+
} catch {
|
|
107
|
+
# A late cleanup error may have already removed a control file. Re-create
|
|
108
|
+
# the exact plan and tool so partial removal always remains retryable.
|
|
109
|
+
if (-not (Test-Path -LiteralPath $root)) { New-Item -ItemType Directory -Path $root | Out-Null }
|
|
110
|
+
Assert-NoLinks $root
|
|
111
|
+
[System.IO.File]::WriteAllText((Join-Path $root 'uninstall-plan.json'), $planSource, $utf8)
|
|
112
|
+
[System.IO.File]::WriteAllText((Join-Path $root 'uninstall-tool.ps1'), $toolSource, (New-Object System.Text.UTF8Encoding($true)))
|
|
113
|
+
$reason = $_.Exception.Message
|
|
114
|
+
$remaining = @($failedTarget)
|
|
115
|
+
foreach ($installation in $planData.installations) {
|
|
116
|
+
$location = if ($installation.kind -eq 'exe') { $installation.file } else { $installation.root }
|
|
117
|
+
if (Test-Path -LiteralPath $location) { $remaining += $location }
|
|
118
|
+
}
|
|
119
|
+
foreach ($name in @('runtime', 'desktop-assets', 'logs', 'installations.json', 'installations.lock', 'installations.json.tmp', 'uninstall-plan.json', 'uninstall-tool.ps1', 'uninstall-ready', 'startup-error.json', 'removal-result.json')) {
|
|
120
|
+
$location = Join-Path $root $name
|
|
121
|
+
if (Test-Path -LiteralPath $location) { $remaining += $location }
|
|
122
|
+
}
|
|
123
|
+
$legacyFile = Join-Path $localRoot 'classroom-app/connection.json'
|
|
124
|
+
if (Test-Path -LiteralPath $legacyFile) { $remaining += $legacyFile }
|
|
125
|
+
if (Test-Path -LiteralPath $dataRoot) {
|
|
126
|
+
foreach ($file in Get-ChildItem -LiteralPath $dataRoot -File -Force) { if ($file.Name -match '^(connection\.json|settings\.json|vscode-route-journal\.json)(\..*tmp)?$') { $remaining += $file.FullName } }
|
|
127
|
+
}
|
|
128
|
+
$remaining = @($remaining | Select-Object -Unique)
|
|
129
|
+
$message = "移除未完成:$reason`n剩餘項目:`n$($remaining -join "`n")`n請重試完整移除;若 VansPod 已重新啟動,請從畫面或 vanspod uninstall 重試。保留的 uninstall-tool.ps1 僅在服務已結束時可直接執行。"
|
|
130
|
+
[System.IO.File]::WriteAllText((Join-Path $root 'removal-result.json'), (@{ok = $false; message = $message; remaining = $remaining} | ConvertTo-Json), $utf8)
|
|
131
|
+
if (-not $planData.headless) { Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show($message, 'VansPod') | Out-Null }
|
|
132
|
+
exit 1
|
|
133
|
+
}
|