/// <summary> /// Aborts execution of this script. /// </summary> public void Abort() { IsRunning = false; waitEvent.Release(); try { Aborted?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { ScriptDomain.HandleUnhandledException(this, new UnhandledExceptionEventArgs(ex, true)); } if (thread != null) { thread.Abort(); thread = null; Log.Message(Log.Level.Info, "Aborted script ", Name, "."); } // Unregister any console commands attached to this script var console = AppDomain.CurrentDomain.GetData("Console") as Console; console?.UnregisterCommands(ScriptInstance.GetType()); }
/// <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, "ScriptHookVDotNet*.dll", SearchOption.TopDirectoryOnly)) { Log.Message(Log.Level.Debug, "Loading API from ", apiPath, " ..."); try { var assembly = Assembly.LoadFrom(apiPath); scriptApis.Add(assembly); // set the default script api to use. if (assembly.GetName().Version.Major == 2) { Log.Message(Log.Level.Info, "Found default script api: ", assembly.GetName().FullName); defaultScriptApi = assembly; } } 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) { // Make absolute path to scrips location if (!Path.IsPathRooted(scriptPath)) { scriptPath = Path.Combine(Path.GetDirectoryName(basePath), scriptPath); } scriptPath = Path.GetFullPath(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.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 setup.ApplicationBase = scriptPath; var appdomain = AppDomain.CreateDomain(name, null, setup, new System.Security.PermissionSet(System.Security.Permissions.PermissionState.Unrestricted)); 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 { var loaded = appdomain.CreateInstanceFromAndUnwrap(typeof(ScriptDomain).Assembly.Location, typeof(ScriptDomain).FullName, false, BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { basePath }, null, null); Log.Message(Log.Level.Info, "Loaded: ", loaded.GetType().ToString()); scriptdomain = (ScriptDomain)loaded; } catch (Exception ex) { Log.Message(Log.Level.Error, "Failed to create script domain: ", ex.ToString(), ex.StackTrace); AppDomain.Unload(appdomain); } // Remove resolve handler again AppDomain.CurrentDomain.AssemblyResolve -= HandleResolve; 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; } }