Private/Handlers/Invoke-FoNativeHandlers.ps1

function Wait-FoHandlerProcessExit {
    param(
        [System.Diagnostics.Process]$Process,
        [int]$TimeoutSeconds
    )

    if ($TimeoutSeconds -gt 0) {
        $timeoutMs = $TimeoutSeconds * 1000
        if (-not $Process.WaitForExit($timeoutMs)) {
            try { $Process.Kill() } catch { Write-Debug $_.Exception.Message }
            try { $Process.WaitForExit(5000) } catch { Write-Debug $_.Exception.Message }
            return $false
        }
        return $true
    }

    $Process.WaitForExit()
    return $true
}

function Start-FoAsyncStreamCopy {
    param(
        [System.IO.Stream]$From,
        [System.IO.Stream]$To
    )

    $ps = [powershell]::Create()
    [void]$ps.AddScript({
        param([System.IO.Stream]$FromStream, [System.IO.Stream]$ToStream)
        $FromStream.CopyTo($ToStream)
    }).AddArgument($From).AddArgument($To)
    $handle = $ps.BeginInvoke()
    return [PSCustomObject]@{
        PowerShell = $ps
        Handle     = $handle
    }
}

function Wait-FoAsyncStreamCopy {
    param(
        $AsyncCopy,
        [int]$TimeoutSeconds
    )

    if (-not $AsyncCopy) { return $true }

    if ($TimeoutSeconds -le 0) {
        $AsyncCopy.PowerShell.EndInvoke($AsyncCopy.Handle)
        $AsyncCopy.PowerShell.Dispose()
        return $true
    }

    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    while (-not $AsyncCopy.Handle.IsCompleted) {
        if ($sw.Elapsed.TotalSeconds -ge $TimeoutSeconds) {
            try { $AsyncCopy.PowerShell.Stop() } catch { Write-Debug $_.Exception.Message }
            try { $AsyncCopy.PowerShell.Dispose() } catch { Write-Debug $_.Exception.Message }
            return $false
        }
        Start-Sleep -Milliseconds 50
    }

    $AsyncCopy.PowerShell.EndInvoke($AsyncCopy.Handle)
    $AsyncCopy.PowerShell.Dispose()
    return $true
}

function Wait-FoGzipHandlerPipeline {
    param(
        [System.Diagnostics.Process[]]$Processes,
        [int]$TimeoutSeconds
    )

    if ($TimeoutSeconds -le 0) {
        foreach ($proc in $Processes) {
            if ($proc -and -not $proc.HasExited) { $proc.WaitForExit() }
        }
        return $true
    }

    $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
    while ($true) {
        $pending = $false
        foreach ($proc in $Processes) {
            if ($proc -and -not $proc.HasExited) { $pending = $true; break }
        }
        if (-not $pending) { return $true }
        if ((Get-Date) -ge $deadline) {
            foreach ($proc in $Processes) {
                if ($proc -and -not $proc.HasExited) {
                    try { $proc.Kill() } catch { Write-Debug $_.Exception.Message }
                }
            }
            return $false
        }
        Start-Sleep -Milliseconds 50
    }
}

