/// <summary> /// Executes assembly in this thread. /// </summary> /// <param name="asmFile">Full path of assembly file.</param> /// <param name="args">To pass to Main.</param> /// <param name="handleExceptions">Handle/print exceptions.</param> public static void Run(string asmFile, string[] args, bool handleExceptions) { try { //using var p1 = perf.local(); _LoadedScriptAssembly lsa = default; Assembly asm = lsa.Find(asmFile, out bool loaded); if (asm == null) { //print.it(loaded); var alc = new AssemblyLoadContext(null, isCollectible: true); //p1.Next(); #if STREAM //Uses LoadFromStream, not LoadFromAssemblyPath. //LoadFromAssemblyPath has this problem: does not reload modified assembly from same file. // Need a dll file with unique name for each version of same script. //Note: the 'loaded' was intended to make faster in some cases. However cannot use it because of the xor. //tested: step-debugging works. Don't need the overload with pdb parameter. var b = File.ReadAllBytes(asmFile); //with WD ~7 ms, but ~25 ms without xor. With Avast ~7 ms regardless of xor. Both fast if already scanned. for (int i = 0; i < b.Length; i++) { b[i] ^= 1; //prevented AV full dll scan twice. Now fully scans once (WD always scans when loading assembly from stream; Avast when loading from stream or earlier). } using var stream = new MemoryStream(b, false); //p1.Next(); asm = alc.LoadFromStream(stream); //with WD always 15-25 ms. With Avast it seems always fast. #else //Uses LoadFromAssemblyPath. If LoadFromStream, no source file/line info in stack traces; also no Assembly.Location, and possibly more problems. //never mind: Creates and loads many dlls when edit-run many times. //tested: .NET unloads dlls, but later than Assembly objects, maybe after 1-2 minutes, randomly. if (loaded) { var s = asmFile.Insert(^ 4, "'" + perf.mcs.ToString()); //info: compiler will delete all files with "'" on first run after editor restart #if true //copy file if (!Api.CopyFileEx(asmFile, s, null, default, null, 0)) { throw new AuException(0, "failed to copy assembly file"); } //p1.Next('C'); //bad: WD makes much slower. Scans 2 times. Avast scans faster, and only when copying. //never mind: compiler should create file with unique name, to avoid copying now. Probably would complicate too much, or even not possible. asm = alc.LoadFromAssemblyPath(s); #else //rename file. Faster, but unreliable when need to run soon again. Then compiler would not find the file and compile again. Or the new file could be replaced with the old, etc. if (!Api.MoveFileEx(asmFile, s, 0)) { throw new AuException(0, "failed to rename assembly file"); } p1.Next('C'); //WD does not scan when renaming asm = alc.LoadFromAssemblyPath(s); p1.Next('L'); //now need to rename or copy back. Else would compile each time. //if (!Api.MoveFileEx(s, asmFile, 0)) throw new AuException(0, "failed to rename assembly file"); //works, but no stack trace Task.Run(() => { Api.CopyFileEx(s, asmFile, null, default, null, 0); }); //make compiler happy next time. Now let AV scan it async.
/// <summary> /// Executes assembly in this thread. /// Handles exceptions. /// </summary> /// <param name="asmFile">Full path of assembly file.</param> /// <param name="args">To pass to Main.</param> /// <param name="pdbOffset">0 or offset of portable PDB in assembly file.</param> /// <param name="flags"></param> /// <param name="fullPathRefs">Paths of assemblies specified using full path.</param> public static void Run(string asmFile, string[] args, int pdbOffset, RAFlags flags = 0, string fullPathRefs = null) { ADebug.PrintIf(pdbOffset == 0, "pdbOffset 0"); bool inEditorThread = 0 != (flags & RAFlags.InEditorThread); bool findLoaded = inEditorThread; _LoadedScriptAssembly lsa = default; Assembly asm = findLoaded ? lsa.Find(asmFile) : null; if (asm == null) { #if true //var p1 = APerf.Create(); var alc = System.Runtime.Loader.AssemblyLoadContext.Default; //SHOULDDO: try to unload. It seems AssemblyLoadContext supports it. Not tested. I guess it would create more problems than is useful. //p1.Next(); using (var stream = AFile.WaitIfLocked(() => File.OpenRead(asmFile))) { //p1.Next(); if (pdbOffset > 0) { var b = new byte[pdbOffset]; stream.Read(b, 0, b.Length); using var msAsm = new MemoryStream(b); b = new byte[stream.Length - pdbOffset]; stream.Read(b, 0, b.Length); using var msDeb = new MemoryStream(b); //p1.Next('f'); asm = alc.LoadFromStream(msAsm, msDeb); //p1.Next(); } else { asm = alc.LoadFromStream(stream); } } //p1.NW(); //APerf.Next('a'); if (fullPathRefs != null) { var fpr = fullPathRefs.SegSplit("|"); alc.Resolving += (System.Runtime.Loader.AssemblyLoadContext alc, AssemblyName an) => { //AOutput.Write(an, an.Name, an.FullName); foreach (var v in fpr) { var s1 = an.Name; int iName = v.Length - s1.Length - 4; if (iName <= 0 || v[iName - 1] != '\\' || !v.Eq(iName, s1, true)) { continue; } if (!AFile.ExistsAsFile(v)) { continue; } //try { return(alc.LoadFromAssemblyPath(v)); //} catch(Exception ex) { ADebug.Print(ex.ToStringWithoutStack()); break; } } return(null); }; } //ADebug.PrintLoadedAssemblies(true, true); //AOutput.Write(asm); #else byte[] bAsm, bPdb = null; using (var stream = AFile.WaitIfLocked(() => File.OpenRead(asmFile))) { bAsm = new byte[pdbOffset > 0 ? pdbOffset : stream.Length]; stream.Read(bAsm, 0, bAsm.Length); try { if (pdbOffset > 0) { bPdb = new byte[stream.Length - pdbOffset]; stream.Read(bPdb, 0, bPdb.Length); } else { var s1 = Path.ChangeExtension(asmFile, "pdb"); if (AFile.ExistsAsFile(s1)) { bPdb = File.ReadAllBytes(s1); } } } catch (Exception ex) { bPdb = null; ADebug.Print(ex); } //not very important } //APerf.Next('f'); //APerf.First(); asm = Assembly.Load(bAsm, bPdb); #endif //APerf.Next('A'); //APerf.NW(); //without AV 7 ms. With Windows Defender 10 ms, but first time 20-900 ms. if (findLoaded) { lsa.Add(asmFile, asm); } //never mind: it's possible that we load a newer compiled assembly version of script than intended. } try { var entryPoint = asm.EntryPoint ?? throw new InvalidOperationException("assembly without entry point (function Main)"); bool useArgs = entryPoint.GetParameters().Length != 0; if (useArgs) { if (args == null) { args = Array.Empty <string>(); } } //APerf.Next('1'); if (!inEditorThread) { Util.Log_.Run.Write("Task started."); } //APerf.Next('2'); if (useArgs) { entryPoint.Invoke(null, new object[] { args }); } else { entryPoint.Invoke(null, null); } if (!inEditorThread) { Util.Log_.Run.Write("Task ended."); } } catch (TargetInvocationException te) { var e = te.InnerException; if (!inEditorThread) { Util.Log_.Run.Write($"Unhandled exception: {e.ToStringWithoutStack()}"); } if (0 != (flags & RAFlags.DontHandleExceptions)) { throw e; } //AOutput.Write(e); AScript.OnHostHandledException(new UnhandledExceptionEventArgs(e, false)); } //see also: TaskScheduler.UnobservedTaskException event. // tested: the event works. // tested: somehow does not terminate process even with <ThrowUnobservedTaskExceptions enabled="true"/>. // Only when ADialog.Show called, the GC.Collect makes it to disappear but the process does not exit. // note: the terminating behavior also can be set in registry or env var. It overrides <ThrowUnobservedTaskExceptions enabled="false"/>. }