ClientScript loading logic
This commit is contained in:
@ -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 };
|
||||
}
|
||||
|
@ -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" )]
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
var assemblies = new Dictionary<ResourceFile, Assembly>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
Reference in New Issue
Block a user