using System; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using ICSharpCode.SharpZipLib.Zip; using McMaster.NETCore.Plugins; using RageCoop.Core; using RageCoop.Core.Scripting; namespace RageCoop.Server.Scripting; /// /// A class representing a server side resource, each resource is isolated from another and will be started alongside /// the server. /// public class ServerResource : PluginLoader { /// /// Get a instance that can be used to show information in console. /// public Logger Logger; internal ServerResource(PluginConfig config) : base(config) { } /// /// Name of the resource /// public string Name { get; internal set; } /// /// A resource-specific folder that can be used to store your files. /// public string DataFolder { get; internal set; } /// /// Get all instance in this resource /// public Dictionary Scripts { get; internal set; } = new(); /// /// Get all that can be used to acces files in this resource /// public Dictionary Files { get; internal set; } = new(); internal static ServerResource LoadFrom(string resDir, string dataFolder, Logger logger = null) { string mainAssemblyPath = Path.Combine(resDir, Path.GetFileName(resDir) + ".dll"); if (!File.Exists(mainAssemblyPath)) throw new FileNotFoundException($"Main assemby not found: {mainAssemblyPath}"); var runtimeLibs = Path.Combine(resDir, "RuntimeLibs", CoreUtils.GetInvariantRID()); if (Directory.Exists(runtimeLibs)) { logger?.Debug("Applying runtime libraries from " + CoreUtils.GetInvariantRID()); CoreUtils.CopyFilesRecursively(new DirectoryInfo(runtimeLibs), new DirectoryInfo(resDir)); } runtimeLibs = Path.Combine(resDir, "RuntimeLibs", RuntimeInformation.RuntimeIdentifier); if (Directory.Exists(runtimeLibs)) { logger?.Debug("Applying runtime libraries from " + CoreUtils.GetInvariantRID()); CoreUtils.CopyFilesRecursively(new DirectoryInfo(runtimeLibs), new DirectoryInfo(resDir)); } var conf = new PluginConfig(Path.GetFullPath(mainAssemblyPath)) { PreferSharedTypes = true, EnableHotReload = false, IsUnloadable = false, LoadInMemory = true }; ServerResource r = new(conf) { Logger = logger, Name = Path.GetFileName(resDir) }; if (!File.Exists(conf.MainAssemblyPath)) { r.Dispose(); throw new FileNotFoundException($"Main assembly for resource \"{r.Name}\" cannot be found."); } r.Scripts = new Dictionary(); r.DataFolder = Path.Combine(dataFolder, r.Name); r.Reloaded += (s, e) => { r.Logger?.Info($"Resource: {r.Name} has been reloaded"); }; Directory.CreateDirectory(r.DataFolder); foreach (var dir in Directory.GetDirectories(resDir, "*", SearchOption.AllDirectories)) r.Files.Add(dir, new ResourceFile { IsDirectory = true, Name = dir.Substring(resDir.Length + 1).Replace('\\', '/') }); var assemblies = new Dictionary(); foreach (var file in Directory.GetFiles(resDir, "*", SearchOption.AllDirectories)) { if (Path.GetFileName(file).CanBeIgnored()) { try { File.Delete(file); } catch { } continue; } var relativeName = file.Substring(resDir.Length + 1).Replace('\\', '/'); var rfile = new ResourceFile { GetStream = () => { return new FileStream(file, FileMode.Open, FileAccess.Read); }, IsDirectory = false, Name = relativeName }; if (file.EndsWith(".dll") && !relativeName.Contains('/') && IsManagedAssembly(file)) assemblies.Add(rfile, r.LoadAssemblyFromPath(Path.GetFullPath(file))); r.Files.Add(relativeName, rfile); } foreach (var a in assemblies) if (a.Key.Name.ToLower() == r.Name.ToLower() + ".dll") try { r.LoadScriptsFromAssembly(a.Key, a.Value); } catch (FileLoadException ex) { if (!ex.Message.EndsWith("Assembly with same name is already loaded")) { logger?.Warning("Failed to load assembly: " + a.Key.Name); logger?.Trace(ex.Message); } } return r; } internal static ServerResource LoadFrom(Stream input, string name, string tmpDir, string dataFolder, Logger logger = null) { tmpDir = Path.Combine(tmpDir, name); if (Directory.Exists(tmpDir)) Directory.Delete(tmpDir, true); Directory.CreateDirectory(tmpDir); new FastZip().ExtractZip(input, tmpDir, FastZip.Overwrite.Always, null, null, null, true, true); return LoadFrom(tmpDir, dataFolder, logger); } private bool LoadScriptsFromAssembly(ResourceFile rfile, Assembly assembly) { var count = 0; try { // Find all script types in the assembly foreach (var type in assembly.GetTypes().Where(x => x.IsSubclassOf(typeof(ServerScript)))) { var constructor = type.GetConstructor(Type.EmptyTypes); if (constructor != null && constructor.IsPublic) try { // Invoke script constructor var script = constructor.Invoke(null) as ServerScript; script.CurrentResource = this; script.CurrentFile = rfile; Scripts.Add(script.GetType().FullName, script); count++; } catch (Exception ex) { Logger?.Error($"Error occurred when loading script: {type.FullName}."); Logger?.Error(ex); } else Logger?.Error($"Script {type.FullName} has an invalid contructor."); } } catch (ReflectionTypeLoadException ex) { Logger?.Error($"Failed to load assembly {rfile.Name}: "); Logger?.Error(ex); foreach (var e in ex.LoaderExceptions) Logger?.Error(e); return false; } if (count != 0) Logger?.Info($"Loaded {count} script(s) in {rfile.Name}"); return count != 0; } internal new void Dispose() { base.Dispose(); } private static bool IsManagedAssembly(string filename) { try { using (Stream file = new FileStream(filename, FileMode.Open, FileAccess.Read)) { if (file.Length < 64) return false; using (var bin = new BinaryReader(file)) { // PE header starts at offset 0x3C (60). Its a 4 byte header. file.Position = 0x3C; var offset = bin.ReadUInt32(); if (offset == 0) offset = 0x80; // Ensure there is at least enough room for the following structures: // 24 byte PE Signature & Header // 28 byte Standard Fields (24 bytes for PE32+) // 68 byte NT Fields (88 bytes for PE32+) // >= 128 byte Data Dictionary Table if (offset > file.Length - 256) return false; // Check the PE signature. Should equal 'PE\0\0'. file.Position = offset; if (bin.ReadUInt32() != 0x00004550) return false; // Read PE magic number from Standard Fields to determine format. file.Position += 20; var peFormat = bin.ReadUInt16(); if (peFormat != 0x10b /* PE32 */ && peFormat != 0x20b /* PE32Plus */) return false; // Read the 15th Data Dictionary RVA field which contains the CLI header RVA. // When this is non-zero then the file contains CLI data otherwise not. file.Position = offset + (peFormat == 0x10b ? 232 : 248); return bin.ReadUInt32() != 0; } } } catch { // This is likely not a valid assembly if any IO exceptions occur during reading return false; } } }