ClientScript loading logic

This commit is contained in:
sardelka9515
2022-10-09 23:35:30 +08:00
parent 617dbc9812
commit fe53e01a4a
8 changed files with 104 additions and 203 deletions

View File

@ -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 };
}

View File

@ -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" )]

View File

@ -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;
}
/// <summary>
/// Connect to a server
/// </summary>

View File

@ -14,12 +14,12 @@ namespace RageCoop.Client.Scripting
protected static API API => Main.API;
/// <summary>
/// This method would be called from background thread, call <see cref="API.QueueAction(System.Action)"/> to dispatch it to main thread.
/// This method would be called from main thread, right after the constructor.
/// </summary>
public abstract void OnStart();
/// <summary>
/// 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 <see cref="System.AppDomain"/> is unloded but prior to <see cref="GTA.Script.Aborted"/>.
/// </summary>
public abstract void OnStop();

View File

@ -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,9 +48,30 @@ 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")
{
@ -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(() =>
{
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;
}

View File

@ -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
/// </summary>
public string Name { get; internal set; }
public string BaseDirectory { get; internal set; }
/// <summary>
/// A resource-specific folder that can be used to store your files.
/// </summary>
public string DataFolder { get; internal set; }
public string DataFolder => Path.Combine(BaseDirectory, "Data");
/// <summary>
/// Get all <see cref="ClientScript"/> instance in this resource.
/// </summary>
@ -39,56 +41,13 @@ namespace RageCoop.Client.Scripting
internal class Resources
{
static readonly API API = Main.API;
private readonly List<ClientResource> LoadedResources = new List<ClientResource>();
internal readonly ConcurrentDictionary<string, ClientResource> LoadedResources = new ConcurrentDictionary<string, ClientResource>();
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<Action> toLoad = new List<Action>(10);
var r = new ClientResource()
{
Logger = Main.Logger,
Logger = Main.API.Logger,
Scripts = new List<ClientScript>(),
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)
{
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)
var assemblies = new Dictionary<ResourceFile, Assembly>();
foreach (var file in Directory.GetFiles(resDir, "*", SearchOption.AllDirectories))
{
a();
}
LoadedResources.Add(r);
file.Close();
}
private bool LoadScriptsFromAssembly(ResourceFile rfile, Assembly assembly, string filename, ClientResource toload)
if (Path.GetFileName(file).CanBeIgnored()) { try { File.Delete(file); } catch { } continue; }
var relativeName = file.Substring(resDir.Length + 1).Replace('\\', '/');
var rfile = new ResourceFile()
{
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);
}
}

View File

@ -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)

View File

@ -59,7 +59,7 @@ namespace RageCoop.Server.Scripting
{
IsDirectory = true,
Name = dir.Substring(resDir.Length + 1).Replace('\\', '/')
}); ;
});
}
var assemblies = new Dictionary<ResourceFile, Assembly>();
foreach (var file in Directory.GetFiles(resDir, "*", SearchOption.AllDirectories))