1. 程式人生 > >C#單個程式集程式碼熱更新

C#單個程式集程式碼熱更新

有的時候我們想更新我們正在執行中的程式碼,而不想軟體重啟。

 微軟提供的標準方法是通過應用程式域來實現程式碼熱更新,意思就是說,把自己想要進行熱更新的程式碼放到另外一個應用程式域中,在檢測到程式碼需要變更的時候,解除安裝掉那個程式域然後重新載入來實現程式碼熱更新。按照微軟的說法,一個應用程式域是無法實現程式碼熱更新的。

 但是,一下方法確實是可以在單個應用程式域中實現程式碼熱更新的,本人嘗試過,在公司的上班中,軟體的頻繁重啟讓人很煩,又不能在另外一個程式域中執行,所以嘗試了一下辦法,在一個應用程式域中改變程式碼是可行的。

 方法如下:假設程式集A中有類a,程式集B中有類b,b要去呼叫a的方法,這個情況下可以對a的部分程式碼(僅對方法和屬性有效)進行熱更新。第一步是針對類a 進行程式碼重寫,生成一個假的A.dll,但是具有和包含真A中類,欄位,屬性,和方法簽名,把真A的Dll放到另外一個資料夾。當B去呼叫A中方法的時候,假的A會通過位元組陣列的形式將真A儲存在自己的一個Assembly(最好放在一個其他類的靜態變數中,我是那麼做的)變數中,並且檢測真A的檔案改動,當真A發生變化的時候對Assembly變數通過位元組陣列的形式重新賦值。這個樣子就可以就可以實現部分程式碼熱更新。

 假A對真A程式碼的重寫原則:假a類中至少要有三個變數,一個object變數,用來存對真a物件的引用。一個Type變數,用來儲存真a的型別資訊,還有真A的Assembly變數。假a在初始化的時候,要初始化剛才的那三個變數,對假a方法的呼叫通過反射轉換為對真a物件(object變數,用來存對真a物件的引用)的呼叫。並且把真a方法的返回結果返回回去在真A變化之後,假A重新載入真A,這個時間點之後,在被建立的假a執行的程式碼已經是程式碼變化之後真A的程式碼,就可以實現程式碼特更新。

 把真A程式碼重構並且生成編譯出假A是一個很費勁容易出錯的過程,如下是我生成假A的程式碼邏輯,應該是存在某些問題的(嘿嘿,主要是說明方法)