function Invoke-FoDefluffPipe {
    param(
        [string]$InputPath,
        [string]$OutputPath,
        [string]$DefluffExe,
        [int]$TimeoutSeconds = 0
    )

    if (-not $DefluffExe -or -not (Test-Path -LiteralPath $DefluffExe)) { return 1 }

    $psi = New-Object System.Diagnostics.ProcessStartInfo
    if ($DefluffExe -match '\.(cmd|bat)$') {
        $psi.FileName = $env:ComSpec
        $psi.Arguments = '/c ' + (Format-FoProcessArgument $DefluffExe)
        $workDir = $env:SystemRoot
    }
    else {
        $psi.FileName = $DefluffExe
        $workDir = Split-Path -Parent $DefluffExe
    }
    $psi.WorkingDirectory = $workDir
    $psi.UseShellExecute = $false
    $psi.RedirectStandardInput = $true
    $psi.RedirectStandardOutput = $true
    $psi.RedirectStandardError = $true
    $psi.CreateNoWindow = $true

    $p = [System.Diagnostics.Process]::Start($psi)
    try {
        if ($TimeoutSeconds -gt 0) {
            $inputStream = [System.IO.File]::OpenRead($InputPath)
            try {
                $stdinCopy = Start-FoAsyncStreamCopy -From $inputStream -To $p.StandardInput.BaseStream
                if (-not (Wait-FoAsyncStreamCopy -AsyncCopy $stdinCopy -TimeoutSeconds $TimeoutSeconds)) {
                    try { $p.StandardInput.Close() } catch { Write-Debug $_.Exception.Message }
                    if (-not $p.HasExited) { try { $p.Kill() } catch { Write-Debug $_.Exception.Message } }
                    if (Test-Path -LiteralPath $OutputPath) {
                        Remove-Item -LiteralPath $OutputPath -Force -ErrorAction SilentlyContinue
                    }
                    return -1
                }
            }
            finally {
                $inputStream.Dispose()
            }
            $p.StandardInput.Close()
        }
        else {
            $inputStream = [System.IO.File]::OpenRead($InputPath)
            try {
                $inputStream.CopyTo($p.StandardInput.BaseStream)
            }
            finally {
                $inputStream.Dispose()
            }
            $p.StandardInput.Close()
        }

        $outputStream = [System.IO.File]::Create($OutputPath)
        try {
            if ($TimeoutSeconds -gt 0) {
                $stdoutCopy = Start-FoAsyncStreamCopy -From $p.StandardOutput.BaseStream -To $outputStream
                if (-not (Wait-FoGzipHandlerPipeline -Processes @($p) -TimeoutSeconds $TimeoutSeconds)) {
                    if (Test-Path -LiteralPath $OutputPath) {
                        Remove-Item -LiteralPath $OutputPath -Force -ErrorAction SilentlyContinue
                    }
                    return -1
                }
                if (-not (Wait-FoAsyncStreamCopy -AsyncCopy $stdoutCopy -TimeoutSeconds 5)) {
                    if (Test-Path -LiteralPath $OutputPath) {
                        Remove-Item -LiteralPath $OutputPath -Force -ErrorAction SilentlyContinue
                    }
                    return -1
                }
            }
            else {
                $p.StandardOutput.BaseStream.CopyTo($outputStream)
                if (-not (Wait-FoHandlerProcessExit -Process $p -TimeoutSeconds 0)) {
                    return -1
                }
            }
        }
        finally {
            $outputStream.Dispose()
        }
        $p.StandardOutput.Close()
        $null = $p.StandardError.ReadToEnd()

        if ($p.ExitCode -ne 0) { return $p.ExitCode }
        if (-not (Test-Path -LiteralPath $OutputPath)) { return 1 }
        return 0
    }
    finally {
        if ($p -and -not $p.HasExited) { $p.Kill() }
        if ($p) { $p.Dispose() }
    }
}

