Private/Invoke-FoPlugin.ps1
|
function Test-FoDisablePluginMaskMatch { param( [string]$Mask, [string]$Haystack ) if ([string]::IsNullOrWhiteSpace($Mask)) { return $false } if ([string]::IsNullOrWhiteSpace($Haystack)) { return $false } $hay = $Haystack.ToUpperInvariant() foreach ($token in ($Mask.Split(',') | ForEach-Object { $_.Trim().ToUpperInvariant() } | Where-Object { $_ })) { if ($hay.Contains($token)) { return $true } } return $false } function Invoke-FoPlugin { [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)] $Step, [Parameter(Mandatory)] [string]$InputFile, [hashtable]$Settings, [string]$PluginPath, [string]$SearchMode ) $tempDir = if ($Settings.TempDirectory) { $Settings.TempDirectory } else { [System.IO.Path]::GetTempPath() } if ($Settings.TempDirectory -and -not (Test-Path -LiteralPath $tempDir)) { New-Item -ItemType Directory -Path $tempDir -Force | Out-Null } $rand = [guid]::NewGuid().ToString('N').Substring(0, 8) $baseName = [System.IO.Path]::GetFileName($InputFile) $tmpIn = Join-Path $tempDir "FileOptimizer_Input_${rand}_$baseName" $tmpOut = Join-Path $tempDir "FileOptimizer_Output_${rand}_$baseName" $sizeBefore = (Get-Item -LiteralPath $InputFile).Length $sw = [System.Diagnostics.Stopwatch]::StartNew() if ($sizeBefore -eq 0) { $sw.Stop() return @{ ExitCode = 0 Skipped = $true Reason = 'ZeroByte' Accepted = $false SizeBefore = 0 SizeAfter = 0 DurationMs = $sw.ElapsedMilliseconds } } if (-not $Settings.Debug) { foreach ($t in @($tmpIn, $tmpOut)) { if (Test-Path -LiteralPath $t) { Remove-Item -LiteralPath $t -Force } } } $stepArgs = $Step.Arguments if ($null -eq $stepArgs) { $stepArgs = '' } $usesTmpOut = $stepArgs -and ($stepArgs -match '%TMPOUTPUTFILE%') $usesTmpInOnly = $stepArgs -and ($stepArgs -match '%TMPINPUTFILE%') -and -not $usesTmpOut -and ($stepArgs -notmatch '%OUTPUTFILE%') $usesInPlace = $Step.Mode -eq 'InPlace' if (-not $usesInPlace -and $stepArgs -and ($stepArgs -match '%INPUTFILE%') -and ($stepArgs -notmatch '%TMPOUTPUTFILE%') -and ($stepArgs -notmatch '%OUTPUTFILE%') -and ($stepArgs -notmatch '%TMPINPUTFILE%')) { $usesInPlace = $true } $inplaceBackup = $null $sizeAfter = $sizeBefore $exitCode = 0 $exitOk = $false $reason = $null try { if ($usesInPlace) { $inplaceBackup = Join-Path $tempDir "FileOptimizer_inplacebak_${rand}_$baseName" Copy-Item -LiteralPath $InputFile -Destination $inplaceBackup -Force } if ($usesTmpInOnly -or ($Step.Handler -and $Step.Mode -eq 'TempInput')) { Copy-Item -LiteralPath $InputFile -Destination $tmpIn -Force } foreach ($requiredExe in (Get-FoStepRequiredExecutables -Step $Step)) { $toolCheck = Resolve-FoPluginExecutable -Name $requiredExe -SearchMode $SearchMode -PluginPath $PluginPath if (-not $toolCheck.Found) { return @{ ExitCode = 1 Skipped = $true Reason = 'ToolMissing' Tool = $requiredExe Accepted = $false SizeBefore = $sizeBefore SizeAfter = $sizeBefore DurationMs = $sw.ElapsedMilliseconds } } } if ($Step.Handler) { if (Test-FoDisablePluginMaskMatch -Mask $Settings.DisablePluginMask -Haystack $Step.Handler) { return @{ ExitCode = 0 Skipped = $true SizeBefore = $sizeBefore SizeAfter = $sizeBefore } } $handlerTimeout = 0 if ($null -ne $Settings.PluginTimeoutSeconds) { $handlerTimeout = [Math]::Max(0, [int]$Settings.PluginTimeoutSeconds) } $handlerExit = Invoke-FoNativeHandler -HandlerName $Step.Handler -InputPath $InputFile -OutputPath $tmpOut ` -SearchMode $SearchMode -PluginPath $PluginPath -TimeoutSeconds $handlerTimeout if ($null -eq $handlerExit) { Write-Warning "Unknown handler '$($Step.Handler)' in step '$($Step.Name)'; treating as failure." $exitCode = 1 } else { $exitCode = $handlerExit } if ($exitCode -eq -1) { # Timeout should not early-return: we still need in-place rollback + temp cleanup. $reason = 'Timeout' } } else { $argTemplate = $stepArgs $argTemplate = $argTemplate.Replace('%INPUTFILE%', (Format-FoProcessArgument $InputFile)) $argTemplate = $argTemplate.Replace('%TMPINPUTFILE%', (Format-FoProcessArgument $tmpIn)) $argTemplate = $argTemplate.Replace('%TMPOUTPUTFILE%', (Format-FoProcessArgument $tmpOut)) $argTemplate = $argTemplate.Replace('%OUTPUTFILE%', '""') $maskHaystack = ($Step.Executable + ' ' + $argTemplate).Trim() if (Test-FoDisablePluginMaskMatch -Mask $Settings.DisablePluginMask -Haystack $maskHaystack) { return @{ ExitCode = 0 Skipped = $true SizeBefore = $sizeBefore SizeAfter = $sizeBefore } } $resolved = Resolve-FoPluginExecutable -Name $Step.Executable -SearchMode $SearchMode -PluginPath $PluginPath $exePath = $resolved.Path $workDir = if ($resolved.Source -eq 'Portable' -and $PluginPath) { $PluginPath } else { Split-Path -Parent $exePath } $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $exePath $psi.Arguments = $argTemplate $psi.WorkingDirectory = $workDir $psi.UseShellExecute = $false $psi.RedirectStandardError = $true $psi.CreateNoWindow = $true $p = $null try { $p = [System.Diagnostics.Process]::Start($psi) } catch { $sw.Stop() return @{ ExitCode = 1 Skipped = $false Accepted = $false Reason = 'ProcessStartFailed' SizeBefore = $sizeBefore SizeAfter = $sizeBefore DurationMs = $sw.ElapsedMilliseconds } } # Capture stderr asynchronously to avoid process deadlocks. # Note: we still read stderr into memory, but we will truncate it # to a configurable maximum before using/logging it. $stderrTask = $p.StandardError.ReadToEndAsync() $timeoutSec = 0 if ($null -ne $Settings.PluginTimeoutSeconds) { $timeoutSec = [Math]::Max(0, [int]$Settings.PluginTimeoutSeconds) } $timedOut = $false if ($timeoutSec -gt 0) { $timeoutMs = $timeoutSec * 1000 if (-not $p.WaitForExit($timeoutMs)) { $timedOut = $true try { $p.Kill() } catch { Write-Debug $_.Exception.Message } try { $p.WaitForExit(5000) } catch { Write-Debug $_.Exception.Message } } } else { $p.WaitForExit() } $stderr = $null try { $stderr = $stderrTask.GetAwaiter().GetResult() } catch { Write-Debug $_.Exception.Message } # Truncate stderr for safety (especially when LogLevel is high). $maxStderrBytes = 1048576 if ($null -ne $Settings.MaxPluginStderrBytes) { $maxStderrBytes = [int64]$Settings.MaxPluginStderrBytes } if ($maxStderrBytes -lt 0) { $maxStderrBytes = 0 } if ($stderr -and $maxStderrBytes -gt 0) { $maxChars = [int64]([Math]::Floor($maxStderrBytes / 2)) if ($stderr.Length -gt $maxChars) { $stderr = $stderr.Substring(0, [int]$maxChars) + '...(truncated)' } } if ($Settings.LogLevel -ge 3 -and $stderr) { Write-Verbose ("Plugin stderr ({0}): {1}" -f $Step.Name, $stderr.Trim()) } $exitCode = if ($timedOut) { -1 } else { $p.ExitCode } $p.Dispose() if ($timedOut) { # Timeout should not early-return: we still need in-place rollback + temp cleanup. $reason = 'Timeout' } } $exitOk = Test-FoStepExitCodeAccepted -Step $Step -ExitCode $exitCode } catch { $sw.Stop() if ($usesInPlace -and $inplaceBackup -and (Test-Path -LiteralPath $inplaceBackup)) { Copy-Item -LiteralPath $inplaceBackup -Destination $InputFile -Force } return @{ ExitCode = 1 Skipped = $false Accepted = $false Reason = 'IOError' SizeBefore = $sizeBefore SizeAfter = $sizeBefore DurationMs = $sw.ElapsedMilliseconds } } $sw.Stop() $accepted = $false if ($exitOk) { if ($usesInPlace) { $sizeAfter = (Get-Item -LiteralPath $InputFile).Length if ($sizeAfter -ge 8 -and $sizeAfter -lt $sizeBefore) { $accepted = $true } } elseif ($usesTmpOut -or $Step.Handler) { if (Test-Path -LiteralPath $tmpOut) { $sizeAfter = (Get-Item -LiteralPath $tmpOut).Length if ($sizeAfter -ge 8 -and $sizeAfter -lt $sizeBefore) { Copy-Item -LiteralPath $tmpOut -Destination $InputFile -Force $accepted = $true } } } elseif ($usesTmpInOnly) { if (Test-Path -LiteralPath $tmpIn) { $sizeAfter = (Get-Item -LiteralPath $tmpIn).Length if ($sizeAfter -ge 8 -and $sizeAfter -lt $sizeBefore) { Copy-Item -LiteralPath $tmpIn -Destination $InputFile -Force $accepted = $true } } } } if ($usesInPlace -and -not $accepted -and $inplaceBackup -and (Test-Path -LiteralPath $inplaceBackup)) { Copy-Item -LiteralPath $inplaceBackup -Destination $InputFile -Force $sizeAfter = $sizeBefore } if (-not $Settings.Debug) { foreach ($t in @($tmpIn, $tmpOut, $inplaceBackup)) { if ($t -and (Test-Path -LiteralPath $t)) { Remove-Item -LiteralPath $t -Force -ErrorAction SilentlyContinue } } } if ($accepted) { $sizeAfter = (Get-Item -LiteralPath $InputFile).Length } else { $sizeAfter = $sizeBefore } return @{ ExitCode = $exitCode Skipped = $false Accepted = $accepted Reason = $reason SizeBefore = $sizeBefore SizeAfter = $sizeAfter DurationMs = $sw.ElapsedMilliseconds } } |