finally в Install-KompasMcpServer спасает только от штатного throw/return. После принудительного убийства процесса посреди установки каталог .tmp\<guid> оставался навсегда — по ~70 МБ на оборванный запуск, сервер публикуется self-contained. Remove-KompasMcpStaleTemp удаляет подкаталоги .tmp старше суток. Возраст считается как максимум LastWriteTimeUtc по всему поддереву: пока сосед льёт байты в один и тот же server.zip, время записи самого каталога не меняется, и по нему можно было бы снести живую установку. Вызов стоит в начале Install-KompasMcpServer, до раннего выхода по Test-KompasMcpInstall — иначе мусор от оборванных попыток лежал бы до следующего релиза. Если каталога .tmp нет, всё сводится к одному Test-Path. Функция не бросает исключений и не пишет в success stream: запуск сервера важнее мусора, а stdout занят JSON-RPC.
This commit is contained in:
@@ -79,6 +79,46 @@ function Test-KompasMcpInstall {
|
||||
return ($installed.version -eq $Lock.version) -and ($installed.sha256 -eq $Lock.sha256)
|
||||
}
|
||||
|
||||
function Remove-KompasMcpStaleTemp {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $TempRoot,
|
||||
[timespan] $MaxAge = ([timespan]::FromDays(1))
|
||||
)
|
||||
|
||||
# Уборка мусора не имеет права мешать запуску сервера: любая ошибка (каталог
|
||||
# держит соседний процесс, отказано в доступе, гонка на файловой системе)
|
||||
# гасится здесь. Диагностика уходит в verbose stream — stdout занят JSON-RPC.
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $TempRoot)) { return }
|
||||
|
||||
$deadline = (Get-Date).ToUniversalTime() - $MaxAge
|
||||
|
||||
foreach ($dir in (Get-ChildItem -LiteralPath $TempRoot -Directory -Force -ErrorAction SilentlyContinue)) {
|
||||
try {
|
||||
# Время записи самого каталога не обновляется, пока сосед льёт в
|
||||
# него один и тот же server.zip (меняется файл, а не список
|
||||
# содержимого), поэтому за «живость» отвечает максимум по всему
|
||||
# поддереву — иначе можно снести чужую активную установку.
|
||||
$touched = $dir.LastWriteTimeUtc
|
||||
foreach ($item in (Get-ChildItem -LiteralPath $dir.FullName -Recurse -Force -ErrorAction SilentlyContinue)) {
|
||||
if ($item.LastWriteTimeUtc -gt $touched) { $touched = $item.LastWriteTimeUtc }
|
||||
}
|
||||
|
||||
if ($touched -lt $deadline) {
|
||||
Remove-Item -LiteralPath $dir.FullName -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "не удалось убрать временный каталог $($dir.FullName): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "не удалось обойти временный каталог ${TempRoot}: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-KompasMcpServer {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
@@ -87,6 +127,13 @@ function Install-KompasMcpServer {
|
||||
[scriptblock] $Downloader
|
||||
)
|
||||
|
||||
# Подметаем брошенные .tmp\<guid> ДО проверки уже установленной версии: finally
|
||||
# ниже спасает только от штатного throw/return, а после принудительного убийства
|
||||
# процесса (kill, перезагрузка) посреди установки каталог остаётся навсегда —
|
||||
# по ~70 МБ на оборванный запуск, сервер публикуется self-contained. Место в
|
||||
# горячем пути дешёвое: без каталога .tmp всё сводится к одному Test-Path.
|
||||
Remove-KompasMcpStaleTemp -TempRoot (Join-Path $InstallRoot '.tmp')
|
||||
|
||||
$target = Join-Path $InstallRoot $Lock.version
|
||||
if (Test-KompasMcpInstall -Directory $target -Lock $Lock) { return $target }
|
||||
|
||||
@@ -206,4 +253,4 @@ function Resolve-KompasMcpExecutable {
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Read-KompasMcpLock, Test-KompasMcpInstall, Install-KompasMcpServer, Resolve-KompasMcpExecutable
|
||||
Export-ModuleMember -Function Read-KompasMcpLock, Test-KompasMcpInstall, Remove-KompasMcpStaleTemp, Install-KompasMcpServer, Resolve-KompasMcpExecutable
|
||||
|
||||
@@ -23,6 +23,19 @@ BeforeAll {
|
||||
Lock = [pscustomobject]@{ version = $Version; sourceSha = 'deadbeef'; url = "file:///$zip"; sha256 = $sha }
|
||||
}
|
||||
}
|
||||
|
||||
# Имитирует брошенный временный каталог установки: с файлом внутри и заданным
|
||||
# «возрастом» (сдвигаем время записи и каталога, и содержимого).
|
||||
function New-TempWorkDir([string] $TempRoot, [string] $Name, [timespan] $Age) {
|
||||
$dir = Join-Path $TempRoot $Name
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $dir 'server.zip') -Value 'недокачанный архив' -Encoding ascii
|
||||
|
||||
$stamp = (Get-Date).ToUniversalTime() - $Age
|
||||
Get-ChildItem -LiteralPath $dir -Recurse -Force | ForEach-Object { $_.LastWriteTimeUtc = $stamp }
|
||||
(Get-Item -LiteralPath $dir).LastWriteTimeUtc = $stamp
|
||||
return $dir
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Install-KompasMcpServer' {
|
||||
@@ -126,6 +139,117 @@ Describe 'Install-KompasMcpServer' {
|
||||
} } | Should -Throw '*негодная*'
|
||||
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
|
||||
# Без угловых скобок в имени: Pester трактует <...> как подстановку переменной.
|
||||
It 'убирает брошенный временный каталог от оборванной прошлой установки' {
|
||||
$root = New-TempRoot
|
||||
try {
|
||||
$rel = New-FakeRelease -Root $root -Version '1.0.0'
|
||||
$installRoot = Join-Path $root 'install'
|
||||
$tempRoot = Join-Path $installRoot '.tmp'
|
||||
New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
|
||||
$abandoned = New-TempWorkDir -TempRoot $tempRoot -Name 'abandoned' -Age ([timespan]::FromDays(3))
|
||||
|
||||
$dir = Install-KompasMcpServer -Lock $rel.Lock -InstallRoot $installRoot -Downloader {
|
||||
param($Url, $Destination) Copy-Item -LiteralPath $rel.Zip -Destination $Destination -Force
|
||||
}
|
||||
|
||||
Test-Path -LiteralPath $abandoned | Should -BeFalse
|
||||
Test-Path -LiteralPath (Join-Path $dir 'kompas-mcp.exe') | Should -BeTrue
|
||||
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
|
||||
It 'убирает мусор и тогда, когда нужная версия уже установлена (качать нечего)' {
|
||||
$root = New-TempRoot
|
||||
try {
|
||||
$rel = New-FakeRelease -Root $root -Version '1.0.0'
|
||||
$installRoot = Join-Path $root 'install'
|
||||
$downloader = { param($Url, $Destination) Copy-Item -LiteralPath $rel.Zip -Destination $Destination -Force }
|
||||
|
||||
Install-KompasMcpServer -Lock $rel.Lock -InstallRoot $installRoot -Downloader $downloader | Out-Null
|
||||
|
||||
$abandoned = New-TempWorkDir -TempRoot (Join-Path $installRoot '.tmp') -Name 'abandoned' -Age ([timespan]::FromDays(3))
|
||||
Install-KompasMcpServer -Lock $rel.Lock -InstallRoot $installRoot -Downloader $downloader | Out-Null
|
||||
|
||||
Test-Path -LiteralPath $abandoned | Should -BeFalse
|
||||
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Remove-KompasMcpStaleTemp' {
|
||||
It 'удаляет каталог старше порога и не трогает свежий' {
|
||||
$root = New-TempRoot
|
||||
try {
|
||||
$tempRoot = Join-Path $root '.tmp'
|
||||
New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
|
||||
$stale = New-TempWorkDir -TempRoot $tempRoot -Name 'stale' -Age ([timespan]::FromDays(2))
|
||||
$fresh = New-TempWorkDir -TempRoot $tempRoot -Name 'fresh' -Age ([timespan]::FromMinutes(5))
|
||||
|
||||
Remove-KompasMcpStaleTemp -TempRoot $tempRoot -MaxAge ([timespan]::FromDays(1))
|
||||
|
||||
Test-Path -LiteralPath $stale | Should -BeFalse
|
||||
Test-Path -LiteralPath $fresh | Should -BeTrue
|
||||
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
|
||||
It 'не трогает чужую активную установку: каталог старый, но внутрь только что писали' {
|
||||
$root = New-TempRoot
|
||||
try {
|
||||
$tempRoot = Join-Path $root '.tmp'
|
||||
New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
|
||||
# Сосед начал качать давно и всё ещё льёт байты в тот же server.zip:
|
||||
# время записи самого каталога с тех пор не менялось, время файла — да.
|
||||
$active = New-TempWorkDir -TempRoot $tempRoot -Name 'active' -Age ([timespan]::FromDays(2))
|
||||
(Get-Item -LiteralPath (Join-Path $active 'server.zip')).LastWriteTimeUtc = (Get-Date).ToUniversalTime()
|
||||
|
||||
Remove-KompasMcpStaleTemp -TempRoot $tempRoot -MaxAge ([timespan]::FromDays(1))
|
||||
|
||||
Test-Path -LiteralPath $active | Should -BeTrue
|
||||
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
|
||||
It 'молча переживает отсутствующий .tmp' {
|
||||
$root = New-TempRoot
|
||||
try {
|
||||
{ Remove-KompasMcpStaleTemp -TempRoot (Join-Path $root '.tmp') } | Should -Not -Throw
|
||||
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
|
||||
It 'не падает на неудаляемом каталоге и всё равно убирает остальные' {
|
||||
$root = New-TempRoot
|
||||
$stream = $null
|
||||
try {
|
||||
$tempRoot = Join-Path $root '.tmp'
|
||||
New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
|
||||
$locked = New-TempWorkDir -TempRoot $tempRoot -Name 'locked' -Age ([timespan]::FromDays(2))
|
||||
$stale = New-TempWorkDir -TempRoot $tempRoot -Name 'stale' -Age ([timespan]::FromDays(2))
|
||||
|
||||
# Открытый дескриптор не даёт Windows удалить файл — Remove-Item обязан
|
||||
# отработать вхолостую, а не сорвать уборку остального мусора.
|
||||
$stream = [System.IO.File]::Open((Join-Path $locked 'server.zip'), 'Open', 'Read', 'None')
|
||||
|
||||
{ Remove-KompasMcpStaleTemp -TempRoot $tempRoot -MaxAge ([timespan]::FromDays(1)) } | Should -Not -Throw
|
||||
|
||||
Test-Path -LiteralPath (Join-Path $locked 'server.zip') | Should -BeTrue
|
||||
Test-Path -LiteralPath $stale | Should -BeFalse
|
||||
} finally {
|
||||
if ($null -ne $stream) { $stream.Dispose() }
|
||||
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
It 'ничего не пишет в success stream' {
|
||||
$root = New-TempRoot
|
||||
try {
|
||||
$tempRoot = Join-Path $root '.tmp'
|
||||
New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
|
||||
New-TempWorkDir -TempRoot $tempRoot -Name 'stale' -Age ([timespan]::FromDays(2)) | Out-Null
|
||||
|
||||
$out = Remove-KompasMcpStaleTemp -TempRoot $tempRoot -MaxAge ([timespan]::FromDays(1))
|
||||
|
||||
$out | Should -BeNullOrEmpty
|
||||
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Resolve-KompasMcpExecutable' {
|
||||
|
||||
Reference in New Issue
Block a user