Skip to content

GhostscriptPipedOutput Throws Exception on .NET 7+ / .NET 10 #146

Description

@tinohager

Issue Summary

Upgrading a project using Ghostscript.NET to .NET 6+ (including .NET 8/10) causes GhostscriptPipedOutput to throw a System.Runtime.InteropServices.SEHException originating from System.IO.Pipes.dll.

Root Cause

  1. Strict Native Handle Management in Modern .NET:
    Starting with .NET 6+, System.IO.Pipes.dll manages unmanaged OS handles much more strictly. GhostscriptPipedOutput uses a legacy workaround (_pipe.ClientSafePipeHandle.SetHandleAsInvalid()), which causes invalid handle access and double-close conflicts when the Garbage Collector or Interop layer cleans up after Ghostscript closes the pipe on the unmanaged C side.
  2. Unsupported Thread.Abort():
    The Dispose() cleanup logic relies on _thread.Abort(), which is unsupported in modern .NET (.NET Core 1.0+ / .NET 5+) and causes a PlatformNotSupportedException or unexpected process state.

Fix / Resolution

Refactor GhostscriptPipedOutput to replace the legacy background Thread with an async/await Task-based reader (ReadAsync) and remove the manual SetHandleAsInvalid() hack in favor of safe handle disposal via DisposeLocalCopyOfClientHandle().

Example Code

I have changed the TargetFramework of Ghostscript.NET to netstandard2.1.

using System;
using System.Buffers;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;

namespace Ghostscript.NET
{
    /// <summary>
    /// Thread-safe, high-performance Ghostscript piped output implementation for modern .NET (8/10+).
    /// </summary>
    public sealed class GhostscriptPipedOutput : IDisposable
    {
        private int _disposed; // 0 = false, 1 = true (for thread-safe Interlocked operations)
        private readonly AnonymousPipeServerStream _pipe;
        private readonly MemoryStream _data = new MemoryStream();
        private readonly Task _readTask;

        public GhostscriptPipedOutput()
        {
            _pipe = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable);
            _readTask = Task.Run(ReadPipeAsync);
        }

        /// <summary>
        /// Gets the client pipe handle string required by Ghostscript.
        /// </summary>
        public string ClientHandle => _pipe.GetClientHandleAsString();

        private async Task ReadPipeAsync()
        {
            // Rent buffer from pool to minimize GC overhead
            byte[] sharedBuffer = ArrayPool<byte>.Shared.Rent(8192);
            try
            {
                int readCount;
                Memory<byte> memoryBuffer = sharedBuffer;

                while ((readCount = await _pipe.ReadAsync(memoryBuffer, CancellationToken.None).ConfigureAwait(false)) > 0)
                {
                    lock (_data)
                    {
                        // Utilize high-performance Span-based memory write
                        _data.Write(memoryBuffer[..readCount].Span);
                    }
                }
            }
            catch (ObjectDisposedException)
            {
                // Expected when the pipe is disposed on the C# side
            }
            catch (IOException)
            {
                // Expected when Ghostscript closes its end of the pipe (EOF)
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(sharedBuffer);
            }
        }

        /// <summary>
        /// Asynchronously retrieves the processed PDF bytes (Recommended).
        /// </summary>
        public async Task<byte[]> GetDataAsync()
        {
            CloseClientHandle();
            await _readTask.ConfigureAwait(false);

            lock (_data)
            {
                return _data.ToArray();
            }
        }

        /// <summary>
        /// Synchronous interface for legacy code integration.
        /// </summary>
        public byte[] Data
        {
            get
            {
                CloseClientHandle();
                _readTask.GetAwaiter().GetResult();

                lock (_data)
                {
                    return _data.ToArray();
                }
            }
        }

        private void CloseClientHandle()
        {
            try
            {
                if (!_pipe.IsConnected)
                {
                    _pipe.DisposeLocalCopyOfClientHandle();
                }
            }
            catch
            {
                // Suppress disposal exceptions if handle was already released
            }
        }

        /// <summary>
        /// Releases all resources used by the GhostscriptPipedOutput instance.
        /// </summary>
        public void Dispose()
        {
            if (Interlocked.Exchange(ref _disposed, 1) == 1)
                return;

            CloseClientHandle();
            _pipe.Dispose();

            try
            {
                // Wait for background read task to complete gracefully with a timeout
                _readTask.Wait(TimeSpan.FromSeconds(2));
            }
            catch
            {
                // Suppress task timeout exceptions during disposal
            }

            _data.Dispose();
            GC.SuppressFinalize(this);
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions