Beispiel #1
0
    /// <summary>
    /// Compiles .cs script into dll/pdb, loads as assembly, and executes Main function.
    /// Temporary dll/pdb gets deleted. If .cs throws exception - it will be converted to
    /// error information, including .cs filename and source code line information.
    /// </summary>
    /// <param name="_path">Path to script which to execute</param>
    /// <param name="bAllowThrow">true if allow to throw exceptions</param>
    /// <param name="errors">Errors if any</param>
    /// <param name="args">Main argument parameters</param>
    /// <param name="bCompileOnly">true if only to compile</param>
    /// <returns>true if execution was successful.</returns>
    static public bool RunScript(String _path, bool bCompileOnly, bool bAllowThrow, out String errors, params String[] args)
    {
        errors = "";

        // ----------------------------------------------------------------
        //  Load script
        // ----------------------------------------------------------------
        String path = Path.GetFullPath(_path);

        if (!File.Exists(path))
        {
            errors = "Error: Could not load file '" + Exception2.getPath(path) + "': File does not exists.";
            if (bAllowThrow)
            {
                throw new Exception2(errors, 1);
            }
            return(false);
        }

        // Create syncProj cache folder next to syncProj.exe executable.
        String cacheDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "syncProjCache");

        if (!Directory.Exists(cacheDir))
        {
            DirectoryInfo di = Directory.CreateDirectory(cacheDir);
            di.Attributes = FileAttributes.Directory | FileAttributes.Hidden;
        }

        String   dllBaseName = Path.GetFileNameWithoutExtension(_path);
        String   tempDll;
        String   dllInfoFile     = "";
        DateTime dllInfoRealDate = DateTime.MinValue;

        for (int i = 1; ; i++)
        {
            tempDll = Path.Combine(cacheDir, dllBaseName);
            if (i != 1)
            {
                tempDll += i;
            }

            dllInfoFile = tempDll + "_script.txt";       // We keep here C# script full path just not to get collisions.
            if (!File.Exists(dllInfoFile))
            {
                File.WriteAllText(dllInfoFile, path);
                break;
            }

            String pathFromFile = "";
            //
            // Another instance of syncProj might be accessing same file at the same time, we try to retry automatically after some delay.
            //
            for (int iTry = 0; iTry < 20; iTry++)
            {
                try
                {
                    pathFromFile = File.ReadAllText(dllInfoFile);
                    break;
                }
                catch (Exception)
                {
                    Thread.Sleep(10 + iTry * 5);   // 5*20+10, overall (5*20+10)*20 / 2 = 1.1 sec
                }
            }

            if (pathFromFile == path)
            {
                dllInfoRealDate = File.GetLastWriteTime(dllInfoFile);
                break;
            }
        }

        String pdb = tempDll + ".pdb";

        tempDll += ".dll";

        List <String> filesToCompile = new List <string>();

        filesToCompile.Add(path);

        //---------------------------------------------------------------------------------------------------
        //  Get referenced .cs script file list, and from referenced files further other referenced files.
        //---------------------------------------------------------------------------------------------------
        CsScriptInfo csInfo = getCsFileInfo(filesToCompile[0], true);

        filesToCompile.AddRange(csInfo.csFiles);

        bool bCompileDll = false;

        //---------------------------------------------------------------------------------------------------
        // Compile .dll only if script.cs and it's dependent .cs are newer than compiled .dll.
        //---------------------------------------------------------------------------------------------------
        if (!File.Exists(tempDll))
        {
            bCompileDll = true;
        }

        DateTime dllInfoTargetDate = DateTime.MinValue;

        //---------------------------------------------------------------------------------------------------
        // Calculate target date anyway, so we can set it to file.
        // I have made such logic that scripts will be compiled if date / time of main script or any sub-script is changed.
        //---------------------------------------------------------------------------------------------------
        List <long> times = filesToCompile.Select(x => File.GetLastWriteTime(x).Ticks).ToList();

        //
        // If we are referencing any local .dll file, add it's time into calculation scheme.
        //
        foreach (String refDll in csInfo.refFiles)
        {
            if (File.Exists(refDll))
            {
                times.Add(File.GetLastAccessTime(refDll).Ticks);
            }
        }

        // If syncProj.exe also changed, requires recompiling all .dll's.
        // GetEntryAssembly() returns null during unit testing.
        String exeFile = System.Reflection.Assembly.GetExecutingAssembly().Location;

        times.Add(File.GetLastWriteTime(exeFile).Ticks);

        times.Sort();

        //---------------------------------------------------------------------------------------------------
        //  Basically we have multiple files, each with it's own modification date, we need to detect if any of files
        //  has changed  - either updated forth (svn update) or back (svn revert with set file dates to last commit time)
        //  We try to calculate date / time from multiple date times
        //---------------------------------------------------------------------------------------------------

        long time = times[0];                               // smallest date/time

        for (int i = 1; i < times.Count; i += 2)
        {
            if (i + 1 == times.Count)
            {
                time = (time + times[i]) / 2;               // medium between current date/time and highest
            }
            else
            {
                time += (times[i + 1] - times[i]) / 2;      // just take different between dates / times and get medium from there.
            }
        }

        dllInfoTargetDate = new DateTime(time);
        if (times.Count != 1)
        {
            dllInfoTargetDate.AddSeconds(-times.Count);     // Just some checksum on how many files we actually have.
        }
        if (!bCompileDll)
        {
            if (dllInfoRealDate != dllInfoTargetDate)
            {
                bCompileDll = true;
            }
        }

        if (csInfo.DebugEnabled())
        {
            if (!bCompileDll)
            {
                Console.WriteLine(Exception2.getPath(path) + " dll is up-to-date.");
            }
            else
            {
                Console.WriteLine(Exception2.getPath(path) + " dll will be compiled.");
            }
            //+ ": Date found: " + dllInfoRealDate.ToString("o") + " Date expected: " + dllInfoTargetDate.ToString("o")
        }

        if (bCompileDll)
        {
            // ----------------------------------------------------------------
            //  Compile it into ram
            // ----------------------------------------------------------------
            if (provider == null)
            {
                provider = new CSharpCodeProvider();
            }
#pragma warning disable 618
            if (compiler == null)
            {
                compiler = provider.CreateCompiler();
            }
#pragma warning restore 618
            CompilerParameters compilerparams = new CompilerParameters();
            compilerparams.GenerateExecutable = false;
#if NODEBUGTRACE
            // Currently it's not possible to generate in ram pdb debugging information.
            // Compiler option /debug:full should in theory allow that, but it does not work.
            compilerparams.GenerateInMemory = true;
#else
            compilerparams.GenerateInMemory        = false;
            compilerparams.IncludeDebugInformation = true;                // Needed to get line / column numbers
            compilerparams.OutputAssembly          = tempDll;
            compilerparams.CompilerOptions         = "/d:DEBUG /d:TRACE"; // /debug+ /debug:full /optimize-
#endif

            // Add assemblies from my domain - all which are not dynamic.
            if (refAssemblies == null)
            {
                var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).Select(a => a.Location).ToList();

                for (int i = 0; i < assemblies.Count; i++)
                {
                    if (assemblies[i].EndsWith(".exe") && !assemblies[i].EndsWith("\\syncproj.exe"))
                    {
                        assemblies.RemoveAt(i);
                        i--;
                    }
                }

                refAssemblies = assemblies.ToArray();
            }
            compilerparams.ReferencedAssemblies.AddRange(refAssemblies);
            foreach (var f in csInfo.refFiles)
            {
                compilerparams.ReferencedAssemblies.Add(f);
            }

            // ----------------------------------------------------------------
            //  If compile errors - report and exit.
            // ----------------------------------------------------------------
            CompilerResults results = compiler.CompileAssemblyFromFileBatch(compilerparams, filesToCompile.ToArray());
            if (results.Errors.HasErrors)
            {
                // Mimic visual studio error handling.
                StringBuilder sb = new StringBuilder();
                foreach (CompilerError error in results.Errors)
                {
                    // Missing reference file will not give any file or line information, we just use compilation
                    // script filename, and first line position. (Not exactly right, but something at least).

                    if (error.FileName == "")
                    {
                        sb.Append(Exception2.getPath(filesToCompile[0]));
                    }
                    else
                    {
                        sb.Append(Exception2.getPath(error.FileName));
                    }

                    if (error.Line == 0)
                    {
                        // error CS0006: Metadata file 'MystiqueDll.dll' could not be found
                        sb.Append("(1,1)");
                    }
                    else
                    {
                        sb.Append("(" + error.Line + "," + error.Column + ")");
                    }

                    sb.AppendFormat(": error {0}: {1}\r\n", error.ErrorNumber, error.ErrorText);
                }
                errors = sb.ToString();
                if (bAllowThrow)
                {
                    throw new Exception2(errors);
                }

                return(false);
            }
        } //if

        try
        {
            File.SetLastWriteTime(dllInfoFile, dllInfoTargetDate);
        }
        catch (Exception)
        {
            // Visual studio can launch multiple instances of syncProj, and then each will try to compile it's own copy.
            // Add here just some guard, let's check if this needs to be improved later on.
        }

        if (bCompileOnly)
        {
            return(true);
        }

        //------------------------------------------------------------------------------------------------------
        //
        // Let's check that script contains correct css_ref (Might be copied from another project).
        // We allow here also multiple copies of syncProj, as long as path to syncProj.exe is valid in .cs header
        // (Can be edited by C# script)
        //
        //------------------------------------------------------------------------------------------------------
        Regex  reCssRef          = new Regex("^ *//css_ref  *(.*syncproj\\.exe);?([\r\n]+|$)", RegexOptions.Multiline | RegexOptions.IgnoreCase);
        bool   bUpdateScriptPath = false;
        String targetCsPath      = "";

        using (StreamReader reader = new StreamReader(path))
        {
            for (int i = 0; i < 10; i++)
            {
                String line = reader.ReadLine() ?? "";
                var    re   = reCssRef.Match(line);
                if (re.Success)
                {
                    // Current path, referred from C# script
                    String currentCsPath = re.Groups[1].Value;
                    String dir           = Path.GetDirectoryName(path);
                    String exePath       = SolutionOrProject.getSyncProjExeLocation(dir, currentCsPath);
                    targetCsPath = Path2.makeRelative(exePath, dir);
                    String referredExe = currentCsPath;

                    if (!Path.IsPathRooted(referredExe))        // Uses relative path, let's make it absolute.
                    {
                        referredExe = Path.Combine(dir, currentCsPath);
                    }

                    if (currentCsPath != targetCsPath && !File.Exists(referredExe))
                    {
                        bUpdateScriptPath = true;               // Path is not the same as ours, and .exe referred by C# script does not exists.
                    }
                } //if
            } //for
        }     //using

        if (bUpdateScriptPath)
        {
            String file    = File.ReadAllText(path);
            String newFile = reCssRef.Replace(file, new MatchEvaluator(m => { return("//css_ref " + targetCsPath + "\r\n"); }));
            File.WriteAllText(path, newFile);
        }

        // ----------------------------------------------------------------
        //  Preload compiled .dll and it's debug information into ram.
        // ----------------------------------------------------------------
        MethodInfo entry    = null;
        String     funcName = "";
        Assembly   asm      = Assembly.LoadFrom(tempDll);

        //Assembly asm = results.CompiledAssembly;
        // ----------------------------------------------------------------
        //  Locate entry point
        // ----------------------------------------------------------------
        BindingFlags flags        = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.IgnoreCase;
        Type         builderClass = null;

        foreach (Type type in asm.GetTypes())
        {
            funcName = "Main";
            entry    = type.GetMethod(funcName, flags);

            if (entry == null)
            {
                funcName = "ScriptMain";
                entry    = type.GetMethod(funcName, flags);
            }

            if (entry != null)
            {
                builderClass = type;
                break;
            }
        }

        if (entry == null)
        {
            errors = String.Format("{0}(1,1): error: Code does not have 'Main' function\r\n", Exception2.getPath(path));
            if (bAllowThrow)
            {
                throw new Exception2(errors);
            }
            return(false);
        }

        if (entry.GetParameters().Length != 1)
        {
            errors = String.Format("{0}(1,1): error: Function '{1}' is not expected to have {2} parameter(s)\r\n", Exception2.getPath(path), funcName, entry.GetParameters().Length);
            if (bAllowThrow)
            {
                throw new Exception2(errors);
            }
            return(false);
        }

        String oldScriptRelativeDir = SolutionProjectBuilder.m_scriptRelativeDir;
        String scriptSubPath        = Path2.makeRelative(Path.GetDirectoryName(_path), SolutionProjectBuilder.m_workPath);
        SolutionProjectBuilder.m_scriptRelativeDir = scriptSubPath;
        String oldScriptPath = SolutionProjectBuilder.m_currentlyExecutingScriptPath;
        SolutionProjectBuilder.m_currentlyExecutingScriptPath = _path;
        String oldDir = Environment.CurrentDirectory;
        //
        // We set current directory to where script is, just so script can use Directory.GetFiles without specifying directory.
        //
        Directory.SetCurrentDirectory(Path.GetDirectoryName(_path));

        // ----------------------------------------------------------------
        //  Run script
        // ----------------------------------------------------------------
        try
        {
            entry.Invoke(null, new object[] { args });
            Directory.SetCurrentDirectory(oldDir);
            SolutionProjectBuilder.m_scriptRelativeDir            = oldScriptRelativeDir;
            SolutionProjectBuilder.m_currentlyExecutingScriptPath = oldScriptPath;
        }
        catch (Exception ex)
        {
            Directory.SetCurrentDirectory(oldDir);
            SolutionProjectBuilder.m_scriptRelativeDir            = oldScriptRelativeDir;
            SolutionProjectBuilder.m_currentlyExecutingScriptPath = oldScriptPath;
            Exception2 ex2 = ex.InnerException as Exception2;
            if (ex2 != null && bAllowThrow)
            {
                throw ex2;
            }

            try
            {
                StackFrame[] stack    = new StackTrace(ex.InnerException, true).GetFrames();
                StackFrame   lastCall = stack[0];

                errors = String.Format("{0}({1},{2}): error: {3}\r\n", path,
                                       lastCall.GetFileLineNumber(), lastCall.GetFileColumnNumber(), ex.InnerException.Message);
            } catch (Exception ex3)
            {
                errors = String.Format("{0}(1,1): error: Internal error - exception '{3}'\r\n", path, ex3.Message);
            }
            if (bAllowThrow)
            {
                throw new Exception2(errors);
            }
            return(false);
        }

        return(true);
    } //RunScript
Beispiel #2
0
 static void DoTest1()
 {
     Console.WriteLine("This script ran from: " + Path2.makeRelative(getCsDir(), m_workPath));
     Console.WriteLine("Script name: " + getCsFileName());
 } //Main