/// <summary> /// Initializes the script domain inside its application domain. /// </summary> /// <param name="apiBasePath">The path to the root directory containing the scripting API assemblies.</param> private ScriptDomain(string apiBasePath) { // Each application domain has its own copy of this static variable, so only need to set it once CurrentDomain = this; // Attach resolve handler to new domain AppDomain.AssemblyResolve += HandleResolve; AppDomain.UnhandledException += HandleUnhandledException; // Load API assemblies into this script domain foreach (string apiPath in Directory.EnumerateFiles(apiBasePath, "ScriptHookRDRNetAPI.dll", SearchOption.TopDirectoryOnly)) { Log.Message(Log.Level.Debug, "Loading API from ", apiPath, " ..."); try { scriptApis.Add(Assembly.LoadFrom(apiPath)); Log.Message(Log.Level.Debug, "API from ", apiPath, " loaded", " ..."); } catch (Exception ex) { Log.Message(Log.Level.Error, "Unable to load ", Path.GetFileName(apiPath), ": ", ex.ToString()); } } }
/// <summary> /// The main execution logic of all scripts. /// </summary> void MainLoop() { IsRunning = true; // Wait for script domain to continue this script continueEvent.Wait(); while (IsRunning) { // Process keyboard events while (keyboardEvents.TryDequeue(out Tuple <bool, KeyEventArgs> ev)) { try { if (!ev.Item1) { KeyUp?.Invoke(this, ev.Item2); } else { KeyDown?.Invoke(this, ev.Item2); } } catch (ThreadAbortException) { // Stop main loop immediately on a thread abort exception return; } catch (Exception ex) { ScriptDomain.HandleUnhandledException(this, new UnhandledExceptionEventArgs(ex, false)); break; // Break out of key event loop, but continue to run script } } try { Tick?.Invoke(this, EventArgs.Empty); } catch (ThreadAbortException) { // Stop main loop immediately on a thread abort exception return; } catch (Exception ex) { ScriptDomain.HandleUnhandledException(this, new UnhandledExceptionEventArgs(ex, true)); // An exception during tick is fatal, so abort the script and stop main loop Abort(); return; } // Yield execution to next tick Wait(Interval); } }
/// <summary> /// Unloads scripts and destroys an existing script domain. /// </summary> /// <param name="domain">The script domain to unload.</param> public static void Unload(ScriptDomain domain) { Log.Message(Log.Level.Info, "Unloading script domain ..."); domain.Abort(); domain.Dispose(); try { AppDomain.Unload(domain.AppDomain); } catch (Exception ex) { Log.Message(Log.Level.Error, "Failed to unload script domain: ", ex.ToString()); } }
/// <summary> /// Creates a new script domain. /// </summary> /// <param name="basePath">The path to the application root directory.</param> /// <param name="scriptPath">The path to the directory containing scripts.</param> /// <returns>The script domain or <c>null</c> in case of failure.</returns> public static ScriptDomain Load(string basePath, string scriptPath) { string _scriptPath; // Make absolute path to scrips location //if (!Path.IsPathRooted(scriptPath)) _scriptPath = Path.GetFullPath(Path.Combine(basePath, scriptPath)); // Create application and script domain for all the scripts to reside in var name = "ScriptDomain_" + (_scriptPath.GetHashCode() ^ Environment.TickCount).ToString("X"); var setup = new AppDomainSetup(); setup.ApplicationBase = _scriptPath; setup.ShadowCopyFiles = "true"; // Copy assemblies into memory rather than locking the file, so they can be updated while the domain is still loaded setup.ShadowCopyDirectories = _scriptPath; // Only shadow copy files in the scripts directory var appdomain = AppDomain.CreateDomain(name, null, setup, new System.Security.PermissionSet(System.Security.Permissions.PermissionState.Unrestricted)); appdomain.SetCachePath(Path.GetTempPath()); appdomain.SetShadowCopyFiles(); appdomain.SetShadowCopyPath(_scriptPath); appdomain.InitializeLifetimeService(); // Give the application domain an infinite lifetime // Need to attach the resolve handler to the current domain too, so that the .NET framework finds this assembly in the ASI file AppDomain.CurrentDomain.AssemblyResolve += HandleResolve; ScriptDomain scriptdomain = null; try { scriptdomain = (ScriptDomain)appdomain.CreateInstanceFromAndUnwrap(typeof(ScriptDomain).Assembly.Location, typeof(ScriptDomain).FullName, false, BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { basePath }, null, null); //Log.Message(Log.Level.Debug, "Script domain created: ", "Name: ", name, " FullName: ", typeof(ScriptDomain).FullName, " Assembly Location: ", typeof(ScriptDomain).Assembly.Location); } catch (Exception ex) { Log.Message(Log.Level.Error, "Failed to create script domain: ", ex.ToString(), " ", name, " ", typeof(ScriptDomain).FullName, " ", typeof(ScriptDomain).Assembly.Location); AppDomain.Unload(appdomain); } // Remove resolve handler again AppDomain.CurrentDomain.AssemblyResolve -= HandleResolve; Log.Message(Log.Level.Debug, "Resolve handler removed"); return(scriptdomain); }
/// <summary> /// Aborts execution of this script. /// </summary> public void Abort() { IsRunning = false; try { Aborted?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { ScriptDomain.HandleUnhandledException(this, new UnhandledExceptionEventArgs(ex, true)); } waitEvent.Release(); if (thread != null) { Log.Message(Log.Level.Warning, "Aborted script ", Name, "."); thread.Abort(); thread = null; } }