diff --git a/src/mod/languages/mod_managed/managed/PluginInterfaces.cs b/src/mod/languages/mod_managed/managed/PluginInterfaces.cs new file mode 100644 index 0000000000..06d2a38951 --- /dev/null +++ b/src/mod/languages/mod_managed/managed/PluginInterfaces.cs @@ -0,0 +1,91 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application - mod_managed + * Copyright (C) 2008, Michael Giagnocavo + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application - mod_managed + * + * The Initial Developer of the Original Code is + * Michael Giagnocavo + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Michael Giagnocavo + * Jeff Lenk + * + * PluginInterfaces.cs -- Public interfaces for plugins + * + */ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FreeSWITCH { + + public class AppContext { + readonly string arguments; + readonly Native.ManagedSession session; + + public AppContext(string arguments, Native.ManagedSession session) { + this.arguments = arguments; + this.session = session; + } + + public string Arguments { get { return arguments; } } + public Native.ManagedSession Session { get { return session; } } + } + + public class ApiContext { + readonly string arguments; + readonly Native.Stream stream; + readonly Native.Event evt; + + public ApiContext(string arguments, Native.Stream stream, Native.Event evt) { + this.arguments = arguments; + this.stream = stream; + this.evt = evt; + } + + public string Arguments { get { return arguments; } } + public Native.Stream Stream { get { return stream; } } + public Native.Event Event { get { return evt; } } + } + + public class ApiBackgroundContext { + readonly string arguments; + + public ApiBackgroundContext(string arguments) { + this.arguments = arguments; + } + + public string Arguments { get { return arguments; } } + } + + public interface IApiPlugin { + void Execute(ApiContext context); + void ExecuteBackground(ApiBackgroundContext context); + } + + public interface IAppPlugin { + void Run(AppContext context); + } + + public interface ILoadNotificationPlugin { + bool Load(); + } + +} diff --git a/src/mod/languages/mod_managed/managed/PluginManager.cs b/src/mod/languages/mod_managed/managed/PluginManager.cs new file mode 100644 index 0000000000..c82d663fee --- /dev/null +++ b/src/mod/languages/mod_managed/managed/PluginManager.cs @@ -0,0 +1,291 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application - mod_managed + * Copyright (C) 2008, Michael Giagnocavo + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application - mod_managed + * + * The Initial Developer of the Original Code is + * Michael Giagnocavo + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Michael Giagnocavo + * Jeff Lenk + * + * PluginManager.cs -- Plugin execution code + * + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace FreeSWITCH { + + internal abstract class PluginExecutor : MarshalByRefObject { + public override object InitializeLifetimeService() { + return null; + } + + /// Names by which this plugin may be executed. + public List Aliases { get { return aliases; } } + readonly List aliases = new List(); + + /// The canonical name to identify this plugin (informative). + public string Name { get { return name; } } + readonly string name; + + protected PluginExecutor(string name, List aliases) { + if (string.IsNullOrEmpty(name)) throw new ArgumentException("No name provided."); + if (aliases == null || aliases.Count == 0) throw new ArgumentException("No aliases provided."); + this.name = name; + this.aliases = aliases.Distinct().ToList(); + } + + int useCount = 0; + protected void IncreaseUse() { + System.Threading.Interlocked.Increment(ref useCount); + } + protected void DecreaseUse() { + var count = System.Threading.Interlocked.Decrement(ref useCount); + if (count == 0 && onZeroUse != null) { + onZeroUse(); + } + } + + Action onZeroUse; + public void SetZeroUseNotification(Action onZeroUse) { + this.onZeroUse = onZeroUse; + if (useCount == 0) onZeroUse(); + } + + protected static void LogException(string action, string moduleName, Exception ex) { + Log.WriteLine(LogLevel.Error, "{0} exception in {1}: {2}", action, moduleName, ex.Message); + Log.WriteLine(LogLevel.Debug, "{0} exception: {1}", moduleName, ex.ToString()); + } + } + + internal sealed class AppPluginExecutor : PluginExecutor { + + readonly Func createPlugin; + + public AppPluginExecutor(string name, List aliases, Func creator) + : base(name, aliases) { + if (creator == null) throw new ArgumentNullException("Creator cannot be null."); + this.createPlugin = creator; + } + + public bool Execute(string args, IntPtr sessionHandle) { + IncreaseUse(); + try { + using (var session = new Native.ManagedSession(new Native.SWIGTYPE_p_switch_core_session(sessionHandle, false))) { + session.Initialize(); + session.SetAutoHangup(false); + try { + var plugin = createPlugin(); + var context = new AppContext(args, session);; + plugin.Run(context); + return true; + } catch (Exception ex) { + LogException("Run", Name, ex); + return false; + } + } + } finally { + DecreaseUse(); + } + } + } + + internal sealed class ApiPluginExecutor : PluginExecutor { + + readonly Func createPlugin; + + public ApiPluginExecutor(string name, List aliases, Func creator) + : base(name, aliases) { + if (creator == null) throw new ArgumentNullException("Creator cannot be null."); + this.createPlugin = creator; + } + + public bool ExecuteApi(string args, IntPtr streamHandle, IntPtr eventHandle) { + IncreaseUse(); + try { + using (var stream = new Native.Stream(new Native.switch_stream_handle(streamHandle, false))) + using (var evt = eventHandle == IntPtr.Zero ? null : new Native.Event(new Native.switch_event(eventHandle, false), 0)) { + try { + var context = new ApiContext(args, stream, evt); + var plugin = createPlugin(); + plugin.Execute(context); + return true; + } catch (Exception ex) { + LogException("Execute", Name, ex); + return false; + } + } + } finally { + DecreaseUse(); + } + } + + public bool ExecuteApiBackground(string args) { + // Background doesn't affect use count + new System.Threading.Thread(() => { + try { + var context = new ApiBackgroundContext(args); + var plugin = createPlugin(); + plugin.ExecuteBackground(context); + Log.WriteLine(LogLevel.Debug, "ExecuteBackground in {0} completed.", Name); + } catch (Exception ex) { + LogException("ExecuteBackground", Name, ex); + } + }).Start(); + return true; + } + } + + internal abstract class PluginManager : MarshalByRefObject { + public override object InitializeLifetimeService() { + return null; + } + + public List ApiExecutors { get { return _apiExecutors; } } + readonly List _apiExecutors = new List(); + + public List AppExecutors { get { return _appExecutors; } } + + readonly List _appExecutors = new List(); + + bool isLoaded = false; + + public bool Load(string file) { + Console.WriteLine("Loading {0} from domain {1}", file, AppDomain.CurrentDomain.FriendlyName); + if (isLoaded) throw new InvalidOperationException("PluginManager has already been loaded."); + if (string.IsNullOrEmpty(file)) throw new ArgumentNullException("file cannot be null or empty."); + if (AppDomain.CurrentDomain.IsDefaultAppDomain()) throw new InvalidOperationException("PluginManager must load in its own AppDomain."); + var res = LoadInternal(file); + isLoaded = true; + + res = res && AppExecutors.Count > 0 && ApiExecutors.Count > 0; + return res; + } + + protected abstract bool LoadInternal(string fileName); + + protected bool RunLoadNotify(Type[] allTypes) { + // Run Load on all the load plugins + var ty = typeof(ILoadNotificationPlugin); + var pluginTypes = allTypes.Where(x => ty.IsAssignableFrom(x) && !x.IsAbstract).ToList(); + if (pluginTypes.Count == 0) return true; + foreach (var pt in pluginTypes) { + var load = ((ILoadNotificationPlugin)Activator.CreateInstance(pt, false)); + if (!load.Load()) { + Log.WriteLine(LogLevel.Notice, "Type {0} requested no loading. Assembly will not be loaded.", pt.FullName); + return false; + } + } + return true; + } + + protected void AddApiPlugins(Type[] allTypes) { + var iApiTy = typeof(IApiPlugin); + foreach (var ty in allTypes.Where(x => iApiTy.IsAssignableFrom(x) && !x.IsAbstract)) { + var del = CreateConstructorDelegate(ty); + var exec = new ApiPluginExecutor(ty.FullName, new List { ty.FullName, ty.Name }, del); + this.ApiExecutors.Add(exec); + } + } + + protected void AddAppPlugins(Type[] allTypes) { + var iAppTy = typeof(IAppPlugin); + foreach (var ty in allTypes.Where(x => iAppTy.IsAssignableFrom(x) && !x.IsAbstract)) { + var del = CreateConstructorDelegate(ty); + var exec = new AppPluginExecutor(ty.FullName, new List { ty.FullName, ty.Name }, del); + this.AppExecutors.Add(exec); + } + } + + #region Unload + + bool isUnloading = false; + int unloadCount; + System.Threading.ManualResetEvent unloadSignal = new System.Threading.ManualResetEvent(false); + void decreaseUnloadCount() { + if (System.Threading.Interlocked.Decrement(ref unloadCount) == 0) { + unloadSignal.Set(); + } + } + + public void BlockUntilUnloadIsSafe() { + if (isUnloading) throw new InvalidOperationException("PluginManager is already unloading."); + isUnloading = true; + unloadCount = ApiExecutors.Count + AppExecutors.Count; + ApiExecutors.ForEach(x => x.SetZeroUseNotification(decreaseUnloadCount)); + AppExecutors.ForEach(x => x.SetZeroUseNotification(decreaseUnloadCount)); + unloadSignal.WaitOne(); + } + + #endregion + + public static Func CreateConstructorDelegate(Type ty) { + var destTy = typeof(T); + if (!destTy.IsAssignableFrom(ty)) throw new ArgumentException(string.Format("Type {0} is not assignable from {1}.", destTy.FullName, ty.FullName)); + var con = ty.GetConstructor(Type.EmptyTypes); + if (con == null) throw new ArgumentException(string.Format("Type {0} doesn't have an accessible parameterless constructor.", ty.FullName)); + + var rand = Guid.NewGuid().ToString().Replace("-", ""); + var dm = new DynamicMethod("CREATE_" + ty.FullName.Replace('.', '_') + rand, ty, null, true); + var il = dm.GetILGenerator(); + il.Emit(OpCodes.Newobj, ty.GetConstructor(Type.EmptyTypes)); + il.Emit(OpCodes.Ret); + + return (Func)dm.CreateDelegate(typeof(Func)); + } + } + + internal class AsmPluginManager : PluginManager { + + protected override bool LoadInternal(string fileName) { + Assembly asm; + try { + asm = Assembly.LoadFrom(fileName); + } catch (Exception ex) { + Log.WriteLine(LogLevel.Info, "Couldn't load {0}: {1}", fileName, ex.Message); + return false; + } + + // Ensure it's a plugin assembly + var ourName = Assembly.GetExecutingAssembly().GetName().Name; + if (!asm.GetReferencedAssemblies().Any(n => n.Name == ourName)) { + Log.WriteLine(LogLevel.Debug, "Assembly {0} doesn't reference FreeSWITCH.Managed, not loading."); + return false; + } + + // See if it wants to be loaded + var allTypes = asm.GetExportedTypes(); + if (!RunLoadNotify(allTypes)) return false; + + AddApiPlugins(allTypes); + AddAppPlugins(allTypes); + + return true; + } + + } + +} diff --git a/src/mod/languages/mod_managed/managed/ScriptPluginManager.cs b/src/mod/languages/mod_managed/managed/ScriptPluginManager.cs new file mode 100644 index 0000000000..7b0192d036 --- /dev/null +++ b/src/mod/languages/mod_managed/managed/ScriptPluginManager.cs @@ -0,0 +1,242 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application - mod_managed + * Copyright (C) 2008, Michael Giagnocavo + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application - mod_managed + * + * The Initial Developer of the Original Code is + * Michael Giagnocavo + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Michael Giagnocavo + * + * ScriptPluginManager.cs -- Dynamic compilation and script execution + * + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.IO; +using System.Reflection; +using System.Reflection.Emit; + + +namespace FreeSWITCH { + + public enum ScriptContextType { + App, + Api, + ApiBackground, + } + + public static class Script { + + [ThreadStatic] + internal static ScriptContextType contextType; + [ThreadStatic] + internal static object context; + + public static ScriptContextType ContextType { get { return contextType; } } + + public static ApiContext GetApiContext() { + return getContext(ScriptContextType.Api); + } + public static ApiBackgroundContext GetApiBackgroundContext() { + return getContext(ScriptContextType.ApiBackground); + } + public static AppContext GetAppContext() { + return getContext(ScriptContextType.App); + } + + public static T getContext(ScriptContextType sct) { + var ctx = context; + if (ctx == null) throw new InvalidOperationException("Current context is null."); + if (contextType != sct) throw new InvalidOperationException("Current ScriptContextType is not " + sct.ToString() + "."); + return (T)ctx; + } + + } + + internal class ScriptPluginManager : PluginManager { + + protected override bool LoadInternal(string fileName) { + Assembly asm; + if (Path.GetExtension(fileName).ToLowerInvariant() == ".exe") { + asm = Assembly.LoadFrom(fileName); + } else { + asm = compileAssembly(fileName); + } + if (asm == null) return false; + + return processAssembly(fileName, asm); + } + + Assembly compileAssembly(string fileName) { + var comp = new CompilerParameters(); + var mainRefs = new List { + Path.Combine(Native.freeswitch.SWITCH_GLOBAL_dirs.mod_dir, "FreeSWITCH.Managed.dll"), + "System.dll", "System.Xml.dll", "System.Data.dll" + }; + var extraRefs = new List { + "System.Core.dll", + "System.Xml.Linq.dll", + }; + comp.ReferencedAssemblies.AddRange(mainRefs.ToArray()); + comp.ReferencedAssemblies.AddRange(extraRefs.ToArray()); + CodeDomProvider cdp; + var ext = Path.GetExtension(fileName).ToLowerInvariant(); + switch (ext) { + case ".fsx": + cdp = CodeDomProvider.CreateProvider("f#"); + break; + case ".csx": + cdp = new Microsoft.CSharp.CSharpCodeProvider(new Dictionary { { "CompilerVersion", "v3.5" } }); + break; + case ".vbx": + cdp = new Microsoft.VisualBasic.VBCodeProvider(new Dictionary { { "CompilerVersion", "v3.5" } }); + break; + case ".jsx": + // Have to figure out better JS support + cdp = CodeDomProvider.CreateProvider("js"); + extraRefs.ForEach(comp.ReferencedAssemblies.Remove); + break; + default: + if (CodeDomProvider.IsDefinedExtension(ext)) { + cdp = CodeDomProvider.CreateProvider(CodeDomProvider.GetLanguageFromExtension(ext)); + } else { + return null; + } + break; + } + + comp.GenerateInMemory = true; + comp.GenerateExecutable = true; + + Log.WriteLine(LogLevel.Info, "Compiling {0}", fileName); + var res = cdp.CompileAssemblyFromFile(comp, fileName); + + var errors = res.Errors.Cast().Where(x => !x.IsWarning).ToList(); + if (errors.Count > 0) { + Log.WriteLine(LogLevel.Error, "There were {0} errors compiling {1}.", errors.Count, fileName); + foreach (var err in errors) { + if (string.IsNullOrEmpty(err.FileName)) { + Log.WriteLine(LogLevel.Error, "{0}: {1}", err.ErrorNumber, err.ErrorText); + } else { + Log.WriteLine(LogLevel.Error, "{0}: {1}:{2}:{3} {4}", err.ErrorNumber, err.FileName, err.Line, err.Column, err.ErrorText); + } + } + return null; + } + Log.WriteLine(LogLevel.Info, "File {0} compiled successfully.", fileName); + return res.CompiledAssembly; + } + + bool processAssembly(string fileName, Assembly asm) { + var allTypes = asm.GetExportedTypes(); + if (!RunLoadNotify(allTypes)) return false; + + // Scripts can specify classes too + AddApiPlugins(allTypes); + AddAppPlugins(allTypes); + + // Add the script executors + var entryPoint = getEntryDelegate(asm.EntryPoint); + var name = Path.GetFileName(fileName); + var aliases = new List { name }; + this.ApiExecutors.Add(new ApiPluginExecutor(name, aliases, () => new ScriptApiWrapper(entryPoint))); + this.AppExecutors.Add(new AppPluginExecutor(name, aliases, () => new ScriptAppWrapper(entryPoint))); + + return true; + } + + class ScriptApiWrapper : IApiPlugin { + + readonly Action entryPoint; + public ScriptApiWrapper(Action entryPoint) { + this.entryPoint = entryPoint; + } + + public void Execute(ApiContext context) { + Script.contextType = ScriptContextType.Api; + Script.context = context; + try { + entryPoint(); + } finally { + Script.context = null; + } + } + + public void ExecuteBackground(ApiBackgroundContext context) { + Script.contextType = ScriptContextType.ApiBackground; + Script.context = context; + try { + entryPoint(); + } finally { + Script.context = null; + } + } + + } + + class ScriptAppWrapper : IAppPlugin { + + readonly Action entryPoint; + public ScriptAppWrapper(Action entryPoint) { + this.entryPoint = entryPoint; + } + + public void Run(AppContext context) { + Script.contextType = ScriptContextType.App; + Script.context = context; + try { + entryPoint(); + } finally { + Script.context = null; + } + } + + } + + static Action getEntryDelegate(MethodInfo entryPoint) { + if (!entryPoint.IsPublic || !entryPoint.DeclaringType.IsPublic) { + Log.WriteLine(LogLevel.Error, "Entry point: {0}.{1} is not public. This may cause errors with Mono.", + entryPoint.DeclaringType.FullName, entryPoint.Name); + } + var dm = new DynamicMethod(entryPoint.DeclaringType.Assembly.GetName().Name + "_entrypoint_" + entryPoint.DeclaringType.FullName + entryPoint.Name, null, null, true); + var il = dm.GetILGenerator(); + var args = entryPoint.GetParameters(); + if (args.Length > 1) throw new ArgumentException("Cannot handle entry points with more than 1 parameter."); + if (args.Length == 1) { + if (args[0].ParameterType != typeof(string[])) throw new ArgumentException("Entry point paramter must be a string array."); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, typeof(string)); + } + il.EmitCall(OpCodes.Call, entryPoint, null); + if (entryPoint.ReturnType != typeof(void)) { + il.Emit(OpCodes.Pop); + } + il.Emit(OpCodes.Ret); + return (Action)dm.CreateDelegate(typeof(Action)); + } + + } +}