using Microsoft.JScript.Vsa;
using System.Reflection;
using System.Linq;
using System;
using System.Collections.Generic;
using Microsoft.JScript;
using System.CodeDom.Compiler;
using System.Text.RegularExpressions;
// Following pragma is to remove the warnings over the use of obsolete Microsoft.Vsa
#pragma warning disable 612
namespace Scaredfinger.JSShell
{
/// <summary>
/// Defines a base for shells
/// </summary>
public interface IShell : IDisposable
{
/// <summary>
/// Gets whether the shell is running
/// </summary>
bool Running { get; }
/// <summary>
/// Adds a variable to the global scope
/// </summary>
/// <param name="name">Variable name</param>
/// <param name="value">Variable's value</param>
void AddGlobalObject(string name, object value);
/// <summary>
/// Evaluates a java script line and return the result
/// </summary>
/// <param name="textline">Javascript block to execute. block : js expression (; js expression)*</param>
/// <returns>block output, if any</returns>
object Eval(string textline);
/// <summary>
/// Creates an Event Handler from a specified function body. This handler would work as an instance method of the shell. This means
/// withing its body, "this" would refer to the shell's scope. Sender and event args are available as "sender" and "e" respectively.
/// </summary>
/// <typeparam name="TEventArgs">Event args type, to create a strongly typed handler</typeparam>
/// <param name="functionBody">Java script function body, just the body, no declaration nor curlies</param>
/// <returns>Delegate instance</returns>
EventHandler<TEventArgs> CreateEventHandler<TEventArgs>(string functionBody)
where TEventArgs : EventArgs;
/// <summary>
/// Opens this shell. Starts running it, readies it for work
/// </summary>
void OpenShell();
/// <summary>
/// Closes this shell. No more operations are posible.
/// </summary>
void CloseShell();
}
/// <summary>
/// Provides a basic implementation using existing VSA elements.
/// </summary>
/// <remarks>
/// Many features of <see cref="Microsoft.JScript"/> relay on the use of a <see cref="Microsoft.Vsa.IVsaEngine"/>, which is
/// obsolete, but not yet replaced; it is also obscure (very), poorly documented, dificult to use and the worst: inextensible, too many
/// internals, some of them abstract.
///
/// For all these reassons my shells are not suposed to be created directly but instead using a <see cref="IShellFactory{TShell}"/>.
///
/// To Create a new Shell:
/// <list type="numbered">
/// <item>Create a class that extends shell</item>
/// <item>Mark the methods you'd like to make available from the shell with <see cref="ShellOperationAttribute"/></item>
/// <item>Create a <see cref="ShellFactory{TShell}"/></item>
/// <item>Create a shell instance, <see cref="ShellFactory{TShell}.CreateShell()"/></item>
/// </list>
///
/// Javascript naming convention is the same as Java (Camelcase with lower start letter). I prefer Pascal (Upper Camelcase) case,
/// so my examples would be Pascal case.
///
/// Sample implementation:
/// <code>
///
///namespace SampleShell
///{
/// public class SampleShell : Shell
/// {
/// public TextWriter Output
/// {
/// get;
/// set;
/// }
///
/// public SampleShell()
/// : this(Console.Out)
/// {
/// }
///
/// public SampleShell(TextWriter output)
/// {
/// Output = output;
/// }
///
/// [ShellOperation]
/// [Description("Shows this help screen")]
/// public void Help()
/// {
/// foreach (var method in GetType().GetMethods().Where(x => x.GetCustomAttributes(typeof(ShellOperationAttribute), false).Any()))
/// {
/// var attr = method.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute;
///
/// var parameters = string.Join(",",
/// (
/// from p in method.GetParameters()
/// select string.Format("{0} {1}", p.ParameterType.Name, p.Name)
/// ).ToArray()
/// );
///
/// var text = string.Format("\t{0} {1}({2}):\t{3}", method.ReturnType.Name, method.Name, parameters,
/// attr == null ? "" : attr.Description);
///
/// Print(text);
/// }
/// }
///
/// [ShellOperation]
/// [Description("Prints a text")]
/// public void Print(string text)
/// {
/// Output.WriteLine(text);
/// }
///
/// [ShellOperation]
/// [Description("Closes this shell")]
/// public void Exit()
/// {
/// CloseShell();
/// }
/// }
///}
/// </code>
/// </remarks>
public class Shell : IShell
{
#region Constants
/// <summary>
/// String pattern for handler functions
/// </summary>
const string StrEventHandlerBody = @"
var __anonymous_Handler = function(sender, e)
{{
{0};
}}
";
#endregion
#region Fields
/// <summary>
/// Scripting engine. Behind this reference is the power of the dark side...
/// </summary>
VsaEngine Engine
{
get;
set;
}
/// <summary>
/// Represents a private scope which known this instance
/// </summary>
private ShellScope Scope
{
get;
set;
}
/// <summary>
/// Gets or sets whether the shell is closed
/// </summary>
private bool Closed
{
get;
set;
}
#endregion
#region IShell Members
/// <inheritdoc/>
public bool Running
{
get;
private set;
}
/// <inheritdoc/>
public void AddGlobalObject(string name, object value)
{
if (value == null)
throw new ArgumentNullException("value");
if (string.IsNullOrEmpty(name))
throw new ArgumentException("'name' cannot be null nor empty");
if (Scope.HasField(name))
throw new DuplicatedGlobalObjectException(name);
// Adding global object to scope
Scope.CreateField(name, value);
}
/// <inheritdoc/>
public object Eval(string source)
{
if (!Running)
throw new ShellNotRunningException();
// Overriding default scope with a ShellScope
// This ways shell functions and user defined global objects
// are available from the shell
Engine.PushScriptObject(Scope);
try
{
// Executing the evaluate
return Microsoft.JScript.Eval.JScriptEvaluate(source, Engine);
}
finally
{
// Restoring default scope
Engine.PopScriptObject();
}
}
/// <inheritdoc/>
public virtual EventHandler<TEventArgs> CreateEventHandler<TEventArgs>(string functionBody)
where TEventArgs : EventArgs
{
// Compile the function
var function = Eval(string.Format(StrEventHandlerBody, functionBody)) as ScriptFunction;
// Check if everything went ok
if (function == null)
throw new BadHandlerException();
// Create the delegate
return (sender, e) => function.Invoke(Scope, new[] {sender, e});
}
/// <inheritdoc/>
public void OpenShell()
{
if (Running)
throw new ShellAlreadyRunningException();
// Gets the Vsa Engine. Remember, this is the power of the dark side....
var getEngine = GetType().GetMethod("GetEngine", BindingFlags.NonPublic | BindingFlags.Instance);
Engine = (VsaEngine)getEngine.Invoke(this, new object[0]);
// Creates a scope to override the global scope
Scope = new ShellScope(this, Engine.GetMainScope());
Running = true;
}
/// <inheritdoc/>
public void CloseShell()
{
Running = false;
if (Closed)
return;
Closed = true;
}
#endregion
#region IDisposable Members
/// <inheritdoc/>
public void Dispose()
{
CloseShell();
// Closing Darth Engine
Engine.Close();
}
#endregion
}
/// <summary>
/// A Block scope with reference to the shell object.
/// </summary>
/// <remarks>
/// This scope would override global scope and make available all shell functions and defined global objects. For each function in
/// <see cref="Target"/> shell, there will be a variable with the same name in the scope. Global variables would be instance or static
/// fields in the Scope object. <see cref="CreateField"/>
///
/// Let's see following example:
///
/// Shell definition:
/// <code>
/// class MyShell : Shell
/// {
/// public void Foo() {...}
/// public int Goo(int x) {...}
/// }
/// </code>
///
/// From the shell it would be like if have done:
/// <code>
/// var __this = new MyShell();
/// var Foo = __this.Foo ;
/// var Goo = __this.Goo ;
/// </code>
///
/// And using it would be completely transparent:
/// <code>
/// > Foo()
/// // What ever MyShell::Foo() might output
/// > Goo(1); Goo(2); // And so
/// // What ever ...you know
/// </code>
/// </remarks>
internal class ShellScope : BlockScope
{
#region Static Tools
static readonly Regex RexGuidCleaner = new Regex("[{}-]", RegexOptions.Compiled);
static readonly Random RndIdGenerator = new Random();
static string CreateScopeName()
{
return RexGuidCleaner.Replace(Guid.NewGuid().ToString(), "_");
}
static int CreateScopeId()
{
return RndIdGenerator.Next(100, int.MaxValue);
}
const string StrFunctionCallFormat = "{0}.{1};";
#endregion
#region Fields
/// <summary>
/// Keeps a reference to the shell
/// </summary>
private IShell Target
{
get;
set;
}
/// <summary>
/// Holds the name of current scope
/// </summary>
private string ScopeName
{
get;
set;
}
/// <summary>
/// Numeric id for current scope
/// </summary>
public int ScopeId
{
get;
set;
}
/// <summary>
/// To avoid any posible thinkable name clash
/// </summary>
private string ShellReference
{
get { return "__this" + ScopeName; }
}
/// <summary>
/// Keeps track of scope variables
/// </summary>
private readonly Dictionary<string, FieldInfo> _dicMembers = new Dictionary<string, FieldInfo>();
#endregion
#region Creation && Initialization
/// <summary>
/// A completely flexible constructor
/// </summary>
/// <param name="shell">The shell execuing code</param>
/// <param name="parent">The parent Vsa scope, currently the global scope</param>
/// <param name="scopeName">A name for he scope</param>
/// <param name="scopeId">An int id for the scope</param>
ShellScope(IShell shell, ScriptObject parent, string scopeName, int scopeId)
: base(parent, scopeName, scopeId)
{
ScopeName = scopeName;
ScopeId = scopeId;
Target = shell;
CreateFields();
}
/// <summary>
/// Creates a new scope
/// </summary>
/// <param name="shell">The shell execuing code</param>
/// <param name="parent">The parent Vsa scope, currently the global scope</param>
public ShellScope(IShell shell, ScriptObject parent)
: this(shell, parent, CreateScopeName(), CreateScopeId())
{
}
/// <summary>
/// Creates a field for each <see cref="ShellOperationAttribute">shell operation</see>.
/// </summary>
private void CreateFields()
{
// Creating shell reference var
CreateField(ShellReference, FieldAttributes.Public, Target);
// Creating a field for each function
foreach (var member in Target.GetType().GetMethods().Where(x => x.GetCustomAttributes(typeof(ShellOperationAttribute), false).Any()))
CreateField(member.Name, FieldAttributes.Public, CreateFunction(member));
}
/// <summary>
/// Creates a scriptfunction reference to assign to variables named after functions
/// </summary>
/// <param name="member">Function</param>
/// <returns>ScriptFunction</returns>
private ScriptFunction CreateFunction(MethodInfo member)
{
try
{
// Pushing current scope in the top
// So shell reference is available
engine.PushScriptObject(this);
// Creating the ScriptFunction Object
return (ScriptFunction)Eval.JScriptEvaluate(
string.Format(StrFunctionCallFormat, ShellReference, member.Name), engine);
}
finally
{
// Restoring current scope
engine.PopScriptObject();
}
}
#endregion
#region Public Members
/// <summary>
/// Queries the existance of specified field
/// </summary>
/// <param name="name">Field name</param>
/// <returns>true if it does, false if it doesn't</returns>
public bool HasField(string name)
{
return _dicMembers.ContainsKey(name);
}
/// <summary>
/// Creates a new field, a global var for instance
/// </summary>
/// <param name="name">Field name</param>
/// <param name="value">Field value</param>
public void CreateField(string name, object value)
{
CreateField(name, FieldAttributes.Public, value);
}
#endregion
#region Overrides
/// <summary>
/// Creates a new "global" variable
/// </summary>
/// <param name="name">Variable name</param>
/// <param name="attributeFlags">Field attributes for variable creation</param>
/// <param name="value">Initial value</param>
/// <returns>The reflection object for this variable</returns>
protected override JSVariableField CreateField(string name, FieldAttributes attributeFlags, object value)
{
if (_dicMembers.ContainsKey(name))
throw new DuplicatedGlobalObjectException(name);
var result = base.CreateField(name, attributeFlags, value);
_dicMembers.Add(name, result);
return result;
}
/// <summary>
/// Finds matching scope member. Overrides default behaviour returning members from internal dicionary
/// </summary>
/// <param name="name">Member name</param>
/// <param name="bindingAttr">Attributes to refine search</param>
/// <returns>Found member.</returns>
public override MemberInfo[] GetMember(string name, BindingFlags bindingAttr)
{
return _dicMembers.ContainsKey(name) ? new MemberInfo[] { _dicMembers[name] } : base.GetMember(name, bindingAttr);
}
#endregion
}
/// <summary>
/// Base interface for generating Shells
/// </summary>
/// <typeparam name="TShell"></typeparam>
public interface IShellFactory<TShell>
where TShell : class, IShell, new()
{
void AddReference(Assembly assembly);
void AddImport(string import);
TShell CreateShell();
}
/// <summary>
///
/// </summary>
/// <typeparam name="TShell"></typeparam>
/// <remarks>
/// This implementation would create a JScript.NET class derived from <see cref="TShell"/>, then it would compile it using
/// <see cref="JScriptCodeProvider"/>. Resulting assembly would be able to many <see cref="Microsoft.JScript"/> features, including
/// the obscure ones.
/// <code>
/// var factory = new ShellFactory<MyShell>() ;
///
/// factory.AddReference(system) ; // Creates a reference to assembly System
/// factory.AddImport("System"); // Allows using System types from the shell
/// factory.AddImport("System.Reflection") ; // Allows using System.Reflection types from the shell
/// </code>
/// </remarks>
public class ShellFactory<TShell> : IShellFactory<TShell>
where TShell : class, IShell, new()
{
#region Constants
/// <summary>
/// Javascript import format string
/// </summary>
const string StrImport = "import {0};";
/// <summary>
/// Shell class format string
/// </summary>
const string StrShell = @"
{2}
import {1};
import System;
class __ShellImpl extends {1}.{0}
{{
}}
";
static readonly Regex RexImportValidator = new Regex("^[a-zA-Z_]\\w*(\\.[a-zA-Z_]\\w*)*$", RegexOptions.Compiled);
/// <summary>
/// Gets a compiler info to create default compiler parameters later
/// </summary>
/// <seealso cref="System.CodeDom"/>
/// <seealso cref="CodeDomProvider"/>
/// <seealso cref="CompilerParameters"/>
static readonly CompilerInfo JsCompilerInfo = CodeDomProvider.GetCompilerInfo("JScript");
/// <summary>
/// Creates a JS
/// </summary>
static readonly JScriptCodeProvider JsCompiler = new JScriptCodeProvider();
/// <summary>
/// Holds a Type instance for shell type
/// </summary>
static readonly Type ShellType = typeof(TShell);
/// <summary>
/// Holds a default (shared) factory instance. Might be useull when creating Shell instances from diferent scopes, this way
/// you don't need to "save" a shared instance on your own. But be aware, changes on a factory affect al Shells created with
/// it.
/// </summary>
public static ShellFactory<TShell> Default = new ShellFactory<TShell>();
#endregion
readonly List<Assembly> _lstReferences = new List<Assembly>();
public void AddReference(Assembly reference)
{
if (reference == null)
throw new ArgumentNullException("reference");
_lstReferences.Add(reference);
}
readonly List<string> _lstImports = new List<string>();
public void AddImport(string import)
{
if (! RexImportValidator.Match(import).Success)
throw new ImportFormatException(import);
_lstImports.Add(import);
}
public TShell CreateShell()
{
// Creating default parameters for the JS crompiler
var options = JsCompilerInfo.CreateDefaultCompilerParameters();
// Adding referenced assemblies to compiler parameters
options.ReferencedAssemblies.AddRange(
(from x in _lstReferences
select x.Location).ToArray()
);
// These 2 will always be referenced
options.ReferencedAssemblies.Add(ShellType.Assembly.Location);
options.ReferencedAssemblies.Add("mscorlib");
// Creating imports text
// import System ;
// import System.Reflection;
// stuff like that
var imports = string.Join("\r\n", (from import in _lstImports
select string.Format(StrImport, import)).ToArray());
// Formats the actual shell class
var source = string.Format(StrShell, ShellType.Name, ShellType.Namespace, imports);
// Compiles the shell class
var result = JsCompiler.CompileAssemblyFromSource(options, source);
// There should be no compilation errors
if (result.Errors.Count > 0)
throw new CriticalException("Something has gone real wrong!!");
return (TShell)Activator.CreateInstance(result.CompiledAssembly.GetType("__ShellImpl"));
}
}
/// <summary>
/// To mark the methods available from the shell
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class ShellOperationAttribute : Attribute
{
}
}
#pragma warning restore 612