Created
January 15, 2014 16:52
-
-
Save DevJohnC/8439908 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // Author: John Carruthers (johnc@frag-labs.com) | |
| // | |
| // Copyright (C) 2013 John Carruthers | |
| // | |
| // Permission is hereby granted, free of charge, to any person obtaining | |
| // a copy of this software and associated documentation files (the | |
| // "Software"), to deal in the Software without restriction, including | |
| // without limitation the rights to use, copy, modify, merge, publish, | |
| // distribute, sublicense, and/or sell copies of the Software, and to | |
| // permit persons to whom the Software is furnished to do so, subject to | |
| // the following conditions: | |
| // | |
| // The above copyright notice and this permission notice shall be | |
| // included in all copies or substantial portions of the Software. | |
| // | |
| // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
| // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
| // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
| // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
| // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
| // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| // | |
| #if !ANDROID | |
| using System; | |
| using System.CodeDom.Compiler; | |
| using System.Collections.Generic; | |
| using System.IO; | |
| using System.Linq; | |
| using System.Reflection; | |
| using FragLabs.AdjutantOS.API.Apps; | |
| using FragLabs.AdjutantOS.API.Jobs; | |
| using FragLabs.AdjutantOS.API.Log; | |
| using FragLabs.AdjutantOS.API.Modules.Domains; | |
| using FragLabs.AdjutantOS.API.Services; | |
| namespace FragLabs.AdjutantOS.API.Modules | |
| { | |
| /// <summary> | |
| /// Module loader that loads scripts from a scripts directory containing .cs or .vb files. | |
| /// If a file changes it will be reloaded. | |
| /// </summary> | |
| public class ScriptModuleLoader : IModuleLoader | |
| { | |
| /// <summary> | |
| /// Directory to search for scripts. | |
| /// </summary> | |
| public string Directory { get; protected set; } | |
| /// <summary> | |
| /// Create a new instance of ScriptModuleLoader. | |
| /// </summary> | |
| /// <param name="directory">Directory to search for scripts.</param> | |
| public ScriptModuleLoader(string directory) | |
| { | |
| Directory = directory; | |
| JobScheduler.Current.Add(new ScriptMonitorJob(this)); | |
| } | |
| /// <summary> | |
| /// Gets an array of available modules. | |
| /// </summary> | |
| public IEnumerable<Module> AvailableModules | |
| { | |
| get | |
| { | |
| var dir = new DirectoryInfo(Directory); | |
| if (dir.Exists) | |
| { | |
| foreach (var file in dir.EnumerateFiles()) | |
| { | |
| if (file.Name.EndsWith(".cs")) | |
| yield return new ScriptModule(file.FullName, ScriptLanguage.CSharp, file.Name, this); | |
| if (file.Name.EndsWith(".vb")) | |
| yield return new ScriptModule(file.FullName, ScriptLanguage.VisualBasic, file.Name, this); | |
| } | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Load a module. | |
| /// </summary> | |
| /// <param name="module">Module to load.</param> | |
| public void Load(Module module) | |
| { | |
| if (module == null) throw new ArgumentNullException("module"); | |
| if (module.Loader == null) throw new ArgumentException("Module load not set to an instance of an object"); | |
| if (!(module is ScriptModule)) throw new ArgumentException("Module must be a ScriptModule"); | |
| var scriptModule = module as ScriptModule; | |
| Logger.Write(LogLevel.Debug, "Creating appdomain for new module"); | |
| var fi = new FileInfo(scriptModule.Filename); | |
| var path = fi.Directory.FullName; | |
| var niceName = fi.Name; | |
| if (niceName.EndsWith(fi.Extension)) | |
| niceName = niceName.Substring(0, niceName.Length - fi.Extension.Length); | |
| var evidence = new System.Security.Policy.Evidence(AppDomain.CurrentDomain.Evidence); | |
| var setup = AppDomain.CurrentDomain.SetupInformation; | |
| setup.PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory; | |
| setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; | |
| var appDomain = AppDomain.CreateDomain("Module::" + niceName, evidence, setup); | |
| var remoteController = (DomainController)appDomain.CreateInstanceAndUnwrap(typeof(DomainController).Assembly.FullName, typeof(DomainController).FullName, | |
| true, BindingFlags.Default, null, new object[] { DomainManager.Current }, null, null); | |
| remoteController.Initialize(DomainManager.Current); | |
| module.DomainController = remoteController; | |
| var compiler = DomainController.Current.CreateMarshalledReference(remoteController, typeof(ScriptCompiler).GetDomainType()) as ScriptCompiler; | |
| Logger.Write(LogLevel.Debug, "Compiling script to module: {0}", scriptModule.Name); | |
| compiler.CompileAndAddAssembly(File.ReadAllText(scriptModule.Filename), scriptModule.Language); | |
| Logger.Write(LogLevel.Debug, "Installing apps in module \"{0}\"", module.Name); | |
| var apps = remoteController.FindTypesThatImplement(typeof(IApp)); | |
| AppHost.Current.Add(apps); | |
| Logger.Write(LogLevel.Debug, "Installing services in module \"{0}\"", module.Name); | |
| var services = remoteController.FindTypesThatImplement(typeof(IService<>)); | |
| ServiceHost.Current.Add(services); | |
| } | |
| } | |
| public class ScriptCompiler : DomainShareObject | |
| { | |
| private static string[] _references = new string[] | |
| { | |
| "System.dll", | |
| "API.dll", | |
| "libHTTP.dll", | |
| "Network.dll", | |
| "ServiceStack.Text.dll", | |
| "System.Core.dll", | |
| "System.Web.dll", | |
| "System.Xml.dll", | |
| "System.Xml.Linq.dll", | |
| }; | |
| public void CompileAndAddAssembly(string sourceCode, ScriptLanguage language) | |
| { | |
| try | |
| { | |
| var asm = Compile(sourceCode, language); | |
| DomainController.Current.AddAssembly(asm); | |
| foreach (var type in asm.GetTypes()) | |
| { | |
| if (type.Name == "Script") | |
| { | |
| var method = type.GetMethod("Run"); | |
| var instance = Activator.CreateInstance(type); | |
| method.Invoke(instance, new object[0] { }); | |
| } | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.Write(LogLevel.Warning, "Failed to compile script: {0}", ex); | |
| } | |
| } | |
| private Assembly Compile(string sourceCode, ScriptLanguage language) | |
| { | |
| var provider = CodeDomProvider.CreateProvider(language.ToString()); | |
| var cp = new CompilerParameters | |
| { | |
| GenerateInMemory = true, | |
| GenerateExecutable = false, | |
| TreatWarningsAsErrors = false | |
| }; | |
| cp.ReferencedAssemblies.AddRange(_references); | |
| var cr = provider.CompileAssemblyFromSource(cp, sourceCode); | |
| if (cr.Errors.Count == 0) | |
| return cr.CompiledAssembly; | |
| Logger.Write(LogLevel.Debug, "Script compilation error"); | |
| foreach(var error in cr.Errors) | |
| Logger.Write(LogLevel.Debug, error.ToString()); | |
| throw new Exception("Failed to compile script"); | |
| } | |
| } | |
| /// <summary> | |
| /// A script module. | |
| /// </summary> | |
| public class ScriptModule : Module | |
| { | |
| /// <summary> | |
| /// Script filename. | |
| /// </summary> | |
| public string Filename { get; private set; } | |
| /// <summary> | |
| /// Modified time. | |
| /// </summary> | |
| public DateTime ModifiedTime { get; private set; } | |
| /// <summary> | |
| /// Script language. | |
| /// </summary> | |
| public ScriptLanguage Language { get; private set; } | |
| public ScriptModule(string filename, ScriptLanguage language, string name, IModuleLoader loader) : base(name, loader) | |
| { | |
| if (filename == null) throw new ArgumentNullException("filename"); | |
| Filename = filename; | |
| Language = language; | |
| var info = new FileInfo(filename); | |
| info.Refresh(); | |
| ModifiedTime = info.LastWriteTime; | |
| } | |
| } | |
| /// <summary> | |
| /// Scripting language. | |
| /// </summary> | |
| public enum ScriptLanguage | |
| { | |
| /// <summary> | |
| /// Visual Basic .NET | |
| /// </summary> | |
| VisualBasic, | |
| /// <summary> | |
| /// C# | |
| /// </summary> | |
| CSharp | |
| } | |
| /// <summary> | |
| /// Job to monitor when scripts are updated. | |
| /// </summary> | |
| public class ScriptMonitorJob : DomainShareObject, IScheduledJob | |
| { | |
| /// <summary> | |
| /// Script loader. | |
| /// </summary> | |
| private readonly ScriptModuleLoader _loader; | |
| /// <summary> | |
| /// Maintained list of scripts. | |
| /// </summary> | |
| private ScriptModule[] _scripts; | |
| public ScriptMonitorJob(ScriptModuleLoader loader) | |
| { | |
| if (loader == null) throw new ArgumentNullException("loader"); | |
| _loader = loader; | |
| NextScheduledTime = DateTime.Now + TimeSpan.FromSeconds(2); | |
| Enabled = true; | |
| _scripts = _loader.AvailableModules.OfType<ScriptModule>().ToArray(); | |
| } | |
| /// <summary> | |
| /// Permission token, required if the job needs security clearance to make service calls etc. | |
| /// </summary> | |
| public JobPermissionToken PermissionToken { get; set; } | |
| /// <summary> | |
| /// Gets the next time the job is scheduled to be executed. | |
| /// </summary> | |
| public DateTime NextScheduledTime { get; private set; } | |
| /// <summary> | |
| /// Gets if the job is enabled. | |
| /// </summary> | |
| public bool Enabled { get; private set; } | |
| /// <summary> | |
| /// Runs the job. | |
| /// </summary> | |
| public void Run() | |
| { | |
| try | |
| { | |
| var compare = _loader.AvailableModules.OfType<ScriptModule>().ToArray(); | |
| foreach (var scriptModule in compare) | |
| { | |
| var comparison = _scripts.FirstOrDefault(tmp => tmp.Filename == scriptModule.Filename); | |
| if (comparison == null) | |
| { | |
| Logger.Write(LogLevel.Debug, "Script {0} created, loading", new FileInfo(scriptModule.Filename).Name); | |
| ModuleManager.Current.Load(scriptModule); | |
| } | |
| else if (scriptModule.ModifiedTime > comparison.ModifiedTime) | |
| { | |
| Logger.Write(LogLevel.Debug, "Script {0} updated, reloading", new FileInfo(scriptModule.Filename).Name); | |
| ModuleManager.Current.Unload(FindInstalledModule(scriptModule)); | |
| ModuleManager.Current.Load(scriptModule); | |
| } | |
| } | |
| foreach (var scriptModule in _scripts) | |
| { | |
| var comparison = compare.FirstOrDefault(tmp => tmp.Filename == scriptModule.Filename); | |
| if (comparison == null) | |
| { | |
| Logger.Write(LogLevel.Debug, "Script {0} deleted, unloading", new FileInfo(scriptModule.Filename).Name); | |
| ModuleManager.Current.Unload(FindInstalledModule(scriptModule)); | |
| } | |
| } | |
| _scripts = compare; | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.Write(LogLevel.Warning, "Exception monitoring scripts: {0}", ex); | |
| } | |
| finally | |
| { | |
| NextScheduledTime = DateTime.Now + TimeSpan.FromSeconds(2); | |
| } | |
| } | |
| /// <summary> | |
| /// Finds the installed Module object that matches the given ScriptModule | |
| /// </summary> | |
| /// <param name="module"></param> | |
| /// <returns></returns> | |
| private Module FindInstalledModule(ScriptModule module) | |
| { | |
| return ModuleManager.Current.LoadedModules.OfType<ScriptModule>(). | |
| FirstOrDefault(scriptModule => scriptModule.Filename == module.Filename); | |
| } | |
| } | |
| } | |
| #endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment