void Init(string fileNname, string domainName) { //difference comparing to InitLagacy: // CreateInstanceAndUnwrap instead of CreateInstanceFromAndUnwrap // // setup.ApplicationBase = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); // instead of setup.ApplicationBase = Path.GetDirectoryName(assemblyFileName); // // In 2016 just discovered that InitLegacy doesn't longer work. May be because some changes in .NET versions... // This is a low impact change as AssemblyExecutor is only used for cached vs. non-cached execution in stand alone // hosting mode. assemblyFileName = fileNname; AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationBase = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); setup.PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory; setup.ApplicationName = Utils.GetAssemblyFileName(Assembly.GetExecutingAssembly()); setup.ShadowCopyFiles = "true"; setup.ShadowCopyDirectories = Path.GetDirectoryName(assemblyFileName); appDomain = AppDomain.CreateDomain(domainName, null, setup); remoteExecutor = (RemoteExecutor)appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(RemoteExecutor).ToString()); remoteExecutor.searchDirs = ExecuteOptions.options.searchDirs; }
public AssemblyExecutor(string fileNname, string domainName) { assemblyFileName = fileNname; AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationBase = Path.GetDirectoryName(assemblyFileName); setup.PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory; setup.ApplicationName = Utils.GetAssemblyFileName(Assembly.GetExecutingAssembly()); setup.ShadowCopyFiles = "true"; setup.ShadowCopyDirectories = Path.GetDirectoryName(assemblyFileName); appDomain = AppDomain.CreateDomain(domainName, null, setup); remoteExecutor = (RemoteExecutor)appDomain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(RemoteExecutor).ToString()); remoteExecutor.searchDirs = ExecuteOptions.options.searchDirs; }
bool InitLegacy(string fileNname, string domainName) { try { assemblyFileName = fileNname; AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationBase = Path.GetDirectoryName(assemblyFileName); setup.PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory; setup.ApplicationName = Path.GetFileName(Assembly.GetExecutingAssembly().Location()); setup.ShadowCopyFiles = Environment.GetEnvironmentVariable("AppDomainSetup.ShadowCopyFiles") ?? "true"; setup.ShadowCopyDirectories = Path.GetDirectoryName(assemblyFileName); appDomain = AppDomain.CreateDomain(domainName, null, setup); // EXPECTED TO FAIL VERY OFTEN. IT'S NORMAL. remoteExecutor = (RemoteExecutor)appDomain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(RemoteExecutor).ToString()); remoteExecutor.searchDirs = ExecuteOptions.options.searchDirs; return(true); } catch { return(false); } }
/// <summary> /// This method implements compiling and execution of the script. /// </summary> void ExecuteImpl() { try { //System.Diagnostics.Debug.Assert(false); if (options.processFile) { CSharpParser.InitInfo initInfo = options.initContext as CSharpParser.InitInfo; if (initInfo != null && initInfo.CoInitializeSecurity) ComInitSecurity(initInfo.RpcImpLevel, initInfo.EoAuthnCap); if (options.local) Environment.CurrentDirectory = Path.GetDirectoryName(Path.GetFullPath(options.scriptFileName)); if (!options.noLogo) { Console.WriteLine(AppInfo.appLogo); } if (Environment.GetEnvironmentVariable("EntryScript") == null) Environment.SetEnvironmentVariable("EntryScript", Path.GetFullPath(options.scriptFileName)); { CSSUtils.VerbosePrint("> ----------------", options); CSSUtils.VerbosePrint(" TragetFramework: " + options.TargetFramework, options); CSSUtils.VerbosePrint(" Provider: " + options.altCompiler, options); try { CSSUtils.VerbosePrint(" Engine: " + Assembly.GetExecutingAssembly().Location, options); } catch { } try { CSSUtils.VerbosePrint(string.Format(" Console Encoding: {0} ({1}) - {2}", Console.OutputEncoding.WebName, Console.OutputEncoding.EncodingName, (Utils.IsDefaultConsoleEncoding ? "system default" : "set by engine")), options); } catch { } //will fail for windows app CSSUtils.VerbosePrint(" CurrentDirectory: " + Environment.CurrentDirectory, options); if (!Utils.IsLinux() && options.verbose) { CSSUtils.VerbosePrint(" NuGet manager: " + NuGet.NuGetExeView, options); CSSUtils.VerbosePrint(" NuGet cache: " + NuGet.NuGetCacheView, options); } CSSUtils.VerbosePrint(" Executing: " + Path.GetFullPath(options.scriptFileName), options); CSSUtils.VerbosePrint(" Script arguments: ", options); for (int i = 0; i < scriptArgs.Length; i++) CSSUtils.VerbosePrint(" " + i + " - " + scriptArgs[i], options); CSSUtils.VerbosePrint(" SearchDirectories: ", options); { int offset = 0; if (Path.GetFullPath(options.searchDirs[0]) != Environment.CurrentDirectory) { CSSUtils.VerbosePrint(" 0 - " + Environment.CurrentDirectory, options); offset++; } for (int i = 0; i < options.searchDirs.Length; i++) CSSUtils.VerbosePrint(" " + (i + offset) + " - " + options.searchDirs[i], options); } CSSUtils.VerbosePrint("> ----------------", options); CSSUtils.VerbosePrint("", options); } // The following long comment is also reflected on Wiki // Execution consist of multiple stages and some of them need to be atomic and need to be synchronized system wide. // Note: synchronization (concurrency control) may only be required for execution of a given script by two or more competing // processes. If one process executes script_a.cs and another one executes script_b.cs then there is no need for any synchronization // as the script files are different and their executions do not collide with each other. // --- // VALIDATION // First, script should be validated: assessed for having valid already compiled up to date assembly. Validation is done // by checking if the compiled assembly available at all and then comparing timestamps of the assembly and the script file. // After all on checks are done all script dependencies (imports and ref assemblies) are also validated. Dependency validation is // also timestamp based. For the script the dependencies are identified by parsing the script and for the assembly by extracting the // dependencies metadata injected into assembly during the last compilation by the script engine. // The whole validation stage is atomic and it's synchronized system wide via SystemWideLock validatingFileLock. // SystemWideLock is a decorated Mutex-like system wide synchronization object: Mutex on Windows and file-lock on Linux. // This stage is very fast as there is no heavy lifting to be done just comparing timestamps. // Timeout is infinite as there is very little chance for the stage to hang. // --- // COMPILATION // Next, if assembly is valid (the script hasn't been changed since last compilation) the it is loaded for further execution without recompilation. // Otherwise it is compiled again. Compilation stage is also atomic, so concurrent compilations (if happen) do not try to build the assembly potentially // in the same location with the same name (e.g. caching). The whole validation stage is atomic and it's also synchronized system wide via SystemWideLock // 'compilingFileLock'. // This stage is potentially heavy. Some compilers, while being relatively fast may introduce significant startup overhead (like Roslyn). // That is why caching is a preferred execution approach. // Timeout is fixed as there is a chance that the third party compiler can hang. // --- // EXECUTION // Next, the script assembly needs to be loaded/executed.This stage is extremely heavy as the execution may take infinite time depending on business logic. // When the assembly file is loaded it is locked by CLR and none can delete/recreate it. Meaning that if any one is to recompile the assembly then this // cannot be done until the execution is completed and CLR releases the assembly file. System wide synchronization doesn't make much sense in this case // as open end waiting is not practical at all. Thus it's more practical to let the compiler throw an informative locking (access denied) exception. // // Note: if the assembly is loaded as in-memory file copy (options.inMemoryAsm) then the assembly locking is completely eliminated. This is in fact an extremely // attractive execution model as it eliminates any problems associated with the assembly looking during the execution. The only reason why it's not activated by // default is contradicts the traditional .NET loading model when the assembly loaded as a file. // // While execution stage cannot benefit from synchronization it is still using executingFileLock synchronization object. Though the objective is not to wait // when file lock is detected but rather to detect unlocking and start compiling with a little delay. Reason for this is that (on Windows at least) CLR holds // the file lock a little bit longer event after the assembly execution is finished. It is completely undocumented CLR behaver, that is hard to catch and reproduce. // It has bee confirmed by the users that this work around helps in the intense concurrent "border line" scenarios. Though there is no warranty it will be still // valid with any future releases of CLR. There ware no reports about this behaver on Linux. // --- // Synchronizing the stages via the lock object that is based on the assembly file name seems like the natural and best option. However the actual assembly name // (unless caching is active) is only determined during the compilation, which needs to be synchronized (chicken egg problem). Thus script file is a more practical // (and more conservative) approach for basing synchronization objects identity. Though if the need arises the assembly-based approach can be attempted. // ------------------------------------------------------ // The concurrency model described above was an unconditional behaver until v3.16 // Since v3.16 concurrency model can be chosen based on the user preferences: // * ConcurrencyControl.HighResolution // The model described above. // // * ConcurrencyControl.Standard // Due to the limited choices with the system wide named synchronization objects on Linux both Validation and Compilations stages are treated as a single stage, // controlled by a single synch object compilingFileLock. // This happens to be a good default choice for Windows as well. // // * ConcurrencyControl.None // All synchronization is the responsibility of the hosting environment. using (SystemWideLock validatingFileLock = new SystemWideLock(options.scriptFileName, "v")) using (SystemWideLock compilingFileLock = new SystemWideLock(options.scriptFileName, null)) using (SystemWideLock executingFileLock = new SystemWideLock(options.scriptFileName, "e")) { bool lockByCSScriptEngine = false; bool lockedByCompiler = false; // --- VALIDATE --- switch (options.concurrencyControl) { case ConcurrencyControl.Standard: lockedByCompiler = !compilingFileLock.Wait(3000); break; case ConcurrencyControl.HighResolution: lockByCSScriptEngine = !validatingFileLock.Wait(-1); break; } //GetAvailableAssembly also checks timestamps string assemblyFileName = options.useCompiled ? GetAvailableAssembly(options.scriptFileName) : null; if (options.useCompiled && options.useSmartCaching) { if (assemblyFileName != null) { if (MetaDataItems.IsOutOfDate(options.scriptFileName, assemblyFileName)) { assemblyFileName = null; } } } if (options.forceCompile && assemblyFileName != null) { switch (options.concurrencyControl) { case ConcurrencyControl.Standard: /*we already acquired compiler lock*/ break; case ConcurrencyControl.HighResolution: lockedByCompiler = !compilingFileLock.Wait(3000); break; } //If file is still locked (lockedByCompiler == true) FileDelete will throw the exception Utils.FileDelete(assemblyFileName, true); assemblyFileName = null; } if (assemblyFileName != null) { //OK, there is a compiled script file and it is current. //Validating stage is over as we are not going to recompile the script //Release the lock immediately so other instances can do their validation if waiting. validatingFileLock.Release(); } else { //do not release validation lock as we will need to compile the script and any pending validations //will need to wait until we finish. } //add searchDirs to PATH to support search path for native dlls //need to do this before compilation or execution string path = Environment.GetEnvironmentVariable("PATH"); foreach (string s in options.searchDirs) path += ";" + s; #if net1 SetEnvironmentVariable("PATH", path); #else Environment.SetEnvironmentVariable("PATH", path); #endif //it is possible that there are fully compiled/cached and up to date script but no host compiled yet string host = ScriptLauncherBuilder.GetLauncherName(assemblyFileName); bool surrogateHostMissing = (options.useSurrogateHostingProcess && (!File.Exists(host) || !CSSUtils.HaveSameTimestamp(host, assemblyFileName))); // --- COMPILE --- if (options.buildExecutable || !options.useCompiled || (options.useCompiled && assemblyFileName == null) || options.forceCompile || surrogateHostMissing) { // Wait for other COMPILATION to complete(if any) // infinite is not good here as it may block forever but continuing while the file is still locked will // throw a nice informative exception switch (options.concurrencyControl) { case ConcurrencyControl.Standard: /*we already acquired compiler lock*/ break; case ConcurrencyControl.HighResolution: lockedByCompiler = !compilingFileLock.Wait(3000); break; } //no need to act on lockedByCompiler/lockedByHost as Compile(...) will throw the exception if (!options.inMemoryAsm && !Utils.IsLinux()) { // wait for other EXECUTION to complete (if any) bool lockedByHost = !executingFileLock.Wait(1000); if (!lockedByHost) { //!lockedByHost means that if there is a host executing the assemblyFileName script it has finished the execution //but the assemblyFileName file itself may still be locked by the IO because it's host process may be just exiting Utils.WaitForFileIdle(assemblyFileName, 1000); } } try { CSSUtils.VerbosePrint("Compiling script...", options); CSSUtils.VerbosePrint("", options); TimeSpan initializationTime = Profiler.Stopwatch.Elapsed; Profiler.Stopwatch.Reset(); Profiler.Stopwatch.Start(); assemblyFileName = Compile(options.scriptFileName); if (Profiler.Stopwatch.IsRunning) { Profiler.Stopwatch.Stop(); TimeSpan compilationTime = Profiler.Stopwatch.Elapsed; CSSUtils.VerbosePrint("Initialization time: " + initializationTime.TotalMilliseconds + " msec", options); CSSUtils.VerbosePrint("Compilation time: " + compilationTime.TotalMilliseconds + " msec", options); CSSUtils.VerbosePrint("> ----------------", options); CSSUtils.VerbosePrint("", options); } } catch { if (!CSSUtils.IsRuntimeErrorReportingSupressed) { print("Error: Specified file could not be compiled.\n"); if (NuGet.newPackageWasInstalled) { print("> -----\nA new NuGet package has been installed. If some of it's components are not found you may need to restart the script again.\n> -----\n"); } } throw; } finally { //release as soon as possible validatingFileLock.Release(); compilingFileLock.Release(); } } else { Profiler.Stopwatch.Stop(); CSSUtils.VerbosePrint(" Loading script from cache...", options); CSSUtils.VerbosePrint("", options); CSSUtils.VerbosePrint(" Cache file: \n " + assemblyFileName, options); CSSUtils.VerbosePrint("> ----------------", options); CSSUtils.VerbosePrint("Initialization time: " + Profiler.Stopwatch.Elapsed.TotalMilliseconds + " msec", options); CSSUtils.VerbosePrint("> ----------------", options); } // --- EXECUTE --- if (!options.supressExecution) { try { if (options.useSurrogateHostingProcess) { throw new SurrogateHostProcessRequiredException(assemblyFileName, scriptArgs, options.startDebugger); } if (options.startDebugger) { SaveDebuggingMetadata(options.scriptFileName); System.Diagnostics.Debugger.Launch(); if (System.Diagnostics.Debugger.IsAttached) { System.Diagnostics.Debugger.Break(); } } if (options.useCompiled || options.cleanupShellCommand != "") { AssemblyResolver.CacheProbingResults = true; //it is reasonable safe to do the aggressive probing as we are executing only a single script (standalone execution not a script hosting model) //despite the name of the class the execution (assembly loading) will be in the current domain //I am just reusing some functionality of the RemoteExecutor class. RemoteExecutor executor = new RemoteExecutor(options.searchDirs); executor.ExecuteAssembly(assemblyFileName, scriptArgs, executingFileLock); } else { //Load and execute assembly in a different domain to make it possible to unload assembly before clean up AssemblyExecutor executor = new AssemblyExecutor(assemblyFileName, "AsmExecution"); executor.Execute(scriptArgs); } } catch (SurrogateHostProcessRequiredException) { throw; } catch { if (!CSSUtils.IsRuntimeErrorReportingSupressed) print("Error: Specified file could not be executed.\n"); throw; } //cleanup if (File.Exists(assemblyFileName) && !options.useCompiled && options.cleanupShellCommand == "") { Utils.ClearFile(assemblyFileName); } if (options.cleanupShellCommand != "") { try { string counterFile = Path.Combine(GetScriptTempDir(), "counter.txt"); int prevRuns = 0; if (File.Exists(counterFile)) using (StreamReader sr = new StreamReader(counterFile)) { prevRuns = int.Parse(sr.ReadToEnd()); } if (prevRuns > options.doCleanupAfterNumberOfRuns) { prevRuns = 1; string[] cmd = options.ExtractShellCommand(options.cleanupShellCommand); if (cmd.Length > 1) Process.Start(cmd[0], cmd[1]); else Process.Start(cmd[0]); } else prevRuns++; using (StreamWriter sw = new StreamWriter(counterFile)) sw.Write(prevRuns); } catch { } } } } } } catch (Exception e) { Exception ex = e; if (e is System.Reflection.TargetInvocationException) ex = e.InnerException; if (rethrow || e is SurrogateHostProcessRequiredException) { lastException = ex; } else { Environment.ExitCode = 1; if (!CSSUtils.IsRuntimeErrorReportingSupressed) { if (options.reportDetailedErrorInfo) print(ex.ToString()); else print(ex.Message); //Mono friendly } } } }
/// <summary> /// This method implements compiling and execution of the script. /// </summary> private void ExecuteImpl() { try { if (options.processFile) { if (options.local) Environment.CurrentDirectory = Path.GetDirectoryName(Path.GetFullPath(options.scriptFileName)); if (!options.noLogo) { Console.WriteLine(AppInfo.appLogo); } //compile string assemblyFileName = options.useCompiled ? GetAvailableAssembly(options.scriptFileName) : null; if (options.useCompiled && options.useSmartCaching) { if (assemblyFileName != null) { MetaDataItems depInfo = new MetaDataItems(); if (depInfo.ReadFileStamp(assemblyFileName)) { string dependencyFile = ""; foreach (MetaDataItems.MetaDataItem item in depInfo.items) { if (item.assembly) dependencyFile = Path.Combine(Path.GetDirectoryName(options.scriptFileName), item.file); //assembly should be in the same directory with the script else dependencyFile = FileParser.ResolveFile(item.file, options.searchDirs, false); FileInfo info = new FileInfo(dependencyFile); if (!info.Exists || info.LastWriteTimeUtc != item.date) { assemblyFileName = null; break; } } } else assemblyFileName = null; } } if (options.forceCompile && assemblyFileName != null) { File.Delete(assemblyFileName); assemblyFileName = null; } if (options.buildExecutable || !options.useCompiled || (options.useCompiled && assemblyFileName == null) || options.forceCompile) { try { assemblyFileName = Compile(options.scriptFileName); } catch { print("Error: Specified file could not be compiled.\n"); throw; } } //execute if (!options.supressExecution) { try { if (options.startDebugger) { System.Diagnostics.Debugger.Launch(); if (System.Diagnostics.Debugger.IsAttached) System.Diagnostics.Debugger.Break(); } if (options.useCompiled || options.cleanupShellCommand != "") { //despite the name of the class the execution will be in the current domain RemoteExecutor executor = new RemoteExecutor(options.searchDirs); executor.ExecuteAssembly(assemblyFileName, scriptArgs); } else { ExecuteAssembly(assemblyFileName); } } catch { print("Error: Specified file could not be executed.\n"); throw; } //cleanup if (File.Exists(assemblyFileName) && !options.useCompiled && options.cleanupShellCommand == "") { try { File.Delete(assemblyFileName); } catch { } } if (options.cleanupShellCommand != "") { string counterFile = Path.Combine(GetScriptTempDir(), "counter.txt"); int prevRuns = 0; try { using (StreamReader sr = new StreamReader(counterFile)) { prevRuns = int.Parse(sr.ReadToEnd()); } } catch { } if (prevRuns > options.doCleanupAfterNumberOfRuns) { prevRuns = 1; string[] cmd = options.ExtractShellCommand(options.cleanupShellCommand); if (cmd.Length > 1) Process.Start(cmd[0], cmd[1]); else Process.Start(cmd[0]); } else prevRuns++; try { using (StreamWriter sw = new StreamWriter(counterFile)) sw.Write(prevRuns); } catch { } } } } } catch (Exception e) { if (rethrow) { lastException = e; } else { Environment.ExitCode = 1; if (options.reportDetailedErrorInfo) print(e.ToString()); else print(e.Message); //Mono friendly } } }
/// <summary> /// This method implements compiling and execution of the script. /// </summary> void ExecuteImpl() { try { //System.Diagnostics.Debug.Assert(false); if (options.processFile) { CSharpParser.InitInfo initInfo = options.initContext as CSharpParser.InitInfo; if (initInfo != null && initInfo.CoInitializeSecurity) ComInitSecurity(initInfo.RpcImpLevel, initInfo.EoAuthnCap); if (options.local) Environment.CurrentDirectory = Path.GetDirectoryName(Path.GetFullPath(options.scriptFileName)); if (!options.noLogo) { Console.WriteLine(AppInfo.appLogo); } if (Environment.GetEnvironmentVariable("EntryScript") == null) Environment.SetEnvironmentVariable("EntryScript", Path.GetFullPath(options.scriptFileName)); { CSSUtils.VerbosePrint("> ----------------", options); CSSUtils.VerbosePrint(" TragetFramework: " + options.TargetFramework, options); try { CSSUtils.VerbosePrint(string.Format(" Console Encoding: {0} ({1}) - {2}", Console.OutputEncoding.WebName, Console.OutputEncoding.EncodingName, (Utils.IsDeraultConsoleEncoding ? "system default" : "set by engine")), options); } catch { } //will fail for windows app CSSUtils.VerbosePrint(" CurrentDirectory: " + Environment.CurrentDirectory, options); if (!Utils.IsLinux()) { CSSUtils.VerbosePrint(" NuGet manager: " + NuGet.NuGetExeView, options); CSSUtils.VerbosePrint(" NuGet cache: " + NuGet.NuGetCacheView, options); } CSSUtils.VerbosePrint(" Executing: " + Path.GetFullPath(options.scriptFileName), options); CSSUtils.VerbosePrint(" Script arguments: ", options); for (int i = 0; i < scriptArgs.Length; i++) CSSUtils.VerbosePrint(" " + i + " - " + scriptArgs[i], options); CSSUtils.VerbosePrint(" SearchDirectories: ", options); for (int i = 0; i < options.searchDirs.Length; i++) CSSUtils.VerbosePrint(" " + i + " - " + options.searchDirs[i], options); CSSUtils.VerbosePrint("> ----------------", options); CSSUtils.VerbosePrint("", options); } bool compilingFileUnlocked = false; //Infinite timeout is not good choice here as it may block forever but continuing while the file is still locked will //throw a nice informative exception. using (Mutex validatingFileLock = Utils.FileLock(options.scriptFileName, "TimestampValidating")) using (Mutex compilingFileLock = Utils.FileLock(options.scriptFileName, "Compiling")) using (Mutex executingFileLock = Utils.FileLock(options.scriptFileName, "Executing")) //will need it in the future for more accurate concurrency control try { //validate bool availableForChecking = validatingFileLock.WaitOne(-1, false); //GetAvailableAssembly also checks timestamps string assemblyFileName = options.useCompiled ? GetAvailableAssembly(options.scriptFileName) : null; if (options.useCompiled && options.useSmartCaching) { if (assemblyFileName != null) { if (MetaDataItems.IsOutOfDate(options.scriptFileName, assemblyFileName)) { assemblyFileName = null; } } } if (options.forceCompile && assemblyFileName != null) { bool lockedByCompiler = !compilingFileLock.WaitOne(3000, false); //no need to act on lockedByCompiler as FileDelete will throw the exception Utils.FileDelete(assemblyFileName, true); assemblyFileName = null; } if (assemblyFileName != null) Utils.ReleaseFileLock(validatingFileLock); //OK, there is a compiled script file and it is current //add searchDirs to PATH to support search path for native dlls //need to do this before compilation or execution string path = Environment.GetEnvironmentVariable("PATH"); foreach (string s in options.searchDirs) path += ";" + s; #if net1 SetEnvironmentVariable("PATH", path); #else Environment.SetEnvironmentVariable("PATH", path); #endif //it is possible that there are fully compiled/cached and up to date script but no host compiled yet string host = ScriptLauncherBuilder.GetLauncherName(assemblyFileName); bool surrogateHostMissing = (options.useSurrogateHostingProcess && (!File.Exists(host) || !CSSUtils.HaveSameTimestamp(host, assemblyFileName))); //compile if (options.buildExecutable || !options.useCompiled || (options.useCompiled && assemblyFileName == null) || options.forceCompile || surrogateHostMissing) { //infinite is not good here as it may block forever but continuing while the file is still locked will //throw a nice informative exception bool lockedByCompiler = !compilingFileLock.WaitOne(3000, false); bool lockedByHost = !executingFileLock.WaitOne(3000, false); if (!lockedByHost) { //!lockedByHost means that if there is a host executing the assemblyFileName script it has finished the execution //but the assemblyFileName file itself may still be locked by the IO because it's host process may be just exiting Utils.WaitForFileIdle(assemblyFileName, 1000); } //no need to act on lockedByCompiler/lockedByHost as Compile(...) will throw the exception try { CSSUtils.VerbosePrint("Compiling script...", options); CSSUtils.VerbosePrint("", options); TimeSpan initializationTime = Profiler.Stopwatch.Elapsed; Profiler.Stopwatch.Reset(); Profiler.Stopwatch.Start(); assemblyFileName = Compile(options.scriptFileName); Debug.WriteLine("Asm is compiled"); if (Profiler.Stopwatch.IsRunning) { Profiler.Stopwatch.Stop(); TimeSpan compilationTime = Profiler.Stopwatch.Elapsed; CSSUtils.VerbosePrint("Initialization time: " + initializationTime.TotalMilliseconds + " msec", options); CSSUtils.VerbosePrint("Compilation time: " + compilationTime.TotalMilliseconds + " msec", options); CSSUtils.VerbosePrint("> ----------------", options); CSSUtils.VerbosePrint("", options); } } catch { if (!CSSUtils.IsRuntimeErrorReportingSupressed) print("Error: Specified file could not be compiled.\n"); throw; } finally { Utils.ReleaseFileLock(validatingFileLock); Utils.ReleaseFileLock(compilingFileLock); compilingFileUnlocked = true; } } else { Profiler.Stopwatch.Stop(); CSSUtils.VerbosePrint(" Loading script from cache...", options); CSSUtils.VerbosePrint("", options); CSSUtils.VerbosePrint(" Cache file: \n " + assemblyFileName, options); CSSUtils.VerbosePrint("> ----------------", options); CSSUtils.VerbosePrint("Initialization time: " + Profiler.Stopwatch.Elapsed.TotalMilliseconds + " msec", options); CSSUtils.VerbosePrint("> ----------------", options); } //execute if (!options.supressExecution) { try { if (options.useSurrogateHostingProcess) { throw new SurrogateHostProcessRequiredException(assemblyFileName, scriptArgs, options.startDebugger); } if (options.startDebugger) { SaveDebuggingMetadata(options.scriptFileName); System.Diagnostics.Debugger.Launch(); if (System.Diagnostics.Debugger.IsAttached) { System.Diagnostics.Debugger.Break(); } } if (options.useCompiled || options.cleanupShellCommand != "") { AssemblyResolver.CacheProbingResults = true; //it is reasonable safe to do the aggressive probing as we are executing only a single script (standalone execution not a script hosting model) //despite the name of the class the execution (assembly loading) will be in the current domain //I am just reusing some functionality of the RemoteExecutor class. RemoteExecutor executor = new RemoteExecutor(options.searchDirs); executor.ExecuteAssembly(assemblyFileName, scriptArgs, executingFileLock); } else { //Load and execute assembly in a different domain to make it possible to unload assembly before clean up AssemblyExecutor executor = new AssemblyExecutor(assemblyFileName, "AsmExecution"); executor.Execute(scriptArgs); } } catch (SurrogateHostProcessRequiredException) { throw; } catch { if (!CSSUtils.IsRuntimeErrorReportingSupressed) print("Error: Specified file could not be executed.\n"); throw; } //cleanup if (File.Exists(assemblyFileName) && !options.useCompiled && options.cleanupShellCommand == "") { Utils.FileDelete(assemblyFileName); } //Debug.Assert(false); if (options.cleanupShellCommand != "") { try { string counterFile = Path.Combine(GetScriptTempDir(), "counter.txt"); int prevRuns = 0; if (File.Exists(counterFile)) using (StreamReader sr = new StreamReader(counterFile)) { prevRuns = int.Parse(sr.ReadToEnd()); } if (prevRuns > options.doCleanupAfterNumberOfRuns) { prevRuns = 1; string[] cmd = options.ExtractShellCommand(options.cleanupShellCommand); if (cmd.Length > 1) Process.Start(cmd[0], cmd[1]); else Process.Start(cmd[0]); } else prevRuns++; using (StreamWriter sw = new StreamWriter(counterFile)) sw.Write(prevRuns); } catch { } } } } finally { //strictly speaking releasing isn't needed as the 'using' block ensures the mutex is released //though some .NET versions/implementations may not do this. if (!compilingFileUnlocked) //avoid throwing unnecessary exception Utils.ReleaseFileLock(compilingFileLock); Utils.ReleaseFileLock(executingFileLock); } } } catch (Exception e) { Exception ex = e; if (e is System.Reflection.TargetInvocationException) ex = e.InnerException; if (rethrow || e is SurrogateHostProcessRequiredException) { lastException = ex; } else { Environment.ExitCode = 1; if (!CSSUtils.IsRuntimeErrorReportingSupressed) { if (options.reportDetailedErrorInfo) print(ex.ToString()); else print(ex.Message); //Mono friendly } } } }
/// <summary> /// This method implements compiling and execution of the script. /// </summary> private void ExecuteImpl() { try { //System.Diagnostics.Debug.Assert(false); if (options.processFile) { if (options.local) Environment.CurrentDirectory = Path.GetDirectoryName(Path.GetFullPath(options.scriptFileName)); if (!options.noLogo) { Console.WriteLine(AppInfo.appLogo); } if (Environment.GetEnvironmentVariable("EntryScript") == null) Environment.SetEnvironmentVariable("EntryScript", Path.GetFullPath(options.scriptFileName)); { CSSUtils.VerbosePrint("> ----------------", options); CSSUtils.VerbosePrint(" TragetFramework: " + options.TargetFramework, options); CSSUtils.VerbosePrint(" CurrentDirectory: " + Environment.CurrentDirectory, options); CSSUtils.VerbosePrint(" Executing: " + Path.GetFullPath(options.scriptFileName), options); CSSUtils.VerbosePrint(" Script arguments: ", options); for (int i = 0; i < scriptArgs.Length; i++) CSSUtils.VerbosePrint(" " + i + " - " + scriptArgs[i], options); CSSUtils.VerbosePrint(" SearchDirectories: ", options); for (int i = 0; i < options.searchDirs.Length; i++) CSSUtils.VerbosePrint(" " + i + " - " + options.searchDirs[i], options); CSSUtils.VerbosePrint("> ----------------", options); CSSUtils.VerbosePrint("", options); } bool fileUnlocked = false; using (Mutex fileLock = new Mutex(false, "Process." + options.scriptFileName.GetHashCode().ToString())) try { int start = Environment.TickCount; fileLock.WaitOne(3000, false); //let other thread/process (if any) to finish loading/compiling the same file; 3 seconds should be enough, if you need more use more sophisticated synchronization //Trace.WriteLine(">>> Waited " + (Environment.TickCount - start)); //compile string assemblyFileName = options.useCompiled ? GetAvailableAssembly(options.scriptFileName) : null; if (options.useCompiled && options.useSmartCaching) { if (assemblyFileName != null) { if (MetaDataItems.IsOutOfDate(options.scriptFileName, assemblyFileName)) { assemblyFileName = null; } } } if (options.forceCompile && assemblyFileName != null) { File.Delete(assemblyFileName); assemblyFileName = null; } //add searchDirs to PATH to support search path for native dlls //need to do this before compilation or execution string path = Environment.GetEnvironmentVariable("PATH"); foreach (string s in options.searchDirs) path += ";" + s; #if net1 SetEnvironmentVariable("PATH", path); #else Environment.SetEnvironmentVariable("PATH", path); #endif //it is possible that there are fully compiled/cached and up to date script but no host compiled yet string host = ScriptLauncherBuilder.GetLauncherName(assemblyFileName); bool surrogateHostMissing = (options.useSurrogateHostingProcess && (!File.Exists(host) || !CSSUtils.HaveSameTimestamp(host, assemblyFileName))); if (options.buildExecutable || !options.useCompiled || (options.useCompiled && assemblyFileName == null) || options.forceCompile || surrogateHostMissing) { try { CSSUtils.VerbosePrint("Compiling script...", options); CSSUtils.VerbosePrint("", options); TimeSpan initializationTime = Profiler.Stopwatch.Elapsed; Profiler.Stopwatch.Reset(); Profiler.Stopwatch.Start(); assemblyFileName = Compile(options.scriptFileName); if (Profiler.Stopwatch.IsRunning) { Profiler.Stopwatch.Stop(); TimeSpan compilationTime = Profiler.Stopwatch.Elapsed; CSSUtils.VerbosePrint("Initialization time: " + initializationTime.TotalMilliseconds + " msec", options); CSSUtils.VerbosePrint("Compilation time: " + compilationTime.TotalMilliseconds + " msec", options); CSSUtils.VerbosePrint("> ----------------", options); CSSUtils.VerbosePrint("", options); } } catch { print("Error: Specified file could not be compiled.\n"); throw; } finally { try { fileLock.ReleaseMutex(); } catch { } fileUnlocked = true; } } else { Profiler.Stopwatch.Stop(); CSSUtils.VerbosePrint(" Loading script from cache...", options); CSSUtils.VerbosePrint("", options); CSSUtils.VerbosePrint(" Cache file: \n " + assemblyFileName, options); CSSUtils.VerbosePrint("> ----------------", options); CSSUtils.VerbosePrint("Initialization time: " + Profiler.Stopwatch.Elapsed.TotalMilliseconds + " msec", options); CSSUtils.VerbosePrint("> ----------------", options); } //execute if (!options.supressExecution) { try { if (options.useSurrogateHostingProcess) { throw new SurrogateHostProcessRequiredException(assemblyFileName, scriptArgs, options.startDebugger); } if (options.startDebugger) { System.Diagnostics.Debugger.Launch(); if (System.Diagnostics.Debugger.IsAttached) System.Diagnostics.Debugger.Break(); } if (options.useCompiled || options.cleanupShellCommand != "") { AssemblyResolver.CacheProbingResults = true; //it is reasonable safe to do the agressive probing as we are executing only a single script (standalone execution not a script hosting model) //despite the name of the class the execution (assembly loading) will be in the current domain //I am just reusing some functionality of the RemoteExecutor class. RemoteExecutor executor = new RemoteExecutor(options.searchDirs); executor.ExecuteAssembly(assemblyFileName, scriptArgs); } else { //Load and execute assembly in a different domain to make it possible to unload assembly before clean up AssemblyExecutor executor = new AssemblyExecutor(assemblyFileName, "AsmExecution"); executor.Execute(scriptArgs); } } catch (SurrogateHostProcessRequiredException) { throw; } catch { print("Error: Specified file could not be executed.\n"); throw; } //cleanup if (File.Exists(assemblyFileName) && !options.useCompiled && options.cleanupShellCommand == "") { try { File.Delete(assemblyFileName); } catch { } } if (options.cleanupShellCommand != "") { try { string counterFile = Path.Combine(GetScriptTempDir(), "counter.txt"); int prevRuns = 0; using (StreamReader sr = new StreamReader(counterFile)) { prevRuns = int.Parse(sr.ReadToEnd()); } if (prevRuns > options.doCleanupAfterNumberOfRuns) { prevRuns = 1; string[] cmd = options.ExtractShellCommand(options.cleanupShellCommand); if (cmd.Length > 1) Process.Start(cmd[0], cmd[1]); else Process.Start(cmd[0]); } else prevRuns++; using (StreamWriter sw = new StreamWriter(counterFile)) sw.Write(prevRuns); } catch { } } } } finally { try { if (!fileUnlocked) fileLock.ReleaseMutex(); } //using fileUnlocked to avoid throwing unnecessary exception catch { } } } } catch (Exception e) { Exception ex = e; if (e is System.Reflection.TargetInvocationException) ex = e.InnerException; if (rethrow || e is SurrogateHostProcessRequiredException) { lastException = ex; } else { Environment.ExitCode = 1; if (options.reportDetailedErrorInfo) print(ex.ToString()); else print(ex.Message); //Mono friendly } } }