function Invoke-FoGzipRecompress {
    param(
        [string]$InputPath,
        [string]$OutputPath,
        [string]$GzipExe,
        [int]$TimeoutSeconds = 0
    )

    $tempRaw = Join-Path ([System.IO.Path]::GetTempPath()) ('fo_gzip_{0}.bin' -f ([guid]::NewGuid().ToString('N')))
    $dp = $null
    $cp = $null

    try {
        $decomp = New-Object System.Diagnostics.ProcessStartInfo
        $decomp.FileName = $GzipExe
        $decomp.Arguments = '-cd ' + (Format-FoProcessArgument $InputPath)
        $decomp.UseShellExecute = $false
        $decomp.RedirectStandardOutput = $true
        $decomp.RedirectStandardError = $true
        $decomp.CreateNoWindow = $true
        $dp = [System.Diagnostics.Process]::Start($decomp)

        $rawStream = [System.IO.File]::Create($tempRaw)
        try {
            if ($TimeoutSeconds -gt 0) {
                $decompCopy = Start-FoAsyncStreamCopy -From $dp.StandardOutput.BaseStream -To $rawStream
                if (-not (Wait-FoGzipHandlerPipeline -Processes @($dp) -TimeoutSeconds $TimeoutSeconds)) {
                    return -1
                }
                if (-not (Wait-FoAsyncStreamCopy -AsyncCopy $decompCopy -TimeoutSeconds 5)) {
                    return -1
                }
            }
            else {
                $dp.StandardOutput.BaseStream.CopyTo($rawStream)
                $dp.WaitForExit()
            }
        }
        finally {
            $rawStream.Dispose()
            $dp.StandardOutput.Close()
        }

        $null = $dp.StandardError.ReadToEnd()
        if ($dp.ExitCode -ne 0) { return $dp.ExitCode }

        $comp = New-Object System.Diagnostics.ProcessStartInfo
        $comp.FileName = $GzipExe
        $comp.Arguments = '-12 -f'
        $comp.UseShellExecute = $false
        $comp.RedirectStandardInput = $true
        $comp.RedirectStandardOutput = $true
        $comp.RedirectStandardError = $true
        $comp.CreateNoWindow = $true
        $cp = [System.Diagnostics.Process]::Start($comp)

        $inStream = [System.IO.File]::OpenRead($tempRaw)
        $outStream = [System.IO.File]::Create($OutputPath)
        try {
            if ($TimeoutSeconds -gt 0) {
                $stdinCopy = Start-FoAsyncStreamCopy -From $inStream -To $cp.StandardInput.BaseStream
                if (-not (Wait-FoAsyncStreamCopy -AsyncCopy $stdinCopy -TimeoutSeconds $TimeoutSeconds)) {
                    return -1
                }
                $cp.StandardInput.Close()
                $stdoutCopy = Start-FoAsyncStreamCopy -From $cp.StandardOutput.BaseStream -To $outStream
                if (-not (Wait-FoGzipHandlerPipeline -Processes @($cp) -TimeoutSeconds $TimeoutSeconds)) {
                    return -1
                }
                if (-not (Wait-FoAsyncStreamCopy -AsyncCopy $stdoutCopy -TimeoutSeconds 5)) {
                    return -1
                }
            }
            else {
                $inStream.CopyTo($cp.StandardInput.BaseStream)
                $cp.StandardInput.Close()
                $cp.StandardOutput.BaseStream.CopyTo($outStream)
                $cp.WaitForExit()
            }
        }
        finally {
            $inStream.Dispose()
            $outStream.Dispose()
            $cp.StandardOutput.Close()
        }

        $null = $cp.StandardError.ReadToEnd()
        if ($cp.ExitCode -ne 0) { return $cp.ExitCode }
        return 0
    }
    finally {
        foreach ($proc in @($dp, $cp)) {
            if ($proc -and -not $proc.HasExited) {
                try { $proc.Kill() } catch { Write-Debug $_.Exception.Message }
            }
            if ($proc) { $proc.Dispose() }
        }
        if (Test-Path -LiteralPath $tempRaw) {
            Remove-Item -LiteralPath $tempRaw -Force -ErrorAction SilentlyContinue
        }
    }
}

function Invoke-FoJsMinPipe {
    param(
        [string]$InputPath,
        [string]$OutputPath,
        [string]$JsMinExe,
        [int]$TimeoutSeconds = 0
    )

    $psi = New-Object System.Diagnostics.ProcessStartInfo
    if ($JsMinExe -match '\.(cmd|bat)$') {
        $psi.FileName = $env:ComSpec
        $psi.Arguments = '/c ' + (Format-FoProcessArgument $JsMinExe)
    }
    else {
        $psi.FileName = $JsMinExe
    }
    $psi.UseShellExecute = $false
    $psi.RedirectStandardInput = $true
    $psi.RedirectStandardOutput = $true
    $psi.CreateNoWindow = $true
    $p = [System.Diagnostics.Process]::Start($psi)
    try {
        $text = [System.IO.File]::ReadAllText($InputPath)
        $p.StandardInput.Write($text)
        $p.StandardInput.Close()
        if (-not (Wait-FoHandlerProcessExit -Process $p -TimeoutSeconds $TimeoutSeconds)) {
            if (Test-Path -LiteralPath $OutputPath) {
                Remove-Item -LiteralPath $OutputPath -Force -ErrorAction SilentlyContinue
            }
            return -1
        }
        $out = $p.StandardOutput.ReadToEnd()
        if ($p.ExitCode -ne 0) { return $p.ExitCode }
        [System.IO.File]::WriteAllText($OutputPath, $out)
        return 0
    }
    finally {
        if (-not $p.HasExited) { $p.Kill() }
        $p.Dispose()
    }
}

