/*************************************************************************** * ScriptCompiler.cs * ------------------- * begin : May 1, 2002 * copyright : (C) The RunUO Software Team * email : info@runuo.com * * $Id$ * ***************************************************************************/ /*************************************************************************** * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * ***************************************************************************/ using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Security.Cryptography; using System.Text; namespace Server { public static class ScriptCompiler { private static List m_AdditionalReferences = new List(); private static Dictionary m_TypeCaches = new Dictionary(); private static TypeCache m_NullCache; public static Assembly[] Assemblies{ get; set; } public static string[] GetReferenceAssemblies() { List list = new List(); string path = Path.Combine(Core.BaseDirectory, "Data/Assemblies.cfg"); if (File.Exists(path)) using (StreamReader ip = new StreamReader(path)) { string line; while ((line = ip.ReadLine()) != null) if (line.Length > 0 && !line.StartsWith("#")) list.Add(line); } list.Add(Core.ExePath); list.AddRange(m_AdditionalReferences); return list.ToArray(); } public static string GetCompilerOptions(bool debug) { StringBuilder sb = null; AppendCompilerOption(ref sb, "/unsafe"); if (!debug) AppendCompilerOption(ref sb, "/optimize"); #if MONO AppendCompilerOption( ref sb, "/d:MONO" ); #endif if (Core.Is64Bit) AppendCompilerOption(ref sb, "/d:x64"); #if NEWTIMERS AppendCompilerOption(ref sb, "/d:NEWTIMERS"); #endif #if NEWPARENT AppendCompilerOption(ref sb, "/d:NEWPARENT"); #endif return sb?.ToString(); } private static void AppendCompilerOption(ref StringBuilder sb, string define) { if (sb == null) sb = new StringBuilder(); else sb.Append(' '); sb.Append(define); } private static byte[] GetHashCode(string compiledFile, string[] scriptFiles, bool debug) { using (MemoryStream ms = new MemoryStream()) { using (BinaryWriter bin = new BinaryWriter(ms)) { FileInfo fileInfo = new FileInfo(compiledFile); bin.Write(fileInfo.LastWriteTimeUtc.Ticks); foreach (string scriptFile in scriptFiles) { fileInfo = new FileInfo(scriptFile); bin.Write(fileInfo.LastWriteTimeUtc.Ticks); } bin.Write(debug); bin.Write(Core.Version.ToString()); ms.Position = 0; using (SHA1 sha1 = SHA1.Create()) { return sha1.ComputeHash(ms); } } } } public static bool CompileCSScripts(out Assembly assembly) { return CompileCSScripts(false, true, out assembly); } public static bool CompileCSScripts(bool debug, out Assembly assembly) { return CompileCSScripts(debug, true, out assembly); } public static bool CompileCSScripts(bool debug, bool cache, out Assembly assembly) { Console.Write("Scripts: Compiling C# scripts..."); string[] files = GetScripts("*.cs"); if (files.Length == 0) { Console.WriteLine("no files found."); assembly = null; return true; } if (File.Exists("Scripts/Output/Scripts.CS.dll")) if (cache && File.Exists("Scripts/Output/Scripts.CS.hash")) try { byte[] hashCode = GetHashCode("Scripts/Output/Scripts.CS.dll", files, debug); using (FileStream fs = new FileStream("Scripts/Output/Scripts.CS.hash", FileMode.Open, FileAccess.Read, FileShare.Read)) { using (BinaryReader bin = new BinaryReader(fs)) { byte[] bytes = bin.ReadBytes(hashCode.Length); if (bytes.Length == hashCode.Length) { bool valid = true; for (int i = 0; i < bytes.Length; ++i) if (bytes[i] != hashCode[i]) { valid = false; break; } if (valid) { assembly = Assembly.LoadFrom("Scripts/Output/Scripts.CS.dll"); if (!m_AdditionalReferences.Contains(assembly.Location)) m_AdditionalReferences.Add(assembly.Location); Console.WriteLine("done (cached)"); return true; } } } } } catch { } DeleteFiles("Scripts.CS*.dll"); using (CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp")) { string path = GetUnusedPath("Scripts.CS"); CompilerParameters parms = new CompilerParameters(GetReferenceAssemblies(), path, debug); string options = GetCompilerOptions(debug); if (options != null) parms.CompilerOptions = options; if (Core.HaltOnWarning) parms.WarningLevel = 4; if (Core.Unix) { parms.CompilerOptions = $"{parms.CompilerOptions} /nowarn:169,219,414 /recurse:Scripts/*.cs"; files = new string[0]; } CompilerResults results = provider.CompileAssemblyFromFile(parms, files); m_AdditionalReferences.Add(path); Display(results); if (results.Errors.Count > 0) { if (!Core.Unix) { assembly = null; return false; } foreach (CompilerError err in results.Errors) if (!err.IsWarning) { assembly = null; return false; } } if (cache && Path.GetFileName(path) == "Scripts.CS.dll") try { byte[] hashCode = GetHashCode(path, files, debug); using (FileStream fs = new FileStream("Scripts/Output/Scripts.CS.hash", FileMode.Create, FileAccess.Write, FileShare.None)) { using (BinaryWriter bin = new BinaryWriter(fs)) { bin.Write(hashCode, 0, hashCode.Length); } } } catch { } assembly = results.CompiledAssembly; return true; } } public static bool CompileVBScripts(out Assembly assembly) { return CompileVBScripts(false, out assembly); } public static bool CompileVBScripts(bool debug, out Assembly assembly) { return CompileVBScripts(debug, true, out assembly); } public static bool CompileVBScripts(bool debug, bool cache, out Assembly assembly) { Console.Write("Scripts: Compiling VB.NET scripts..."); string[] files = GetScripts("*.vb"); if (files.Length == 0) { Console.WriteLine("no files found."); assembly = null; return true; } if (File.Exists("Scripts/Output/Scripts.VB.dll")) if (cache && File.Exists("Scripts/Output/Scripts.VB.hash")) { byte[] hashCode = GetHashCode("Scripts/Output/Scripts.VB.dll", files, debug); try { using (FileStream fs = new FileStream("Scripts/Output/Scripts.VB.hash", FileMode.Open, FileAccess.Read, FileShare.Read)) { using (BinaryReader bin = new BinaryReader(fs)) { byte[] bytes = bin.ReadBytes(hashCode.Length); if (bytes.Length == hashCode.Length) { bool valid = true; for (int i = 0; i < bytes.Length; ++i) if (bytes[i] != hashCode[i]) { valid = false; break; } if (valid) { assembly = Assembly.LoadFrom("Scripts/Output/Scripts.VB.dll"); if (!m_AdditionalReferences.Contains(assembly.Location)) m_AdditionalReferences.Add(assembly.Location); Console.WriteLine("done (cached)"); return true; } } } } } catch { } } DeleteFiles("Scripts.VB*.dll"); using (CodeDomProvider provider = CodeDomProvider.CreateProvider("VisualBasic")) { string path = GetUnusedPath("Scripts.VB"); CompilerParameters parms = new CompilerParameters(GetReferenceAssemblies(), path, debug); string options = GetCompilerOptions(debug); if (options != null) parms.CompilerOptions = options; if (Core.HaltOnWarning) parms.WarningLevel = 4; if (Core.Unix) { parms.CompilerOptions = $"{parms.CompilerOptions} /nowarn:169,219,414 /recurse:Scripts/*.vb"; files = new string[0]; } CompilerResults results = provider.CompileAssemblyFromFile(parms, files); m_AdditionalReferences.Add(path); Display(results); if (results.Errors.Count > 0) { if (!Core.Unix) { assembly = null; return false; } foreach (CompilerError err in results.Errors) if (!err.IsWarning) { assembly = null; return false; } } if (cache && Path.GetFileName(path) == "Scripts.VB.dll") try { byte[] hashCode = GetHashCode(path, files, debug); using (FileStream fs = new FileStream("Scripts/Output/Scripts.VB.hash", FileMode.Create, FileAccess.Write, FileShare.None)) { using (BinaryWriter bin = new BinaryWriter(fs)) { bin.Write(hashCode, 0, hashCode.Length); } } } catch { } assembly = results.CompiledAssembly; return true; } } public static void Display(CompilerResults results) { if (results.Errors.Count > 0) { Dictionary> errors = new Dictionary>(results.Errors.Count, StringComparer.OrdinalIgnoreCase); Dictionary> warnings = new Dictionary>(results.Errors.Count, StringComparer.OrdinalIgnoreCase); foreach (CompilerError e in results.Errors) { string file = e.FileName; // Ridiculous. FileName is null if the warning/error is internally generated in csc. if (string.IsNullOrEmpty(file)) { Console.WriteLine("ScriptCompiler: {0}: {1}", e.ErrorNumber, e.ErrorText); continue; } Dictionary> table = e.IsWarning ? warnings : errors; List list = null; table.TryGetValue(file, out list); if (list == null) table[file] = list = new List(); list.Add(e); } if (errors.Count > 0) Console.WriteLine("failed ({0} errors, {1} warnings)", errors.Count, warnings.Count); else Console.WriteLine("done ({0} errors, {1} warnings)", errors.Count, warnings.Count); string scriptRoot = Path.GetFullPath(Path.Combine(Core.BaseDirectory, "Scripts" + Path.DirectorySeparatorChar)); Uri scriptRootUri = new Uri(scriptRoot); Utility.PushColor(ConsoleColor.Yellow); if (warnings.Count > 0) Console.WriteLine("Warnings:"); foreach (KeyValuePair> kvp in warnings) { string fileName = kvp.Key; List list = kvp.Value; string fullPath = Path.GetFullPath(fileName); string usedPath = Uri.UnescapeDataString(scriptRootUri.MakeRelativeUri(new Uri(fullPath)).OriginalString); Console.WriteLine(" + {0}:", usedPath); Utility.PushColor(ConsoleColor.DarkYellow); foreach (CompilerError e in list) Console.WriteLine(" {0}: Line {1}: {2}", e.ErrorNumber, e.Line, e.ErrorText); Utility.PopColor(); } Utility.PopColor(); Utility.PushColor(ConsoleColor.Red); if (errors.Count > 0) Console.WriteLine("Errors:"); foreach (KeyValuePair> kvp in errors) { string fileName = kvp.Key; List list = kvp.Value; string fullPath = Path.GetFullPath(fileName); string usedPath = Uri.UnescapeDataString(scriptRootUri.MakeRelativeUri(new Uri(fullPath)).OriginalString); Console.WriteLine(" + {0}:", usedPath); Utility.PushColor(ConsoleColor.DarkRed); foreach (CompilerError e in list) Console.WriteLine(" {0}: Line {1}: {2}", e.ErrorNumber, e.Line, e.ErrorText); Utility.PopColor(); } Utility.PopColor(); } else { Console.WriteLine("done (0 errors, 0 warnings)"); } } public static string GetUnusedPath(string name) { string path = Path.Combine(Core.BaseDirectory, $"Scripts/Output/{name}.dll"); for (int i = 2; File.Exists(path) && i <= 1000; ++i) path = Path.Combine(Core.BaseDirectory, $"Scripts/Output/{name}.{i}.dll"); return path; } public static void DeleteFiles(string mask) { try { string[] files = Directory.GetFiles(Path.Combine(Core.BaseDirectory, "Scripts/Output"), mask); foreach (string file in files) try { File.Delete(file); } catch { } } catch { } } public static bool Compile() { return Compile(false); } public static bool Compile(bool debug) { return Compile(debug, true); } public static bool Compile(bool debug, bool cache) { EnsureDirectory("Scripts/"); EnsureDirectory("Scripts/Output/"); if (m_AdditionalReferences.Count > 0) m_AdditionalReferences.Clear(); List assemblies = new List(); Assembly assembly; if (CompileCSScripts(debug, cache, out assembly)) { if (assembly != null) assemblies.Add(assembly); } else { return false; } if (Core.VBdotNet) { if (CompileVBScripts(debug, cache, out assembly)) { if (assembly != null) assemblies.Add(assembly); } else { return false; } } else { Console.WriteLine("Scripts: Skipping VB.NET Scripts...done (use -vb to enable)"); } if (assemblies.Count == 0) return false; Assemblies = assemblies.ToArray(); Console.Write("Scripts: Verifying..."); Stopwatch watch = Stopwatch.StartNew(); Core.VerifySerialization(); watch.Stop(); Console.WriteLine("done ({0} items, {1} mobiles) ({2:F2} seconds)", Core.ScriptItems, Core.ScriptMobiles, watch.Elapsed.TotalSeconds); return true; } public static void Invoke(string method) { List invoke = new List(); for (int a = 0; a < Assemblies.Length; ++a) { Type[] types = Assemblies[a].GetTypes(); for (int i = 0; i < types.Length; ++i) { MethodInfo m = types[i].GetMethod(method, BindingFlags.Static | BindingFlags.Public); if (m != null) invoke.Add(m); } } invoke.Sort(new CallPriorityComparer()); for (int i = 0; i < invoke.Count; ++i) invoke[i].Invoke(null, null); } public static TypeCache GetTypeCache(Assembly asm) { if (asm == null) { if (m_NullCache == null) m_NullCache = new TypeCache(null); return m_NullCache; } TypeCache c = null; m_TypeCaches.TryGetValue(asm, out c); if (c == null) m_TypeCaches[asm] = c = new TypeCache(asm); return c; } public static Type FindTypeByFullName(string fullName) { return FindTypeByFullName(fullName, true); } public static Type FindTypeByFullName(string fullName, bool ignoreCase) { Type type = null; for (int i = 0; type == null && i < Assemblies.Length; ++i) type = GetTypeCache(Assemblies[i]).GetTypeByFullName(fullName, ignoreCase); if (type == null) type = GetTypeCache(Core.Assembly).GetTypeByFullName(fullName, ignoreCase); return type; } public static Type FindTypeByName(string name) { return FindTypeByName(name, true); } public static Type FindTypeByName(string name, bool ignoreCase) { Type type = null; for (int i = 0; type == null && i < Assemblies.Length; ++i) type = GetTypeCache(Assemblies[i]).GetTypeByName(name, ignoreCase); if (type == null) type = GetTypeCache(Core.Assembly).GetTypeByName(name, ignoreCase); return type; } public static void EnsureDirectory(string dir) { string path = Path.Combine(Core.BaseDirectory, dir); if (!Directory.Exists(path)) Directory.CreateDirectory(path); } public static string[] GetScripts(string filter) { List list = new List(); GetScripts(list, Path.Combine(Core.BaseDirectory, "Scripts"), filter); return list.ToArray(); } public static void GetScripts(List list, string path, string filter) { foreach (string dir in Directory.GetDirectories(path)) GetScripts(list, dir, filter); list.AddRange(Directory.GetFiles(path, filter)); } private delegate CompilerResults Compiler(bool debug); } public class TypeCache { public TypeCache(Assembly asm) { if (asm == null) Types = Type.EmptyTypes; else Types = asm.GetTypes(); Names = new TypeTable(Types.Length); FullNames = new TypeTable(Types.Length); Type typeofTypeAliasAttribute = typeof(TypeAliasAttribute); for (int i = 0; i < Types.Length; ++i) { Type type = Types[i]; Names.Add(type.Name, type); FullNames.Add(type.FullName, type); if (type.IsDefined(typeofTypeAliasAttribute, false)) { object[] attrs = type.GetCustomAttributes(typeofTypeAliasAttribute, false); if (attrs.Length > 0 && attrs[0] is TypeAliasAttribute attr) for (int j = 0; j < attr.Aliases.Length; ++j) FullNames.Add(attr.Aliases[j], type); } } } public Type[] Types{ get; } public TypeTable Names{ get; } public TypeTable FullNames{ get; } public Type GetTypeByName(string name, bool ignoreCase) { return Names.Get(name, ignoreCase); } public Type GetTypeByFullName(string fullName, bool ignoreCase) { return FullNames.Get(fullName, ignoreCase); } } public class TypeTable { private Dictionary m_Sensitive, m_Insensitive; public TypeTable(int capacity) { m_Sensitive = new Dictionary(capacity); m_Insensitive = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); } public void Add(string key, Type type) { m_Sensitive[key] = type; m_Insensitive[key] = type; } public Type Get(string key, bool ignoreCase) { Type t = null; if (ignoreCase) m_Insensitive.TryGetValue(key, out t); else m_Sensitive.TryGetValue(key, out t); return t; } } }