using Microsoft.CSharp;
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace DllLoad
{
    class Core2
    {


        List<string> dlls = new List<string>();
        string outpath = string.Empty;

        public EventHandler CompileCompleted;

        public Core2()
        {
        }

        public void SetInfo(List<string> dlls, string outpath)
        {
            this.dlls = dlls;
            this.outpath = outpath;
        }


        public void StartCompile()
        {
            try
            {
                dlls.ForEach(o =>
                {
                    Assembly assem = Assembly.LoadFrom(o);
                    Compile(assem, outpath);
                });
            }
            catch (Exception e)
            {
                KeyValuePair<bool, string> error = new KeyValuePair<bool, string>(false, e.Message);
                CompileCompleted(error, null);
                return;
            }
            KeyValuePair<bool, string> correct = new KeyValuePair<bool, string>(true, null);
            CompileCompleted(correct, null);
        }


        public void Compile(Assembly assembly, string dllOutPath)
        {
            CodeCompileUnit compunit = new CodeCompileUnit();
            Dictionary<string, CodeNamespace> nameDic = new Dictionary<string, CodeNamespace>();

            FileInfo assemblyInfo = new FileInfo(assembly.Location);
            string fileName = assemblyInfo.Name;

            foreach (var typeinfo in assembly.DefinedTypes)
            {
                string namespa = typeinfo.Namespace;
                var space = new CodeNamespace(namespa);
                if (!nameDic.ContainsKey(namespa))
                {
                    nameDic.Add(namespa, space);
                    compunit.Namespaces.Add(space);
                    space.Imports.AddRange((new string[] {"System","System.Linq","System.Reflection","System.Text","Free"
                    }).ToList().Select(o => new CodeNamespaceImport(o)).ToArray());
                }

                #region 新增類中的變數

                CodeTypeDeclaration clas = new CodeTypeDeclaration(typeinfo.Name);
                CodeMemberField obj = new CodeMemberField(typeof(object), "obj");
                obj.InitExpression = new CodePrimitiveExpression(null);
                CodeMemberField self = new CodeMemberField(typeof(Type), "self");
                self.InitExpression = new CodePrimitiveExpression(null);
                CodeMemberField ass = new CodeMemberField(typeof(Assembly), "ass");
                ass.InitExpression = new CodePrimitiveExpression(null);
                clas.Members.Add(obj);
                clas.Members.Add(self);
                clas.Members.Add(ass);
                #endregion


                //新增建構函式
                CodeConstructor constructor = new CodeConstructor()
                {
                    Attributes = MemberAttributes.Public
                };
                constructor.Statements.Add(new CodeSnippetExpression("Inite()"));
                clas.Members.Add(constructor);

                //新增Inite() 函式
                CodeMemberMethod Inite = new CodeMemberMethod()
                {
                    Name = "Inite",
                    ReturnType = new CodeTypeReference(typeof(void))
                };
                Inite.Attributes = MemberAttributes.Private;
                Inite.Statements.Add(new CodeSnippetStatement(@"
                        ass = FastLoadAssembly.GetAssembly();
                        self = ass.GetType(""{0}"");
                        obj = Activator.CreateInstance(self);
                ".Replace("{0}", typeinfo.AsType().FullName)));
                clas.Members.Add(Inite);

                
                clas.BaseTypes.AddRange(new List<CodeTypeReference> { new CodeTypeReference(typeinfo.BaseType) }.Union(typeinfo.GetInterfaces().Select(o => new CodeTypeReference(o))).Distinct().ToArray());
                space.Imports.AddRange(new List<string> { typeinfo.Namespace }.Union(typeinfo.GetInterfaces().Select(o => o.Namespace)).Select(o => new CodeNamespaceImport(o)).Distinct().ToArray());


                foreach (var met in typeinfo.GetMembers())
                {
                    switch (met.MemberType)
                    {
                        case MemberTypes.Method:
                            {
                                MethodInfo info = (MethodInfo)met;
                                if (!info.IsPublic)
                                    continue;
                                CodeMemberMethod metho = SolveMethod(met,typeinfo);
                                clas.Members.Add(metho);
                            }
                            break;

                        case MemberTypes.Property:
                            {

                            }
                            break;
                    }
                }
                space.Types.Add(clas);

            }
            compunit.Namespaces.Add(GetCommonClass(dllOutPath + "\\" + fileName));


            CSharpCodeProvider cprovider = new CSharpCodeProvider();

            ICodeGenerator gen = cprovider.CreateGenerator();
            StringBuilder fileContent = new StringBuilder();
            using (StringWriter sw = new StringWriter(fileContent))
            {
                gen.GenerateCodeFromCompileUnit(compunit, sw, new CodeGeneratorOptions());//想把生成的程式碼儲存為cs檔案
            }

            string reslut = fileContent.ToString();

            ICodeCompiler compiler = cprovider.CreateCompiler();
            //編譯引數
            CompilerParameters cp = new CompilerParameters();

            // 把當前應用引用的程式集全部新增進來
            cp.ReferencedAssemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies().Select(o => o.Location).Distinct().ToArray());

            cp.OutputAssembly = dllOutPath + "\\" + fileName;
            cp.GenerateInMemory = false; //是否只在記憶體中生成
            cp.IncludeDebugInformation = true;//包含除錯符號  pdb檔案
            cp.GenerateExecutable = false;//生成dll,不是exe 
            cp.WarningLevel = 4;
            cp.TreatWarningsAsErrors = false;

            string filePath = dllOutPath + "\\" + fileName + ".cs";
            File.WriteAllText(filePath, fileContent.ToString());


            CompilerResults cr = compiler.CompileAssemblyFromFile(cp, filePath); //儲存檔案再進行編譯 待會兒除錯就比較方便了 ,可以直接斷點到剛才生成的檔案裡面
            //  CompilerResults cr = compiler.CompileAssemblyFromDom(cp, compunit); //這樣的生成 不用寫檔案 ,就是除錯麻煩
            String outputMessage = "";
            foreach (var item in cr.Output)
            {
                outputMessage += item + Environment.NewLine;//除錯的最終輸出資訊
            }
            if (cr.Errors.HasErrors)//有編譯錯誤就丟擲異常
            {
                throw new Exception("error:" + Environment.NewLine + outputMessage);
            }
        }



        private CodeMemberMethod SolveMethod(MemberInfo info,TypeInfo type)
        {
            if (info.MemberType != MemberTypes.Method)
                return null;
            MethodInfo method = (MethodInfo)info;

            CodeMemberMethod newMethod = new CodeMemberMethod()
            {
                Name = method.Name,
                ReturnType = new CodeTypeReference(method.ReturnType)
            };

            newMethod.Parameters.AddRange(
                method.GetParameters().Select(o => {
                    var res = new CodeParameterDeclarationExpression
                    {
                        Type = new CodeTypeReference(o.ParameterType),
                        Name = o.Name
                    };
                    return res;
                }).ToArray()
            );
            switch (method.Attributes)
            {
                case MethodAttributes.Public | MethodAttributes.HideBySig:
                    {
                        if(type.DeclaredMembers.Contains(info) )
                            newMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                        else
                            newMethod.Attributes = MemberAttributes.Public | MemberAttributes.New | MemberAttributes.Final;
                    }
                    break;
                case MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual:
                    newMethod.Attributes = MemberAttributes.Public | MemberAttributes.Override;
                    break;
                case MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.NewSlot:
                    newMethod.Attributes = MemberAttributes.Public;
                    break;
            }
            string statement = @" MethodInfo info= self.GetMethod(""{0}"");"
                               + "\r\nObject result = info.Invoke(obj,  ";

            string param = string.Join(",", method.GetParameters().Select(o => o.Name).ToList());
            string expression = statement.Replace("{0}", info.Name);
            if (!string.IsNullOrWhiteSpace(param))
                newMethod.Statements.Add(new CodeSnippetStatement(expression + "new object[] { " + param + " });"));
            else
                newMethod.Statements.Add(new CodeSnippetStatement(expression + "null );"));

            if (method.ReturnType != typeof(void))
            {
                string returnstr = @"return ({0})result;";
                newMethod.Statements.Add(new CodeSnippetStatement(string.Format(returnstr, method.ReturnType.ToString())));
            }
            return newMethod;
        }

        


        private CodeNamespace GetCommonClass(string dllpath)
        {
            CodeNamespace space = new CodeNamespace("Free");
            CodeTypeDeclaration common = new CodeTypeDeclaration("FastLoadAssembly");
            space.Types.Add(common);
            space.Imports.AddRange((new string[] {"System.Reflection","System.IO"
                    }).ToList().Select(o => new CodeNamespaceImport(o)).ToArray());
            common.Attributes = MemberAttributes.Assembly;

            CodeMemberField assembly = new CodeMemberField(typeof(Assembly), "assembly");
            assembly.Attributes = MemberAttributes.Private | MemberAttributes.Static;
            common.Members.Add(assembly);

            CodeMemberField path = new CodeMemberField(typeof(string), "path");
            path.InitExpression = new CodePrimitiveExpression(dllpath);


            path.Attributes = MemberAttributes.Private | MemberAttributes.Const;
            common.Members.Add(path);

            CodeMemberField watch = new CodeMemberField(typeof(FileSystemWatcher), "watcher");
            watch.Attributes = MemberAttributes.Private | MemberAttributes.Static;
            common.Members.Add(watch);
            CodeMemberMethod Load = new CodeMemberMethod()
            {
                Name = "Load",
                ReturnType = new CodeTypeReference(typeof(void))
            };
            Load.Statements.Add(new CodeSnippetStatement(@"
                    byte[] buffer;
                    FileStream stream = File.OpenRead(path);
                    buffer = new byte[stream.Length];
                    stream.Read(buffer,0,(int)stream.Length);
                    stream.Close();
                    assembly = Assembly.Load(buffer);
            "));

            Load.Attributes = MemberAttributes.Private | MemberAttributes.Static;

            CodeMemberMethod GetAssembly = new CodeMemberMethod()
            {
                Name = "GetAssembly",
                ReturnType = new CodeTypeReference(typeof(Assembly))
            };
            GetAssembly.Statements.Add(new CodeSnippetStatement(@"
                   IniteWatcher();
                   if (assembly == null)
                    Load();
                    return assembly;
                "));

            GetAssembly.Attributes = MemberAttributes.Public | MemberAttributes.Static;

            CodeMemberMethod IniteWatcher = new CodeMemberMethod()
            {
                Name = "IniteWatcher",
                ReturnType = new CodeTypeReference(typeof(void))
            };
            IniteWatcher.Statements.Add(new CodeSnippetStatement(@"
                    if (watcher==null)
                    {
                        FileInfo info = new FileInfo(path);
                        watcher = new FileSystemWatcher();
                        watcher.Filter = info.Name;
                        watcher.Path = info.DirectoryName;
                        watcher.EnableRaisingEvents = true;
                        watcher.Created += (s, o) => { Load(); };
                    }
                "));
            IniteWatcher.Attributes = MemberAttributes.Private | MemberAttributes.Static;

            common.Members.Add(IniteWatcher);
            common.Members.Add(GetAssembly);
            common.Members.Add(Load);

            return space;
        }

        


    }
}