function Invoke-FoSqliteProcess {
    param(
        [string]$SqliteExe,
        [string]$Arguments,
        [int]$TimeoutSeconds = 0,
        [switch]$CaptureStdout
    )

    $psi = New-Object System.Diagnostics.ProcessStartInfo
    $psi.FileName = $SqliteExe
    $psi.Arguments = $Arguments
    $psi.UseShellExecute = $false
    $psi.RedirectStandardError = $true
    $psi.CreateNoWindow = $true
    if ($CaptureStdout) {
        $psi.RedirectStandardOutput = $true
    }

    $p = [System.Diagnostics.Process]::Start($psi)
    try {
        $stdout = $null
        if ($CaptureStdout) {
            $stdout = $p.StandardOutput.ReadToEnd()
        }
        $null = $p.StandardError.ReadToEnd()
        if (-not (Wait-FoHandlerProcessExit -Process $p -TimeoutSeconds $TimeoutSeconds)) {
            return @{ ExitCode = -1; Output = $null }
        }
        return @{ ExitCode = $p.ExitCode; Output = $stdout }
    }
    finally {
        if ($p -and -not $p.HasExited) { $p.Kill() }
        if ($p) { $p.Dispose() }
    }
}

function Invoke-FoSqliteOptimize {
    param(
        [string]$InputPath,
        [string]$OutputPath,
        [string]$SqliteExe,
        [int]$TimeoutSeconds = 0
    )

    $tempDir = [System.IO.Path]::GetTempPath()
    $sqlFile = Join-Path $tempDir ("FileOptimizer_sqlite_{0}.sql" -f (Get-Random -Maximum 999999))
    if (Test-Path -LiteralPath $OutputPath) { Remove-Item -LiteralPath $OutputPath -Force }

    try {
        Set-Content -LiteralPath $sqlFile -Value 'PRAGMA optimize(0xfffe);' -Encoding UTF8
        $dumpArgs = '{0} .dump' -f (Format-FoProcessArgument $InputPath)
        $dump = Invoke-FoSqliteProcess -SqliteExe $SqliteExe -Arguments $dumpArgs -TimeoutSeconds $TimeoutSeconds -CaptureStdout
        if ($dump.ExitCode -eq -1) { return -1 }
        if ($dump.ExitCode -ne 0) { return $dump.ExitCode }
        Add-Content -LiteralPath $sqlFile -Value $dump.Output -Encoding UTF8
        $readCmd = '.read {0}' -f (Format-FoProcessArgument $sqlFile)
        $readArgs = '{0} {1}' -f (Format-FoProcessArgument $OutputPath), (Format-FoProcessArgument $readCmd)
        $read = Invoke-FoSqliteProcess -SqliteExe $SqliteExe -Arguments $readArgs -TimeoutSeconds $TimeoutSeconds
        if ($read.ExitCode -eq -1) {
            if (Test-Path -LiteralPath $OutputPath) { Remove-Item -LiteralPath $OutputPath -Force }
            return -1
        }
        if ($read.ExitCode -ne 0) {
            if (Test-Path -LiteralPath $OutputPath) { Remove-Item -LiteralPath $OutputPath -Force }
            return $read.ExitCode
        }
        return 0
    }
    finally {
        if (Test-Path -LiteralPath $sqlFile) { Remove-Item -LiteralPath $sqlFile -Force }
    }
}