X Tutup
using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; using NUnit.Framework; using Python.Runtime; using PyRuntime = Python.Runtime.Runtime; // // This test case is disabled on .NET Standard because it doesn't have all the // APIs we use. We could work around that, but .NET Core doesn't implement // domain creation, so it's not worth it. // // Unfortunately this means no continuous integration testing for this case. // #if NETFRAMEWORK namespace Python.EmbeddingTest { class TestDomainReload { abstract class CrossCaller : MarshalByRefObject { public abstract ValueType Execute(ValueType arg); } /// /// Test that the python runtime can survive a C# domain reload without crashing. /// /// At the time this test was written, there was a very annoying /// seemingly random crash bug when integrating pythonnet into Unity. /// /// The repro steps that David Lassonde, Viktoria Kovecses and /// Benoit Hudson eventually worked out: /// 1. Write a HelloWorld.cs script that uses Python.Runtime to access /// some C# data from python: C# calls python, which calls C#. /// 2. Execute the script (e.g. make it a MenuItem and click it). /// 3. Touch HelloWorld.cs on disk, forcing Unity to recompile scripts. /// 4. Wait several seconds for Unity to be done recompiling and /// reloading the C# domain. /// 5. Make python run the gc (e.g. by calling gc.collect()). /// /// The reason: /// A. In step 2, Python.Runtime registers a bunch of new types with /// their tp_traverse slot pointing to managed code, and allocates /// some objects of those types. /// B. In step 4, Unity unloads the C# domain. That frees the managed /// code. But at the time of the crash investigation, pythonnet /// leaked the python side of the objects allocated in step 1. /// C. In step 5, python sees some pythonnet objects in its gc list of /// potentially-leaked objects. It calls tp_traverse on those objects. /// But tp_traverse was freed in step 3 => CRASH. /// /// This test distills what's going on without needing Unity around (we'd see /// similar behaviour if we were using pythonnet on a .NET web server that did /// a hot reload). /// [Test] public static void DomainReloadAndGC() { Assert.IsFalse(PythonEngine.IsInitialized); RunAssemblyAndUnload("test1"); Assert.That(PyRuntime.Py_IsInitialized() != 0, "On soft-shutdown mode, Python runtime should still running"); RunAssemblyAndUnload("test2"); Assert.That(PyRuntime.Py_IsInitialized() != 0, "On soft-shutdown mode, Python runtime should still running"); if (PythonEngine.DefaultShutdownMode == ShutdownMode.Normal) { // The default mode is a normal mode, // it should shutdown the Python VM avoiding influence other tests. PyRuntime.PyGILState_Ensure(); PyRuntime.Py_Finalize(); } } #region CrossDomainObject class CrossDomainObjectStep1 : CrossCaller { public override ValueType Execute(ValueType arg) { try { // Create a C# user-defined object in Python. Asssing some values. Type type = typeof(Python.EmbeddingTest.Domain.MyClass); string code = string.Format(@" import clr clr.AddReference('{0}') from Python.EmbeddingTest.Domain import MyClass obj = MyClass() obj.Method() obj.StaticMethod() obj.Property = 1 obj.Field = 10 ", Assembly.GetExecutingAssembly().FullName); using (Py.GIL()) using (var scope = Py.CreateScope()) { scope.Exec(code); using (PyObject obj = scope.Get("obj")) { Debug.Assert(obj.AsManagedObject(type).GetType() == type); // We only needs its Python handle PyRuntime.XIncref(obj.Handle); return obj.Handle; } } } catch (Exception e) { Debug.WriteLine(e); throw; } } } class CrossDomainObjectStep2 : CrossCaller { public override ValueType Execute(ValueType arg) { // handle refering a clr object created in previous domain, // it should had been deserialized and became callable agian. IntPtr handle = (IntPtr)arg; try { using (Py.GIL()) { IntPtr tp = Runtime.Runtime.PyObject_TYPE(handle); IntPtr tp_clear = Marshal.ReadIntPtr(tp, TypeOffset.tp_clear); Assert.That(tp_clear, Is.Not.Null); using (PyObject obj = new PyObject(handle)) { obj.InvokeMethod("Method"); obj.InvokeMethod("StaticMethod"); using (var scope = Py.CreateScope()) { scope.Set("obj", obj); scope.Exec(@" obj.Method() obj.StaticMethod() obj.Property += 1 obj.Field += 10 "); } var clrObj = obj.As(); Assert.AreEqual(clrObj.Property, 2); Assert.AreEqual(clrObj.Field, 20); } } } catch (Exception e) { Debug.WriteLine(e); throw; } return 0; } } /// /// Create a C# custom object in a domain, in python code. /// Unload the domain, create a new domain. /// Make sure the C# custom object created in the previous domain has been re-created /// [Test] public static void CrossDomainObject() { RunDomainReloadSteps(); } #endregion #region Tempary tests // https://github.com/pythonnet/pythonnet/pull/1074#issuecomment-596139665 [Test] public void CrossReleaseBuiltinType() { void ExecTest() { try { PythonEngine.Initialize(); var numRef = CreateNumReference(); Assert.True(numRef.IsAlive); PythonEngine.Shutdown(); // <- "run" 1 ends PythonEngine.Initialize(); // <- "run" 2 starts GC.Collect(); GC.WaitForPendingFinalizers(); // <- this will put former `num` into Finalizer queue Finalizer.Instance.Collect(); // ^- this will call PyObject.Dispose, which will call XDecref on `num.Handle`, // but Python interpreter from "run" 1 is long gone, so it will corrupt memory instead. Assert.False(numRef.IsAlive); } finally { PythonEngine.Shutdown(); } } var errorArgs = new List(); void ErrorHandler(object sender, Finalizer.ErrorArgs e) { errorArgs.Add(e); } Finalizer.Instance.ErrorHandler += ErrorHandler; try { for (int i = 0; i < 10; i++) { ExecTest(); } } finally { Finalizer.Instance.ErrorHandler -= ErrorHandler; } Assert.AreEqual(errorArgs.Count, 0); } [Test] public void CrossReleaseCustomType() { void ExecTest() { try { PythonEngine.Initialize(); var objRef = CreateConcreateObject(); Assert.True(objRef.IsAlive); PythonEngine.Shutdown(); // <- "run" 1 ends PythonEngine.Initialize(); // <- "run" 2 starts GC.Collect(); GC.WaitForPendingFinalizers(); Finalizer.Instance.Collect(); Assert.False(objRef.IsAlive); } finally { PythonEngine.Shutdown(); } } var errorArgs = new List(); void ErrorHandler(object sender, Finalizer.ErrorArgs e) { errorArgs.Add(e); } Finalizer.Instance.ErrorHandler += ErrorHandler; try { for (int i = 0; i < 10; i++) { ExecTest(); } } finally { Finalizer.Instance.ErrorHandler -= ErrorHandler; } Assert.AreEqual(errorArgs.Count, 0); } private static WeakReference CreateNumReference() { var num = 3216757418.ToPython(); Assert.AreEqual(num.Refcount, 1); WeakReference numRef = new WeakReference(num, false); return numRef; } private static WeakReference CreateConcreateObject() { var obj = new Domain.MyClass().ToPython(); Assert.AreEqual(obj.Refcount, 1); WeakReference numRef = new WeakReference(obj, false); return numRef; } #endregion Tempary tests /// /// This is a magic incantation required to run code in an application /// domain other than the current one. /// class Proxy : MarshalByRefObject { public void RunPython() { Console.WriteLine("[Proxy] Entering RunPython"); PythonRunner.RunPython(); Console.WriteLine("[Proxy] Leaving RunPython"); } public object Call(string methodName, params object[] args) { var pythonrunner = typeof(PythonRunner); var method = pythonrunner.GetMethod(methodName); return method.Invoke(null, args); } } static T CreateInstanceInstanceAndUnwrap(AppDomain domain) { Type type = typeof(T); var theProxy = (T)domain.CreateInstanceAndUnwrap( type.Assembly.FullName, type.FullName); return theProxy; } /// /// Create a domain, run the assembly in it (the RunPython function), /// and unload the domain. /// static void RunAssemblyAndUnload(string domainName) { Console.WriteLine($"[Program.Main] === creating domain {domainName}"); AppDomain domain = CreateDomain(domainName); // Create a Proxy object in the new domain, where we want the // assembly (and Python .NET) to reside var theProxy = CreateInstanceInstanceAndUnwrap(domain); theProxy.Call("InitPython", ShutdownMode.Soft); // From now on use the Proxy to call into the new assembly theProxy.RunPython(); theProxy.Call("ShutdownPython"); Console.WriteLine($"[Program.Main] Before Domain Unload on {domainName}"); AppDomain.Unload(domain); Console.WriteLine($"[Program.Main] After Domain Unload on {domainName}"); // Validate that the assembly does not exist anymore try { Console.WriteLine($"[Program.Main] The Proxy object is valid ({theProxy}). Unexpected domain unload behavior"); Assert.Fail($"{theProxy} should be invlaid now"); } catch (AppDomainUnloadedException) { Console.WriteLine("[Program.Main] The Proxy object is not valid anymore, domain unload complete."); } } private static AppDomain CreateDomain(string name) { // Create the domain. Make sure to set PrivateBinPath to a relative // path from the CWD (namely, 'bin'). // See https://stackoverflow.com/questions/24760543/createinstanceandunwrap-in-another-domain var currentDomain = AppDomain.CurrentDomain; var domainsetup = new AppDomainSetup() { ApplicationBase = currentDomain.SetupInformation.ApplicationBase, ConfigurationFile = currentDomain.SetupInformation.ConfigurationFile, LoaderOptimization = LoaderOptimization.SingleDomain, PrivateBinPath = "." }; var domain = AppDomain.CreateDomain( $"My Domain {name}", currentDomain.Evidence, domainsetup); return domain; } /// /// Resolves the assembly. Why doesn't this just work normally? /// static Assembly ResolveAssembly(object sender, ResolveEventArgs args) { var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var assembly in loadedAssemblies) { if (assembly.FullName == args.Name) { return assembly; } } return null; } static void RunDomainReloadSteps() where T1 : CrossCaller where T2 : CrossCaller { ValueType arg = null; Type type = typeof(Proxy); { AppDomain domain = CreateDomain("test_domain_reload_1"); try { var theProxy = CreateInstanceInstanceAndUnwrap(domain); theProxy.Call("InitPython", ShutdownMode.Reload); var caller = CreateInstanceInstanceAndUnwrap(domain); arg = caller.Execute(arg); theProxy.Call("ShutdownPython"); } finally { AppDomain.Unload(domain); } } { AppDomain domain = CreateDomain("test_domain_reload_2"); try { var theProxy = CreateInstanceInstanceAndUnwrap(domain); theProxy.Call("InitPython", ShutdownMode.Reload); var caller = CreateInstanceInstanceAndUnwrap(domain); caller.Execute(arg); theProxy.Call("ShutdownPythonCompletely"); } finally { AppDomain.Unload(domain); } } if (PythonEngine.DefaultShutdownMode == ShutdownMode.Normal) { Assert.IsTrue(PyRuntime.Py_IsInitialized() == 0); } } } // // The code we'll test. All that really matters is // using GIL { Python.Exec(pyScript); } // but the rest is useful for debugging. // // What matters in the python code is gc.collect and clr.AddReference. // // Note that the language version is 2.0, so no $"foo{bar}" syntax. // static class PythonRunner { public static void RunPython() { AppDomain.CurrentDomain.DomainUnload += OnDomainUnload; string name = AppDomain.CurrentDomain.FriendlyName; Console.WriteLine("[{0} in .NET] In PythonRunner.RunPython", name); using (Py.GIL()) { try { var pyScript = string.Format("import clr\n" + "print('[{0} in python] imported clr')\n" + "clr.AddReference('System')\n" + "print('[{0} in python] allocated a clr object')\n" + "import gc\n" + "gc.collect()\n" + "print('[{0} in python] collected garbage')\n", name); PythonEngine.Exec(pyScript); } catch (Exception e) { Console.WriteLine(string.Format("[{0} in .NET] Caught exception: {1}", name, e)); throw; } } } private static IntPtr _state; public static void InitPython(ShutdownMode mode) { PythonEngine.Initialize(mode: mode); _state = PythonEngine.BeginAllowThreads(); } public static void ShutdownPython() { PythonEngine.EndAllowThreads(_state); PythonEngine.Shutdown(); } public static void ShutdownPythonCompletely() { PythonEngine.EndAllowThreads(_state); // XXX: Reload mode will reserve clr objects after `Runtime.Shutdown`, // if it used a another mode(the default mode) in other tests, // when other tests trying to access these reserved objects, it may cause Domain exception, // thus it needs to reduct to Soft mode to make sure all clr objects remove from Python. var defaultMode = PythonEngine.DefaultShutdownMode; if (defaultMode != ShutdownMode.Reload) { PythonEngine.ShutdownMode = defaultMode; } PythonEngine.Shutdown(); } static void OnDomainUnload(object sender, EventArgs e) { Console.WriteLine(string.Format("[{0} in .NET] unloading", AppDomain.CurrentDomain.FriendlyName)); } } } namespace Python.EmbeddingTest.Domain { [Serializable] public class MyClass { public int Property { get; set; } public int Field; public void Method() { } public static void StaticMethod() { } } } #endif
X Tutup