[C#] 基于IpcChannel进程通信开发插件,移除MAF约束


  MAF的进程隔离是MEF、IOC无法实现的,好处在于可以立即结束进程,比如遇到耗时长的任务,无法立刻取消,白白地浪费了CPU、占用了资源,但是MAF的隔离性太强了,多个项目分开文件夹存放,导致了DLL文件无法共享,这无疑增加了安装包的大小,且至少需要7个项目(加主程序),改一个接口协议需要改7个项目代码,实在繁琐,参考官方的IpcChannel通信,只需要三个项目就可以搭建:IpcServerChannel、IpcClientChannel、RemoteObject,这里是先启动服务端,再启动客服端发送请求,改进一下,由客服端主动启动服务端,根据guid生成url,便可生成一个唯一的服务,另外服务端不需要事先引用DLL,而是加载由客服端指定的程序集和远程类,如此,服务端便只是一个空壳,类似MAF的AddInProcess,可能有些小伙伴到了这一步,发现客服端在第二次调用同一个远程类的RegisterWellKnownClientType方法时抛出“远程处理配置失败,异常为“RemotingException: 试图重定向类型“MySC.MyComponent, MyServiceComponent”的激活,而该类型已被重定向”,这是合理的,不使用重定向的方式便可,使用RealProxy直接拿到远程类的实例,使用该实例的方法是被代理的(Ipc通信有效),而使用直接创建远程类的方法是由客服端亲自执行的(非代理的),客服端退出时需要保证服务端退出,使用CreateJobObject解决(看了MAF源码,没发现如何保证插件退出的)

服务端:

using System;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Threading;

namespace IpcServerChannelApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(String.Join(" ", args));

            var guid = args[0];

            var list = from a in args.Skip(1)
                       let txts = a.Split('|')
                       select new
                       {
                           AssemblyName = txts[0],
                           TypeName = txts[1]
                       };

            // Create and register an IPC channel
            IpcServerChannel serverChannel = new IpcServerChannel(guid);
            ChannelServices.RegisterChannel(serverChannel, false);

            foreach (var item in list)
            {
                var assembly = Assembly.LoadFrom(item.AssemblyName);
                var type = assembly.GetType(item.TypeName, true, false);
                // Expose an object
                RemotingConfiguration.RegisterWellKnownServiceType(type, type.Name, WellKnownObjectMode.Singleton);
                Console.WriteLine("RegisterWellKnownServiceType: {0}", item.TypeName);
            }

            var readyEvent = new EventWaitHandle(false, EventResetMode.ManualReset, "IpcProxy:" + guid);
            readyEvent.Set();

            // Wait for calls
            Console.WriteLine("Listening on {0}", serverChannel.GetChannelUri());
            Console.ReadLine();
        }
    }
}

客服端:

using IpcChannelModel;
using System;
using System.Data;
using System.Data.SQLite;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Security.Permissions;
using System.Threading;

