From 16b5704b1fc54fa78b1466cd74d67a6f5e00c40d Mon Sep 17 00:00:00 2001 From: oleg-shilo Date: Wed, 22 Mar 2017 22:21:13 +1000 Subject: [PATCH] Issue #56: Concurrency control for hosted scripts needs to be system wide --- Source/CSScriptLibrary/CSScriptLib.cs | 114 ++++++++++++++---- .../Evaluator.Extensions.Remote.cs | 3 +- Source/Settings.cs | 8 ++ Source/Tests/Eval.Remote.Extensions.cs | 2 +- Source/Utils.cs | 4 +- 5 files changed, 105 insertions(+), 26 deletions(-) diff --git a/Source/CSScriptLibrary/CSScriptLib.cs b/Source/CSScriptLibrary/CSScriptLib.cs index 576fdd0a..3fe1fae8 100644 --- a/Source/CSScriptLibrary/CSScriptLib.cs +++ b/Source/CSScriptLibrary/CSScriptLib.cs @@ -58,6 +58,22 @@ namespace CSScriptLibrary { + public enum HostingConcurrencyControl + { + /// + /// Any call to CSScript.Compile within a given process is synchronized + /// + ProcessScope, + /// + /// Any call to CSScript.Compile a specific script within a given process is synchronized + /// + ScriptProcessScope, + /// + /// Any call to CSScript.Compile a specific script is synchronized system wide + /// + ScriptSystemScope, + } + #if !net1 /// @@ -321,7 +337,7 @@ public void Execute(Action action, T context) /// Reference to the . It is the same object, which is passed as the . public static AppDomain Execute(this AppDomain domain, Action action, params string[] probingDirs) { - var remote = (RemoteExecutor) domain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location, typeof(RemoteExecutor).ToString()); + var remote = (RemoteExecutor)domain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location, typeof(RemoteExecutor).ToString()); remote.InitProbing(probingDirs); remote.Execute(action); remote.UninitProbing(); @@ -359,7 +375,7 @@ public static AppDomain Execute(this AppDomain domain, Action action, T co { //also possible to serialize lambda and execute it in remote AppDomain (yest it is dangerous) //look at MetaLinq\ExpressionBuilder - var remote = (RemoteExecutor) domain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location, typeof(RemoteExecutor).ToString()); + var remote = (RemoteExecutor)domain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location, typeof(RemoteExecutor).ToString()); remote.InitProbing(probingDirs); remote.Execute(action, context); remote.UninitProbing(); @@ -751,7 +767,6 @@ static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args) /// public static Settings GlobalSettings = Settings.Load(Environment.ExpandEnvironmentVariables(@"%CSSCRIPT_DIR%\css_config.xml")); -#if !net1 /// /// Collection of all compiling results. Every time the script is compiled the compiling result is added to this collection regardless of @@ -759,9 +774,31 @@ static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args) /// public static Dictionary CompilingHistory = new Dictionary(); + /// + /// The hosting concurrency control. + /// + /// + /// ProcessScope + /// Mutex name is a derived from the hosting process id. + /// Two calls to compile any script will be synchronized if the calls are made from the same host process. + /// + /// + /// ScriptProcessScope + /// Mutex name is a derived from the combination of the script path and hosting process id. + /// Two calls to compile the same script file will be synchronized but only if the calls are made from the same host process. + /// + /// + /// ScriptSystemScope + /// Mutex name is a derived from the script path. + /// Two calls to compile the same script file will be synchronized system wide. + /// + /// + /// + static public HostingConcurrencyControl HostingConcurrencyControl = HostingConcurrencyControl.ScriptSystemScope; /// - /// The last script compilation result. + /// The last script compilation result. Note, invoking CSScript.Compile/CSScript.Load may not trigger the actual compilation if script caching is + /// engaged. Thus the LastCompilingResult value can be null. /// public static CompilingInfo LastCompilingResult = null; @@ -779,8 +816,6 @@ public static bool KeepCompilingHistory set { keepCompilingHistory = value; } } -#endif - /// /// Invokes global (static) CSExecutor (C# script engine) /// @@ -1032,12 +1067,7 @@ static string ResolveConfigFilePath(string cssConfigFile) return cssConfigFile != null ? cssConfigFile : Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "css_config.xml"); } - static string GetCompilerLockName(string script, Settings scriptSettings) - { - return GetCompilerLockName(script, scriptSettings.OptimisticConcurrencyModel); - } - - static string GetCompilerLockName(string script, bool optimisticConcurrencyModel) + static string GetCompilerLockName_old(string script, bool optimisticConcurrencyModel) { if (optimisticConcurrencyModel) { @@ -1049,6 +1079,33 @@ static string GetCompilerLockName(string script, bool optimisticConcurrencyModel } } + static string GetCompilerLockName(string script) + { + var scriptPath = Path.GetFullPath(script); + + if (!Utils.IsLinux()) + scriptPath = scriptPath.ToLower(); + + switch (HostingConcurrencyControl) + { + // Mutex name is a derived from the hosting process id. + // Two calls to compile any script will be synchronized if the calls are made from the same host process. + case HostingConcurrencyControl.ProcessScope: + return "cs-script." + Process.GetCurrentProcess().Id; + + // Mutex name is a derived from the combination of the script path and hosting process id. + // Two calls to compile the same script file will be synchronized but only if the calls are made from the same host process. + case HostingConcurrencyControl.ScriptProcessScope: + return string.Format("cs-script.{0}.{1}", Process.GetCurrentProcess().Id, CSSUtils.GetHashCodeEx(scriptPath)); + + // Mutex name is a derived from the script path. + // Two calls to compile the same script file will be synchronized system wide. + case HostingConcurrencyControl.ScriptSystemScope: + default: + return "cs-script." + CSSUtils.GetHashCodeEx(scriptPath); + } + } + /// /// Creates the compiler lock object (). The Mutex object is now initially owned. /// This object is to be used for the access synchronization to the compiled script file and it can be useful for the @@ -1058,17 +1115,28 @@ static string GetCompilerLockName(string script, bool optimisticConcurrencyModel /// /// The script file. /// if set to true the operation is thread-safe within the current process. - /// Otherwise the operation is thread-safe system wide.. + /// Otherwise the operation is thread-safe system wide. /// -#if !net4 + [Obsolete("This method is no longer supported use CreateCompilerLock(string compiledScriptFile) instead, which offers more comprehensive concurrency control.")] static public Mutex CreateCompilerLock(string compiledScriptFile, bool optimisticConcurrencyModel) -#else - static public Mutex CreateCompilerLock(string compiledScriptFile, bool optimisticConcurrencyModel = false) -#endif { - return new Mutex(false, GetCompilerLockName(compiledScriptFile, optimisticConcurrencyModel)); + return new Mutex(false, GetCompilerLockName(compiledScriptFile)); } + /// + /// Creates the compiler lock object (). The Mutex object is now initially owned. + /// This object is to be used for the access synchronization to the compiled script file and it can be useful for the + /// tasks like cache purging or explicit script recompilation. + /// The concurrency/lock scope is controlled by . + /// And it is to be used to control the concurrency scope. + /// + /// The script file. + /// Otherwise the operation is thread-safe system wide. + /// + static public Mutex CreateCompilerLock(string compiledScriptFile) + { + return new Mutex(false, GetCompilerLockName(compiledScriptFile)); + } /// /// Compiles script file into assembly with CSExecutor. Uses script engine settings object and compiler specific options. /// @@ -1083,7 +1151,7 @@ static public string CompileWithConfig(string scriptFile, string assemblyFile, b { lock (typeof(CSScript)) { - using (Mutex fileLock = new Mutex(false, GetCompilerLockName(assemblyFile, scriptSettings))) + using (Mutex fileLock = new Mutex(false, GetCompilerLockName(assemblyFile??scriptFile))) { ExecuteOptions oldOptions = CSExecutor.options; try @@ -1202,11 +1270,11 @@ static ExecuteOptions InitExecuteOptions(ExecuteOptions options, Settings script if (scriptFile != "") { - scriptFile = FileParser.ResolveFile(scriptFile, (string[]) dirs.ToArray(typeof(string))); //to handle the case when the script file is specified by file name only + scriptFile = FileParser.ResolveFile(scriptFile, (string[])dirs.ToArray(typeof(string))); //to handle the case when the script file is specified by file name only dirs.Add(Path.GetDirectoryName(scriptFile)); } - options.searchDirs = RemovePathDuplicates((string[]) dirs.ToArray(typeof(string))); + options.searchDirs = RemovePathDuplicates((string[])dirs.ToArray(typeof(string))); options.scriptFileName = scriptFile; @@ -1569,7 +1637,7 @@ static public object Eval(params object[] args) if (lastArg == null || !(lastArg is string)) throw new Exception("You did not specify the code to 'Eval'"); - string methodCode = ((string) lastArg).Trim(); + string methodCode = ((string)lastArg).Trim(); string methodName = methodCode.Split(new char[] { '(', ' ' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); @@ -2001,7 +2069,7 @@ static public Assembly LoadWithConfig(string scriptFile, string assemblyFile, bo { lock (typeof(CSScript)) { - using (Mutex fileLock = new Mutex(false, GetCompilerLockName(assemblyFile, CSScript.GlobalSettings))) + using (Mutex fileLock = new Mutex(false, GetCompilerLockName(assemblyFile??scriptFile))) { ExecuteOptions oldOptions = CSExecutor.options; diff --git a/Source/CSScriptLibrary/Evaluator.Extensions.Remote.cs b/Source/CSScriptLibrary/Evaluator.Extensions.Remote.cs index eb73e709..becff1e8 100644 --- a/Source/CSScriptLibrary/Evaluator.Extensions.Remote.cs +++ b/Source/CSScriptLibrary/Evaluator.Extensions.Remote.cs @@ -29,9 +29,10 @@ public interface IRemoteAgent public static class AttachedProperies { #pragma warning disable 1591 // Missing XML comment for publicly visible type or member - public static ConditionalWeakTable> ObjectCache = new ConditionalWeakTable>(); #pragma warning restore 1591 // Missing XML comment for publicly visible type or member + public static ConditionalWeakTable> ObjectCache = new ConditionalWeakTable>(); + /// /// Sets the named value to the object. /// diff --git a/Source/Settings.cs b/Source/Settings.cs index e35ec7bf..aefe09d0 100644 --- a/Source/Settings.cs +++ b/Source/Settings.cs @@ -486,6 +486,14 @@ public bool OptimisticConcurrencyModel } bool optimisticConcurrencyModel = true; + + [Browsable(false)] + public bool ConcurrencyModel + { + get { return optimisticConcurrencyModel; } + set { optimisticConcurrencyModel = value; } + } + /// /// Gets or sets a value indicating whether auto-class decoration should allow C# 6 specific syntax. /// If it does the statement "using static dbg;" will be injected at the start of the auto-class definition thus the diff --git a/Source/Tests/Eval.Remote.Extensions.cs b/Source/Tests/Eval.Remote.Extensions.cs index 48cf3c1f..f11a7784 100644 --- a/Source/Tests/Eval.Remote.Extensions.cs +++ b/Source/Tests/Eval.Remote.Extensions.cs @@ -57,7 +57,7 @@ public void CreateDelegateRemotelyRoslyn() // MSTest (or xUnit) loads the test assembly from its build directory but places and loads the // dependencies into individual temporary folders (one asm per folder). What a "brilliant" idea!!! // - // MSText does explicit asm resolving, what helps for MonoEval. However Roslyn resolving + // MSTest does explicit asm resolving, what helps for MonoEval. However Roslyn resolving // has to be based on name-only algorithm (a version checking) as it has wired dependencies on // portable asms. Thus we need to set up special asm probing (extra argument) for Roslyn case. diff --git a/Source/Utils.cs b/Source/Utils.cs index 20f7a767..478ec88c 100644 --- a/Source/Utils.cs +++ b/Source/Utils.cs @@ -444,7 +444,9 @@ public static bool IsRuntimeCompatibleAsm(string file) public static bool IsLinux() { - return (Environment.OSVersion.Platform == PlatformID.Unix); + // Note it is not about OS being exactly Linux but rather about OS having Linux type of file system. + // For example path being case sensitive + return (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX); } public static bool ContainsPath(string path, string subPath)