From fe53e01a4a01706dd3433a15e52d6bc77bbca514 Mon Sep 17 00:00:00 2001 From: sardelka9515 Date: Sun, 9 Oct 2022 23:35:30 +0800 Subject: [PATCH] ClientScript loading logic --- RageCoop.Client/Networking/DownloadManager.cs | 2 +- RageCoop.Client/Properties/AssemblyInfo.cs | 4 +- RageCoop.Client/Scripting/API.cs | 8 + RageCoop.Client/Scripting/ClientScript.cs | 4 +- RageCoop.Client/Scripting/ResourceDomain.cs | 64 ++++- RageCoop.Client/Scripting/Resources.cs | 219 +++--------------- RageCoop.Core/CoreUtils.cs | 4 + RageCoop.Server/Scripting/ServerResource.cs | 2 +- 8 files changed, 104 insertions(+), 203 deletions(-) diff --git a/RageCoop.Client/Networking/DownloadManager.cs b/RageCoop.Client/Networking/DownloadManager.cs index 8193181..381bf64 100644 --- a/RageCoop.Client/Networking/DownloadManager.cs +++ b/RageCoop.Client/Networking/DownloadManager.cs @@ -49,7 +49,7 @@ namespace RageCoop.Client } catch (Exception ex) { - Main.Logger.Error("Error occurred when loading server resource:"); + Main.Logger.Error("Error occurred when loading server resource"); Main.Logger.Error(ex); return new Packets.FileTransferResponse() { ID = 0, Response = FileResponse.LoadFailed }; } diff --git a/RageCoop.Client/Properties/AssemblyInfo.cs b/RageCoop.Client/Properties/AssemblyInfo.cs index 71d7865..893c7b2 100644 --- a/RageCoop.Client/Properties/AssemblyInfo.cs +++ b/RageCoop.Client/Properties/AssemblyInfo.cs @@ -16,7 +16,7 @@ using System.Resources; // Version informationr( -[assembly: AssemblyVersion("1.5.4.184")] -[assembly: AssemblyFileVersion("1.5.4.184")] +[assembly: AssemblyVersion("1.5.4.186")] +[assembly: AssemblyFileVersion("1.5.4.186")] [assembly: NeutralResourcesLanguageAttribute( "en-US" )] diff --git a/RageCoop.Client/Scripting/API.cs b/RageCoop.Client/Scripting/API.cs index a448f8f..8e9d49d 100644 --- a/RageCoop.Client/Scripting/API.cs +++ b/RageCoop.Client/Scripting/API.cs @@ -5,6 +5,7 @@ using RageCoop.Core; using RageCoop.Core.Scripting; using System; using System.Collections.Generic; +using System.Linq; using System.Windows.Forms; namespace RageCoop.Client.Scripting @@ -214,6 +215,13 @@ namespace RageCoop.Client.Scripting #endregion #region FUNCTIONS + public ClientResource GetResource(string name) + { + if(Main.Resources.LoadedResources.TryGetValue(name.ToLower(), out var res)){ + return res; + } + return null; + } /// /// Connect to a server /// diff --git a/RageCoop.Client/Scripting/ClientScript.cs b/RageCoop.Client/Scripting/ClientScript.cs index 6a0aff9..ad2ecc3 100644 --- a/RageCoop.Client/Scripting/ClientScript.cs +++ b/RageCoop.Client/Scripting/ClientScript.cs @@ -14,12 +14,12 @@ namespace RageCoop.Client.Scripting protected static API API => Main.API; /// - /// This method would be called from background thread, call to dispatch it to main thread. + /// This method would be called from main thread, right after the constructor. /// public abstract void OnStart(); /// - /// This method would be called from background thread when the client disconnected from the server, you MUST terminate all background jobs/threads in this method. + /// This method would be called from main thread right before the whole is unloded but prior to . /// public abstract void OnStop(); diff --git a/RageCoop.Client/Scripting/ResourceDomain.cs b/RageCoop.Client/Scripting/ResourceDomain.cs index cd9abd7..8c248a7 100644 --- a/RageCoop.Client/Scripting/ResourceDomain.cs +++ b/RageCoop.Client/Scripting/ResourceDomain.cs @@ -8,6 +8,7 @@ using System.Windows.Forms; using System.Collections.Generic; using System.Linq; using System.Collections.Concurrent; +using static System.Net.WebRequestMethods; namespace RageCoop.Client.Scripting { @@ -36,6 +37,8 @@ namespace RageCoop.Client.Scripting // Bridge to current ScriptDomain primary.Tick += Tick; primary.KeyEvent += KeyEvent; + CurrentDomain.Start(); + SetupScripts(); AppDomain.CurrentDomain.SetData("Primary", false); Console.WriteLine("Loaded scondary domain: " + AppDomain.CurrentDomain.Id + " " + Util.IsPrimaryDomain); } @@ -45,10 +48,31 @@ namespace RageCoop.Client.Scripting } public void SetupScripts() { - foreach(var s in ScriptDomain.CurrentDomain.RunningScripts) + foreach (var s in GetClientScripts()) { + + try + { + var script = (ClientScript)s; + var res = Main.API.GetResource(Path.GetFileName(Directory.GetParent(script.Filename).FullName)); + if (res == null) { Main.API.Logger.Warning("Failed to locate resource for script: " + script.Filename); continue; } + script.CurrentResource = res; + script.CurrentFile = res.Files.Values.Where(x => x.Name.ToLower() == script.Filename.Substring(res.BaseDirectory.Length + 1).Replace('\\', '/')).FirstOrDefault(); + res.Scripts.Add(script); + script.OnStart(); + } + catch (Exception ex) + { + Main.API.Logger.Error($"Failed to start {s.GetType().FullName}", ex); + } } } + public object[] GetClientScripts() + { + return ScriptDomain.CurrentDomain.RunningScripts.Where(x => + x.ScriptInstance.GetType().IsAssignableFrom(typeof(ClientScript)) && + !x.ScriptInstance.GetType().IsAbstract).Select(x => x.ScriptInstance).ToArray(); + } public static ResourceDomain Load(string dir = @"RageCoop\Scripts\Debug") { lock (_loadedDomains) @@ -62,7 +86,7 @@ namespace RageCoop.Client.Scripting { throw new Exception("Already loaded"); } - ScriptDomain domain = null; + ScriptDomain sDomain = null; try { dir = Path.GetFullPath(dir); @@ -81,11 +105,10 @@ namespace RageCoop.Client.Scripting .Select(x => Assembly.Load(x.FullName).Location) .Where(x => !string.IsNullOrEmpty(x))); - domain = ScriptDomain.Load(Directory.GetParent(typeof(ScriptDomain).Assembly.Location).FullName, dir); - domain.AppDomain.SetData("Console", ScriptDomain.CurrentDomain.AppDomain.GetData("Console")); - domain.AppDomain.SetData("RageCoop.Client.API", API.GetInstance()); - _loadedDomains.TryAdd(dir, (ResourceDomain)domain.AppDomain.CreateInstanceFromAndUnwrap(typeof(ResourceDomain).Assembly.Location, typeof(ResourceDomain).FullName, false, BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { ScriptDomain.CurrentDomain, api.ToArray() }, null, null)); - domain.Start(); + sDomain = ScriptDomain.Load(Directory.GetParent(typeof(ScriptDomain).Assembly.Location).FullName, dir); + sDomain.AppDomain.SetData("Console", ScriptDomain.CurrentDomain.AppDomain.GetData("Console")); + sDomain.AppDomain.SetData("RageCoop.Client.API", API.GetInstance()); + _loadedDomains.TryAdd(dir, (ResourceDomain)sDomain.AppDomain.CreateInstanceFromAndUnwrap(typeof(ResourceDomain).Assembly.Location, typeof(ResourceDomain).FullName, false, BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { ScriptDomain.CurrentDomain, api.ToArray() }, null, null)); }); // Wait till next tick @@ -96,9 +119,9 @@ namespace RageCoop.Client.Scripting { GTA.UI.Notification.Show(ex.ToString()); Main.Logger.Error(ex); - if (domain != null) + if (sDomain != null) { - ScriptDomain.Unload(domain); + ScriptDomain.Unload(sDomain); } throw; } @@ -109,13 +132,19 @@ namespace RageCoop.Client.Scripting { lock (_loadedDomains) { + Exception ex=null; Main.QueueToMainThread(() => { - domain.Dispose(); - ScriptDomain.Unload(domain.CurrentDomain); - _loadedDomains.TryRemove(domain.BaseDirectory, out _); + try + { + domain.Dispose(); + ScriptDomain.Unload(domain.CurrentDomain); + _loadedDomains.TryRemove(domain.BaseDirectory, out _); + } + catch(Exception e) { ex = e; } }); GTA.Script.Yield(); + if(ex != null) { throw ex; } } } public static void Unload(string dir) @@ -145,6 +174,17 @@ namespace RageCoop.Client.Scripting public void Dispose() { + foreach(var s in GetClientScripts()) + { + try + { + ((ClientScript)s).OnStop(); + } + catch(Exception ex) + { + Main.API.Logger.Error($"Failed to stop {s.GetType().FullName}",ex); + } + } PrimaryDomain.Tick -= Tick; PrimaryDomain.KeyEvent -= KeyEvent; } diff --git a/RageCoop.Client/Scripting/Resources.cs b/RageCoop.Client/Scripting/Resources.cs index 6fba628..a7c087b 100644 --- a/RageCoop.Client/Scripting/Resources.cs +++ b/RageCoop.Client/Scripting/Resources.cs @@ -2,6 +2,7 @@ using RageCoop.Core; using RageCoop.Core.Scripting; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -18,10 +19,11 @@ namespace RageCoop.Client.Scripting /// Name of the resource /// public string Name { get; internal set; } + public string BaseDirectory { get; internal set; } /// /// A resource-specific folder that can be used to store your files. /// - public string DataFolder { get; internal set; } + public string DataFolder => Path.Combine(BaseDirectory, "Data"); /// /// Get all instance in this resource. /// @@ -39,56 +41,13 @@ namespace RageCoop.Client.Scripting internal class Resources { static readonly API API = Main.API; - private readonly List LoadedResources = new List(); + internal readonly ConcurrentDictionary LoadedResources = new ConcurrentDictionary(); private const string BaseScriptType = "RageCoop.Client.Scripting.ClientScript"; private Logger Logger { get; set; } public Resources() { Logger = Main.Logger; } - private void StartAll() - { - lock (LoadedResources) - { - foreach (var d in LoadedResources) - { - foreach (var s in d.Scripts) - { - try - { - s.CurrentResource = d; - s.OnStart(); - } - catch (Exception ex) - { - Logger.Error("Error occurred when starting script:" + s.GetType().FullName); - Logger?.Error(ex); - } - } - } - } - } - private void StopAll() - { - lock (LoadedResources) - { - foreach (var d in LoadedResources) - { - foreach (var s in d.Scripts) - { - try - { - s.OnStop(); - } - catch (Exception ex) - { - Logger.Error("Error occurred when stopping script:" + s.GetType().FullName); - Logger?.Error(ex); - } - } - } - } - } public void Load(string path, string[] zips) { LoadedResources.Clear(); @@ -96,166 +55,56 @@ namespace RageCoop.Client.Scripting { var zipPath = Path.Combine(path, zip); Logger?.Info($"Loading resource: {Path.GetFileNameWithoutExtension(zip)}"); - LoadResource(new ZipFile(zipPath), Path.Combine(path, "data")); + Unpack(zipPath, Path.Combine(path, "Data")); } - StartAll(); + ResourceDomain.Load(path); } public void Unload() { - StopAll(); ResourceDomain.UnloadAll(); } - private void LoadResource(ZipFile file, string dataFolderRoot) + private void Unpack(string zipPath, string dataFolderRoot) { - List toLoad = new List(10); var r = new ClientResource() { - Logger = Main.Logger, + Logger = Main.API.Logger, Scripts = new List(), - Name = Path.GetFileNameWithoutExtension(file.Name), - DataFolder = Path.Combine(dataFolderRoot, Path.GetFileNameWithoutExtension(file.Name)) + Name = Path.GetFileNameWithoutExtension(zipPath), + BaseDirectory = Path.Combine(Directory.GetParent(zipPath).FullName, Path.GetFileNameWithoutExtension(zipPath)) }; Directory.CreateDirectory(r.DataFolder); + var resDir = r.BaseDirectory; + if (Directory.Exists(resDir)) { Directory.Delete(resDir, true); } + else if (File.Exists(resDir)) { File.Delete(resDir); } + Directory.CreateDirectory(resDir); - foreach (ZipEntry entry in file) + new FastZip().ExtractZip(zipPath, resDir, null); + + + foreach (var dir in Directory.GetDirectories(resDir, "*", SearchOption.AllDirectories)) { - ResourceFile rFile; - r.Files.Add(entry.Name, rFile = new ResourceFile() + r.Files.Add(dir, new ResourceFile() { - Name = entry.Name, - IsDirectory = entry.IsDirectory, + IsDirectory = true, + Name = dir.Substring(resDir.Length + 1).Replace('\\', '/') }); - if (!entry.IsDirectory) + } + 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() { - rFile.GetStream = () => { return file.GetInputStream(entry); }; - if (entry.Name.EndsWith(".dll") && !entry.Name.Contains("/")) - { - // Don't load API assembly - if (Path.GetFileName(entry.Name).CanBeIgnored()) { continue; } - var tmp = Path.GetTempFileName(); - var f = File.OpenWrite(tmp); - rFile.GetStream().CopyTo(f); - f.Close(); - if (!IsManagedAssembly(tmp)) - { - continue; - } - var asm = Assembly.LoadFrom(tmp); - toLoad.Add(() => LoadScriptsFromAssembly(rFile, asm, entry.Name, r)); - } - } - } - foreach (var a in toLoad) - { - a(); - } - LoadedResources.Add(r); - file.Close(); - } - private bool LoadScriptsFromAssembly(ResourceFile rfile, Assembly assembly, string filename, ClientResource toload) - { - int count = 0; - - try - { - // Find all script types in the assembly - foreach (var type in assembly.GetTypes().Where(x => IsSubclassOf(x, BaseScriptType))) - { - ConstructorInfo constructor = type.GetConstructor(System.Type.EmptyTypes); - if (constructor != null && constructor.IsPublic) - { - try - { - // Invoke script constructor - var script = constructor.Invoke(null) as ClientScript; - // script.CurrentResource = toload; - script.CurrentFile = rfile; - script.CurrentResource = toload; - toload.Scripts.Add(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; + GetStream = () => { return new FileStream(file, FileMode.Open, FileAccess.Read); }, + IsDirectory = false, + Name = relativeName + }; + r.Files.Add(relativeName, rfile); } - Logger?.Info($"Loaded {count} script(s) in {rfile.Name}"); - return count != 0; - } - private bool IsManagedAssembly(string filename) - { - try - { - using (Stream file = new FileStream(filename, FileMode.Open, FileAccess.Read)) - { - if (file.Length < 64) - return false; - - using (BinaryReader bin = new BinaryReader(file)) - { - // PE header starts at offset 0x3C (60). Its a 4 byte header. - file.Position = 0x3C; - uint 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; - } - } - private bool IsSubclassOf(Type type, string baseTypeName) - { - for (Type t = type.BaseType; t != null; t = t.BaseType) - if (t.FullName == baseTypeName) - return true; - return false; + LoadedResources.TryAdd(r.Name.ToLower(), r); } } diff --git a/RageCoop.Core/CoreUtils.cs b/RageCoop.Core/CoreUtils.cs index dc1b717..f8680c0 100644 --- a/RageCoop.Core/CoreUtils.cs +++ b/RageCoop.Core/CoreUtils.cs @@ -36,6 +36,10 @@ namespace RageCoop.Core { return ToIgnore.Contains(Path.GetFileNameWithoutExtension(name)); } + public static string ToFullPath(this string path) + { + return Path.GetFullPath(path); + } public static void GetBytesFromObject(object obj, NetOutgoingMessage m) { switch (obj) diff --git a/RageCoop.Server/Scripting/ServerResource.cs b/RageCoop.Server/Scripting/ServerResource.cs index 51eff07..db9d5b8 100644 --- a/RageCoop.Server/Scripting/ServerResource.cs +++ b/RageCoop.Server/Scripting/ServerResource.cs @@ -59,7 +59,7 @@ namespace RageCoop.Server.Scripting { IsDirectory = true, Name = dir.Substring(resDir.Length + 1).Replace('\\', '/') - }); ; + }); } var assemblies = new Dictionary(); foreach (var file in Directory.GetFiles(resDir, "*", SearchOption.AllDirectories))