namespace IpcClientChannelApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var path = @"E:\AppAnalyserFiles\data\DataBrowse.db";
            var connectString = ConnectionStringFactory(path);

            using (var proxy = new IpcProcessProxy())
            {
                if (proxy.Start())
                {
                    var sqliteContract = proxy.Contract;

                    if (sqliteContract.Open(connectString))
                    {
                        Console.WriteLine("connect sqlite :" + path);
                        var sql = "select count(*) from DataBrowse";
                        var count = sqliteContract.ExecuteScalar(sql);
                        Console.WriteLine(sql);
                        Console.WriteLine(count);
                    }
                }
            }

            Thread.Sleep(1000);

            using (var proxy = new IpcProcessProxy())
            {
                if (proxy.Start())
                {
                    var sqliteContract = proxy.Contract;
                  
                    if (sqliteContract.Open(connectString))
                    {
                        Console.WriteLine("connect sqlite :" + path);
                        var sql = "select count(*) from DataBrowse";
                        var count = sqliteContract.ExecuteScalar(sql);
                        Console.WriteLine(sql);
                        Console.WriteLine(count);
                    }
                }
            }

            Thread.Sleep(1000);
            {
                var sqliteContract = new SqliteContract();

                if (sqliteContract.Open(connectString))
                {
                    Console.WriteLine("connect sqlite :" + path);
                    var sql = "select count(*) from DataBrowse";
                    var count = sqliteContract.ExecuteScalar(sql);
                    Console.WriteLine(sql);
                    Console.WriteLine(count);
                }
            }

            Console.ReadLine();
        }

        private static string ConnectionStringFactory(string path)
        {
            var builder = new SQLiteConnectionStringBuilder
            {
                DataSource = path,
                Version = 3,
                UseUTF16Encoding = true,
                Pooling = false,
                CacheSize = 100,
                DateTimeFormat = SQLiteDateFormats.CurrentCulture,
                JournalMode = SQLiteJournalModeEnum.Wal,
                DefaultDbType = DbType.String,
                SyncMode = SynchronizationModes.Normal,
                BusyTimeout = 11
            };

            return builder.ToString();
        }
    }

    class IpcProcessProxy : IDisposable where T : MarshalByRefObject, new()
    {
        public bool Start(int startUpTimeout = 10000)
        {
            var type = typeof(T);
            var guid = Guid.NewGuid().ToString();
            var startInfo = new ProcessStartInfo
            {
                CreateNoWindow = true,
                UseShellExecute = false,
                FileName = "IpcServerChannelApp.exe",
                Arguments = $"{guid} {type.Assembly.Location}|{type.FullName}"
            };

            process = Process.Start(startInfo);

            if (process != null && !process.HasExited)
            {
                hJob = NativeMethods.CreateJobObjectW(lpName: "Job:" + guid);

                if (hJob != IntPtr.Zero)
                {
                    // 设置数据以便用于增加限制
                    var size = Marshal.SizeOf(typeof(NativeMethods.JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
                    var newStructBasic = new NativeMethods.JOBOBJECT_BASIC_LIMIT_INFORMATION
                    {
                        LimitFlags = 0x00002000
                    };
                    var newStructExtend = new NativeMethods.JOBOBJECT_EXTENDED_LIMIT_INFORMATION
                    {
                        BasicLimitInformation = newStructBasic
                    };

                    var pStructAddr = Marshal.AllocHGlobal(size + 1);
                    Marshal.StructureToPtr(newStructExtend, pStructAddr, false);

                    // 设置限制
                    NativeMethods.SetInformationJobObject(hJob, 9, pStructAddr, size);

                    Marshal.FreeHGlobal(pStructAddr);

                    NativeMethods.AssignProcessToJobObject(hJob, process.Handle);
                }

                // wait until it's ready
                using (var readyEvent = new EventWaitHandle(false, EventResetMode.ManualReset, "IpcProxy:" + guid))
                {
                    if (readyEvent.WaitOne(startUpTimeout, false))
                    {
                        clientChannel = new IpcClientChannel();
                        ChannelServices.RegisterChannel(clientChannel, false);

                        var url = $"ipc://{guid}/{type.Name}";
                        var messageSink = clientChannel.CreateMessageSink(url, null, out _);

                        var myProxy = new RealProxyBase(typeof(T), url, messageSink);
                        t = (T)myProxy.GetTransparentProxy();
                        return true;
                    }
                }
            }

            Dispose();
            return false;
        }

        public void Dispose()
        {
            if (clientChannel != null)
            {
                ChannelServices.UnregisterChannel(clientChannel);
                clientChannel = null;
            }
            if (process != null)
            {
                if (!process.HasExited)
                    process.Kill();
                process = null;
            }
            if (hJob != IntPtr.Zero)
                NativeMethods.TerminateJobObject(hJob, 0);
        }

        public T Contract => t;

        private Process process;
        private IpcClientChannel clientChannel;
        private T t;
        private IntPtr hJob;
    }

    class RealProxyBase : RealProxy
    {
        private string url;
        private IMessageSink messageSink;

        [PermissionSet(SecurityAction.LinkDemand)]
        public RealProxyBase(Type myType, string url, IMessageSink messageSink) : base(myType)
        {
            this.url = url;
            this.messageSink = messageSink;
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)]
        public override IMessage Invoke(IMessage message)
        {
            message.Properties["__Uri"] = url;
            return messageSink.SyncProcessMessage(message);
        }
    }

    static class NativeMethods
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct JOBOBJECT_BASIC_LIMIT_INFORMATION
        {
            public long PerProcessUserTimeLimit;
            public long PerJobUserTimeLimit;
            public int LimitFlags;
            public uint MinimumWorkingSetSize;
            public uint MaximumWorkingSetSize;
            public int ActiveProcessLimit;
            public long Affinity;
            public int PriorityClass;
            public int SchedulingClass;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
        {
            public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
            public IO_COUNTERS IoInfo;
            public uint ProcessMemoryLimit;
            public uint JobMemoryLimit;
            public uint PeakProcessMemoryUsed;
            public uint PeakJobMemoryUsed;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct IO_COUNTERS
        {
            public long ReadOperationCount;
            public long WriteOperationCount;
            public long OtherOperationCount;
            public long ReadTransferCount;
            public long WriteTransferCount;
            public long OtherTransferCount;
        }

        /// 
        /// 创建任务对象
        /// 
        /// 指向安全相关属性结构体的指针,通常不用
        /// 任务名称
        /// 
        [DllImport("Kernel32.dll")]
        public static extern IntPtr CreateJobObjectW([In, Optional] IntPtr lpJobAttributes, [In, Optional] string lpName);

        /// 
        /// 添加进程到任务对象中
        /// 
        /// 任务句柄
        /// 进程指针
        /// 
        [DllImport("Kernel32.dll")]
        public static extern bool AssignProcessToJobObject([In] IntPtr hJob, [In] IntPtr hProcess);

        /// 
        /// 设置任务限制
        /// 
        /// 任务句柄
        /// 设置的分类
        /// 设置的参数结构体地址指针
        /// 设置的参数结构体长度
        /// 
        [DllImport("Kernel32.dll", SetLastError = true)]
        public static extern bool SetInformationJobObject([In] IntPtr hJob, [In] int jobObjectInformationClass, [In] IntPtr lpJobObjectInformation, [In] int cbJobObjectInformationLength);


        /// 
        /// 添加进程到任务对象中
        /// 
        /// 任务句柄
        /// 进程指针
        /// 
        [DllImport("Kernel32.dll")]
        public static extern bool TerminateJobObject([In] IntPtr hJob, [In] UInt32 uExitCode);
    }
}

远程类:

using System;
using System.Collections.Generic;

namespace IpcChannelModel
{
    public class SqliteContract : MarshalByRefObject, IDisposable
    {
        private SqliteClient client = new SqliteClient();

        public bool Open(string connectString) => client.Open(connectString);

        public int Execute(string sql) => client.Execute(sql);

        public object ExecuteScalar(string sql) => client.ExecuteScalar(sql);

        public List Query(string sql) => client.Query(sql);

        public void Dispose() => client.Dispose();
    }
}
using System;
using System.Collections.Generic;
using System.Data.SQLite;

namespace IpcChannelModel
{
    class SqliteClient : IDisposable
    {
        private SQLiteConnection conn;

        public bool Open(string connectString)
        {
            conn = new SQLiteConnection(connectString).OpenAndReturn();
            return conn != null;
        }

        public int Execute(string sql) => conn.Execute(sql);

        public object ExecuteScalar(string sql) => conn.ExecuteScalar(sql);

        public List Query(string sql) => throw new NotImplementedException();

        public void Dispose() => conn.Dispose();
    }
}
C