Skip to content

Instantly share code, notes, and snippets.

@DevJohnC
Created January 15, 2014 16:52
Show Gist options
  • Select an option

  • Save DevJohnC/8439908 to your computer and use it in GitHub Desktop.

Select an option

Save DevJohnC/8439908 to your computer and use it in GitHub Desktop.
//
// 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