Skip to content

Process isolation library for .NET - protect your application from legacy, unmanaged, or problematic code

License

Notifications You must be signed in to change notification settings

jonnymuir/ProcessSandbox

Repository files navigation

ProcessSandbox

License: MIT .NET

Process isolation library for .NET that protects your application from legacy, unmanaged, or problematic code by running it in separate sandboxed processes.

The Problem

You have legacy code that:

  • Calls into unmanaged COM objects or native DLLs
  • Has resource leaks (memory, handles, GDI objects)
  • Is single-threaded but your app is multi-threaded
  • Occasionally crashes or hangs
  • You can't easily modify or replace

ProcessSandbox solves this by running the problematic code in isolated worker processes with automatic resource monitoring, lifecycle management, and transparent proxying.

Jump straight to the live demo

live demo

Calculator showing it running via the C Com Object

Features

  • 🛡️ Process Isolation: Crashes and leaks don't affect your main application
  • 🔄 Automatic Recycling: Workers recycled based on memory, handles, or call count
  • 🎯 Interface-Based Proxy: Use your interfaces naturally, calls routed transparently
  • High Performance: Named pipes + MessagePack for fast IPC
  • 🔧 32/64-bit Support: Run 32-bit workers from 64-bit apps (COM interop)
  • 🚫 No Orphans: Job Objects ensure workers never leak
  • 📊 Resource Monitoring: Track memory, GDI/USER handles, call counts
  • 🧵 Thread-Safe: Multiple threads can call concurrently
  • 🎚️ Configurable: Extensive options for pool size, limits, timeouts

Quick Start

See the tutorial for an easy to follow example of how ProcessSandbox.Runner works.

See the com tutorial to see an example of how to call a 32 bit com object in an Azure App Service.

Installation

dotnet add package ProcessSandbox.Runner

Basic Usage

using ProcessSandbox;
using Microsoft.Extensions.Logging;

// 1. Define your interface
public interface ILegacyService
{
    string ProcessData(string input);
}

// 2. Create implementation (in separate assembly)
public class LegacyServiceImpl : ILegacyService
{
    public string ProcessData(string input)
    {
        // Your legacy/unmanaged code here
        return LegacyComObject.DoWork(input);
    }
}

// 3. Configuration
var config = new ProcessPoolConfiguration
{
    MaxPoolSize = 5,
    MaxMemoryMB = 1024,
    WorkerAssembly = "MyLegacy.dll",
    WorkerType = "LegacyServiceImpl"
};

// 4. Create a logger factory and configure it to use the Console provider
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());

// 5. Create the proxy factory
using var factory = await ProcessProxyFactory<ILegacyService>.CreateAsync(config, loggerFactory);

// 6. Use the factory to give you a scoped proxy
string overallResult = await factory.UseProxyAsync<string>(async proxy =>
{
    // 7. Use it like any interface
    //    Proxy methods will run in seperate process that has loaded MyLegacy.dll
    //    Proxy object is stateful whist inside this code block
    string result = proxy.ProcessData("test data");
    string result2 = proxy.ProcessData("Some more test data");
    return $"{result} {result2}";
});

Console.WriteLine(overallResult);

// Or if you prefer a lease pattern - you can do that too. Just remember to dispose it otherwise you'll hog the pool
using var lease = await await factory.AcquireLeaseAsync();

string leaseResult = $"{lease.ProcessData("test data")} {lease.ProcessData("Some more test data")}";

Console.WriteLine(leaseResult);

Configuration

var config = new ProcessPoolConfiguration
{
    // Pool sizing
    MinPoolSize = 1,              // Start with 1 worker
    MaxPoolSize = 5,              // Scale up to 5 workers
    
    // Worker process
    DotNetVersion = DotNetVersion.Net48_32Bit,        // Use a 32-bit framework dll for COM
    WorkerAssembly = "MyLegacy.dll",
    WorkerType = "MyLegacy.ServiceImpl",
    
    // Resource limits
    MaxMemoryMB = 1024,           // Recycle at 1GB
    MaxGdiHandles = 10000,        // GDI handle limit
    ProcessRecycleThreshold = 100,// Recycle after 100 calls
    
    // Timeouts
    MethodCallTimeout = TimeSpan.FromSeconds(90),
};

Use Cases

COM Interop (32-bit)

Supply the ComClsid to call directly in to a com object without the need to register it.

Use extra com dependencies if you have dependant com object you need to call into. Look in src/tutorials/ComSandboxDemo for an example of how this works.

Very useful for running in web apps / app services when you don't have access to the registry

var config = new ProcessPoolConfiguration
{
    DotNetVersion = DotNetVersion.Net48_32Bit,
    ComClsid = new Guid("11111111-2222-3333-4444-555555555555"),
    ImplementationAssemblyPath = Path.Combine(AppContext.BaseDirectory, "workers", "SimpleComDelphi32.dll"),
    ExtraComDependencies = [
        new ComDependency
        {
            Clsid = new Guid("B1E9D2C4-8A6F-4E2B-9D3D-1234567890AB"),
            DllPath = Path.Combine(AppContext.BaseDirectory, "workers", "ComEngineInfo32.dll")
        }
    ],
    MaxMemoryMB = 1024,  // well within 32-bit limit
    MaxGdiHandles = 10000,
    NewInstancePerProxy = true // Set to false if you don't want the instance to be cleared down on each run
    
};

Native DLL Calls

var config = new ProcessPoolConfiguration
{
    ProcessRecycleThreshold = 1000,
    MaxProcessLifetime = TimeSpan.FromHours(1)
};

Single-Threaded Legacy Code

var config = new ProcessPoolConfiguration
{
    MaxPoolSize = 10,  // Handle concurrent requests
    MethodCallTimeout = TimeSpan.FromMinutes(5)
};

Architecture

Your App (.NET) → ProcessProxy<T> → [Worker Pool] → Worker Process
                                        ↓              ↓
                                   Named Pipes    Legacy DLL/COM
  • ProcessProxy: Transparent proxy implementing your interface
  • Worker Pool: Manages process lifecycle and resource monitoring
  • Worker Process: Isolated process hosting your implementation
  • IPC: Named pipes with MessagePack serialization

Building

git clone https://github.com/yourusername/ProcessSandbox.git
cd ProcessSandbox
dotnet build
dotnet test

Documentation

Performance

Typical overhead per call:

  • Local calls: < 0.1ms
  • Process proxy: 1-2ms (named pipes + serialization)

Perfect for scenarios where isolation benefits outweigh small latency cost.

Contributing

Contributions welcome! Please read CONTRIBUTING.md first.

License

MIT License - see LICENSE for details.

Roadmap

  • Call com objects directly just via an interface contract. No need for the manifest or an intermediate c# wrapper. This would need to use the COM Binary Interface (the VTable) but skip the COM Infrastructure (the Registry and the Service Control Manager). DONE
  • Telemetry/metrics export

Support

Useful tips when building

If I'm having to make changes to ProcessSandbox and write an end to end solution which uses it from packages, I find it easier to test from a local nuget server rather than having to wait for nuget to publish new versions.

To do this set up a local nuget server e.g.

dotnet nuget add source ~/LocalNuGetFeed --name LocalTestFeed --at-position 1

Then to build all the packages and put them in the local feed you can do the following (from the root of ProcessSanbox - e.g. where the ProcessSandbox.sln is)

dotnet build --configuration Release
dotnet build src/ProcessSandbox.Worker/ProcessSandbox.Worker.csproj -c Release -f net48 -r win-x86
dotnet pack /p:ExcludeProjects="**/ProcessSandbox.Worker.csproj" --configuration Release --no-build --output push-ready-artifacts
dotnet pack src/ProcessSandbox.Worker/ProcessSandbox.Worker.nuspec --configuration Release --output push-ready-artifacts -p:NoWarn=NU5100
cp push-ready-artifacts/* ~/LocalNuGetFeed

Remember to clear the local cache when you want to use them

dotnet nuget locals all --clear

And remember to get rid of from the local package source if you really want to pick them up from nuget.

rm ~/LocalNuGetFeed/*

Deploying the com tutorials to azure

In order to build to SimpleCom object, you need a c compiler. On a mac you can do

brew install mingw-w64

And then to build you need to

i686-w64-mingw32-gcc -shared -static -o publish/workers/net48/win-x86/SimpleCom.dll SimpleCom/SimpleCom.c -lole32 -loleaut32 -lpsapi -Wl,--add-stdcall-alias

Replace jonnymoo_rg_9172 with the name of your resource group and com-sandbox-demo-app with the name of you web app (from src/tutorials/ComSandboxDemo)

dotnet clean
rm -rf publish
rm site.zip
dotnet nuget locals all --clear
dotnet publish AzureSandboxHost/AzureSandboxHost.csproj -c Release -o ./publish
i686-w64-mingw32-gcc -shared -static -o publish/workers/net48/win-x86/SimpleCom.dll SimpleCom/SimpleCom.c -lole32 -loleaut32 -lpsapi -Wl,--add-stdcall-alias
cd publish
zip -r ../site.zip *
cd ..
az webapp deployment source config-zip --resource-group jonnymoo_rg_9172 --name com-sandbox-demo-app --src site.zip

Remember if you want to deploy SimpleComDelphi.dll - you can get it from the artifacts on the github build. You will have to manually go put it onto azure in site/wwwroot/workers/net48/win-x86

About

Process isolation library for .NET - protect your application from legacy, unmanaged, or problematic code

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published