优化
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string[]]$Command,
|
||||
[string]$WorkingDirectory = (Get-Location).Path,
|
||||
[string]$Label = 'local check',
|
||||
[ValidateRange(0, 86400)][int]$WaitSeconds = 900,
|
||||
[ValidateRange(1, 86400)][int]$TimeoutSeconds = 1200,
|
||||
[switch]$Status
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (-not $IsWindows -or $PSVersionTable.PSVersion.Major -lt 7) {
|
||||
throw 'local-check requires Windows and PowerShell 7 (pwsh).'
|
||||
}
|
||||
# 所有工作目录使用同一用户级锁;句柄随进程退出关闭,不依赖删除过期锁文件。
|
||||
$stateDirectory = Join-Path $env:LOCALAPPDATA 'Chongchong/LocalChecks'
|
||||
[IO.Directory]::CreateDirectory($stateDirectory) | Out-Null
|
||||
$lockPath = Join-Path $stateDirectory 'heavy.lock'
|
||||
$ownerPath = Join-Path $stateDirectory 'owner.json'
|
||||
$resultPath = Join-Path $stateDirectory 'last-result.json'
|
||||
function Read-Owner {
|
||||
try { return [IO.File]::ReadAllText($ownerPath) } catch { return 'Owner information is being updated.' }
|
||||
}
|
||||
function Open-Slot {
|
||||
try { return [IO.File]::Open($lockPath, 'OpenOrCreate', 'ReadWrite', 'None') }
|
||||
catch [IO.IOException] {
|
||||
if (($_.Exception.HResult -band 0xffff) -notin @(32, 33)) { throw }
|
||||
return $null
|
||||
}
|
||||
}
|
||||
if ($Status) {
|
||||
$probe = Open-Slot
|
||||
if ($probe) { $probe.Dispose(); Write-Host 'IDLE: heavy check slot is free.' }
|
||||
else { Write-Host 'BUSY:'; Write-Host (Read-Owner) }
|
||||
if (Test-Path -LiteralPath $resultPath) { Write-Host 'Last result:'; Write-Host ([IO.File]::ReadAllText($resultPath)) }
|
||||
exit 0
|
||||
}
|
||||
if (-not $Command -or -not $Command[0]) { throw 'Pass -Command as an array, for example @("pnpm", "typecheck").' }
|
||||
$directory = (Resolve-Path -LiteralPath $WorkingDirectory).Path
|
||||
if (-not (Test-Path -LiteralPath $directory -PathType Container)) { throw 'WorkingDirectory must be a directory.' }
|
||||
if ($env:CHONGCHONG_LOCAL_CHECK_OWNER) {
|
||||
# 拒绝嵌套申请,避免外层等待内层、内层等待同一锁。
|
||||
throw 'Already inside local-check. Run the original command directly inside this checked batch.'
|
||||
}
|
||||
$timer = [Diagnostics.Stopwatch]::StartNew()
|
||||
$slot = $null
|
||||
$job = $null
|
||||
$child = $null
|
||||
$gatePath = Join-Path $stateDirectory ("gate-{0}.tmp" -f [guid]::NewGuid())
|
||||
$owner = $null
|
||||
$exitCode = 1
|
||||
try {
|
||||
$nextNotice = 0
|
||||
while (-not ($slot = Open-Slot)) {
|
||||
if ($timer.Elapsed.TotalSeconds -ge $WaitSeconds) {
|
||||
Write-Host "QUEUE TIMEOUT after $WaitSeconds seconds. No check was started."
|
||||
exit 75
|
||||
}
|
||||
if ($timer.Elapsed.TotalSeconds -ge $nextNotice) {
|
||||
Write-Host "WAITING for the shared heavy check slot: $(Read-Owner)"
|
||||
$nextNotice = $timer.Elapsed.TotalSeconds + 15
|
||||
}
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
$owner = [ordered]@{ label = $Label; pid = $PID; directory = $directory; tool = $Command[0]; startedAt = [DateTimeOffset]::Now.ToString('o') }
|
||||
[IO.File]::WriteAllText($ownerPath, ($owner | ConvertTo-Json -Compress))
|
||||
Write-Host "RUNNING [$Label] in $directory (one shared slot; timeout ${TimeoutSeconds}s)"
|
||||
|
||||
# Job Object 在包装器被强制结束时也回收本次子进程树,不影响其他 Node/Java。
|
||||
if (-not ('LocalCheckJob' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class LocalCheckJob {
|
||||
[StructLayout(LayoutKind.Sequential)] struct BasicLimits {
|
||||
public long PerProcessUserTimeLimit, PerJobUserTimeLimit;
|
||||
public uint LimitFlags;
|
||||
public UIntPtr MinimumWorkingSetSize, MaximumWorkingSetSize;
|
||||
public uint ActiveProcessLimit;
|
||||
public UIntPtr Affinity;
|
||||
public uint PriorityClass, SchedulingClass;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] struct IoCounters {
|
||||
public ulong ReadOperationCount, WriteOperationCount, OtherOperationCount;
|
||||
public ulong ReadTransferCount, WriteTransferCount, OtherTransferCount;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] struct ExtendedLimits {
|
||||
public BasicLimits BasicLimitInformation;
|
||||
public IoCounters IoInfo;
|
||||
public UIntPtr ProcessMemoryLimit, JobMemoryLimit, PeakProcessMemoryUsed, PeakJobMemoryUsed;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] struct Accounting {
|
||||
public long UserTime, KernelTime, PeriodUserTime, PeriodKernelTime;
|
||||
public uint PageFaults, TotalProcesses, ActiveProcesses, TerminatedProcesses;
|
||||
}
|
||||
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern IntPtr CreateJobObject(IntPtr attributes, string name);
|
||||
[DllImport("kernel32.dll", SetLastError=true)] static extern bool SetInformationJobObject(IntPtr job, int infoClass, ref ExtendedLimits info, uint length);
|
||||
[DllImport("kernel32.dll", SetLastError=true)] static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
|
||||
[DllImport("kernel32.dll", SetLastError=true)] static extern bool TerminateJobObject(IntPtr job, uint code);
|
||||
[DllImport("kernel32.dll", SetLastError=true)] static extern bool QueryInformationJobObject(IntPtr job, int infoClass, out Accounting info, uint length, IntPtr returnedLength);
|
||||
[DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr handle);
|
||||
public static IntPtr Create() {
|
||||
var job = CreateJobObject(IntPtr.Zero, null);
|
||||
if (job == IntPtr.Zero) throw new Win32Exception();
|
||||
var limits = new ExtendedLimits();
|
||||
limits.BasicLimitInformation.LimitFlags = 0x2000; // KILL_ON_JOB_CLOSE
|
||||
if (!SetInformationJobObject(job, 9, ref limits, (uint)Marshal.SizeOf<ExtendedLimits>())) {
|
||||
int error = Marshal.GetLastWin32Error(); CloseHandle(job); throw new Win32Exception(error);
|
||||
}
|
||||
return job;
|
||||
}
|
||||
public static void Attach(IntPtr job, IntPtr process) {
|
||||
if (!AssignProcessToJobObject(job, process)) throw new Win32Exception();
|
||||
}
|
||||
public static void Stop(IntPtr job) {
|
||||
if (!TerminateJobObject(job, 124)) throw new Win32Exception();
|
||||
// 后代全部退出再归还执行名额,避免退出瞬间下一任务与遗留 worker 重叠。
|
||||
Accounting info;
|
||||
do {
|
||||
if (!QueryInformationJobObject(job, 1, out info, (uint)Marshal.SizeOf<Accounting>(), IntPtr.Zero)) throw new Win32Exception();
|
||||
if (info.ActiveProcesses != 0) System.Threading.Thread.Sleep(50);
|
||||
} while (info.ActiveProcesses != 0);
|
||||
}
|
||||
}
|
||||
'@
|
||||
}
|
||||
$job = [LocalCheckJob]::Create()
|
||||
$payload = @{ command = $Command; gate = $gatePath } | ConvertTo-Json -Compress
|
||||
$payload64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($payload))
|
||||
$bootstrap = @'
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$payload = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:CHONGCHONG_LOCAL_CHECK_PAYLOAD)) | ConvertFrom-Json
|
||||
while (-not (Test-Path -LiteralPath $payload.gate)) { Start-Sleep -Milliseconds 50 }
|
||||
$env:CHONGCHONG_LOCAL_CHECK_PAYLOAD = $null
|
||||
$commandArgs = @($payload.command | Select-Object -Skip 1)
|
||||
$executable = [string]$payload.command[0]
|
||||
if ([IO.Path]::GetFileNameWithoutExtension($executable) -in @('mvn', 'mvnw')) {
|
||||
if (@($commandArgs | Where-Object { $_ -match '^-T|^--threads|^-D(forkCount|reuseForks|parallel)=' }).Count) {
|
||||
throw 'Do not override Maven concurrency in local-check; -T 1 and forkCount=1 are supplied automatically.'
|
||||
}
|
||||
$commandArgs = @('-T', '1', '-DforkCount=1', '-DreuseForks=true') + $commandArgs
|
||||
}
|
||||
if ([IO.Path]::GetFileNameWithoutExtension($executable) -eq 'node' -and $commandArgs -contains '--test') {
|
||||
if (@($commandArgs | Where-Object { $_ -match '^--test-concurrency' }).Count) { throw 'local-check supplies --test-concurrency=2.' }
|
||||
$commandArgs = @('--test-concurrency=2') + $commandArgs
|
||||
}
|
||||
$global:LASTEXITCODE = 0
|
||||
& $executable @commandArgs
|
||||
if (-not $?) { if ($LASTEXITCODE) { exit $LASTEXITCODE }; exit 1 }
|
||||
exit $LASTEXITCODE
|
||||
'@
|
||||
$startInfo = [Diagnostics.ProcessStartInfo]::new()
|
||||
$startInfo.FileName = (Get-Process -Id $PID).Path
|
||||
foreach ($arg in @('-NoLogo', '-NoProfile', '-NonInteractive', '-OutputFormat', 'Text', '-EncodedCommand', [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($bootstrap)))) { $startInfo.ArgumentList.Add($arg) }
|
||||
$startInfo.WorkingDirectory = $directory
|
||||
$startInfo.UseShellExecute = $false
|
||||
$startInfo.Environment['CHONGCHONG_LOCAL_CHECK_OWNER'] = [string]$PID
|
||||
$startInfo.Environment['CHONGCHONG_LOCAL_CHECK_PAYLOAD'] = $payload64
|
||||
$startInfo.Environment['VITEST_MAX_WORKERS'] = '2'
|
||||
$startInfo.Environment['NODE_OPTIONS'] = (($env:NODE_OPTIONS -replace '--max-old-space-size(?:=|\s+)\S+', '') + ' --max-old-space-size=2048').Trim()
|
||||
$startInfo.Environment['JAVA_TOOL_OPTIONS'] = (($env:JAVA_TOOL_OPTIONS -replace '-XX:ActiveProcessorCount=\S+|-Xmx\S+', '') + ' -XX:ActiveProcessorCount=2 -Xmx1536m').Trim()
|
||||
# Maven 启动器可能在 JAVA_TOOL_OPTIONS 之后应用 MAVEN_OPTS,末尾再限定一次。
|
||||
$startInfo.Environment['MAVEN_OPTS'] = (($env:MAVEN_OPTS -replace '-XX:ActiveProcessorCount=\S+|-Xmx\S+', '') + ' -XX:ActiveProcessorCount=2 -Xmx1536m').Trim()
|
||||
$child = [Diagnostics.Process]::Start($startInfo)
|
||||
[LocalCheckJob]::Attach($job, $child.Handle)
|
||||
# 先纳入 Job 再放行,防止短命包装器提前产生未受管理的后代。
|
||||
[IO.File]::WriteAllText($gatePath, '')
|
||||
$running = [Diagnostics.Stopwatch]::StartNew()
|
||||
while (-not $child.WaitForExit(250)) {
|
||||
if ($running.Elapsed.TotalSeconds -ge $TimeoutSeconds) {
|
||||
Write-Host "EXECUTION TIMEOUT [$Label]; stopping only this check's process tree."
|
||||
$exitCode = 124
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($exitCode -ne 124) { $exitCode = $child.ExitCode }
|
||||
} finally {
|
||||
if ($job) {
|
||||
try { [LocalCheckJob]::Stop($job) }
|
||||
finally { [LocalCheckJob]::CloseHandle($job) | Out-Null }
|
||||
}
|
||||
if ($child) {
|
||||
if (-not $child.HasExited) { $child.Kill($true); $child.WaitForExit() }
|
||||
$child.Dispose()
|
||||
}
|
||||
if (Test-Path -LiteralPath $gatePath) { Remove-Item -LiteralPath $gatePath -Force }
|
||||
if ($slot) {
|
||||
try {
|
||||
if ($owner) {
|
||||
$owner['finishedAt'] = [DateTimeOffset]::Now.ToString('o')
|
||||
$owner['exitCode'] = $exitCode
|
||||
[IO.File]::WriteAllText($resultPath, ($owner | ConvertTo-Json -Compress))
|
||||
}
|
||||
} finally { $slot.Dispose() }
|
||||
}
|
||||
}
|
||||
Write-Host "FINISHED [$Label] exit=$exitCode"
|
||||
exit $exitCode
|
||||
Reference in New Issue
Block a user