Rework the Loading Mechanism
Lisias opened this issue · 13 comments
The KPSe.InstallChecker
stunt is nice, but it have a flaw:
When the user updates KSP from CurseForge or GitHub, the first boot will have two KSPe.dll
on memory - and so, the KSPe's initialisation was happening twice.
This is less then ideal, but not exactly (too much) bad because the Install Checker will get rid of one of them after updating and then will ask the user for rebooting KSP. However, I left one use case unhandled: when the user updates https://github.com/net-lisias-ksp/ModularManagement but KSPe itself wasn't upgraded in the process, and so the Install Checker just removes the KSPe.dll
inside GameData/000_KSPe
and call it a day.
And so we have two KSPes in memory, being initialised twice, what means it will try to load the auxiliary DLLs twice. An annoyance on KSP >= 1.8.0, but a problem on KSP < 1.8.0 as it will load KSPe (and the auxiliary DLLs) on a new AppDomain slowing up everything until the next KSP boot.
Things got a bit worse with the implementation of the Task #49 , however. Now the second KSPe that was silently being reloading the auxiliary DLLs are being yelled at, and so I broke the update process.
Task #49 is important, it will help me (and eventual clientes) to avoid messing up the loading process, as well be yet a new barrier against a really nasty installation problem where multiple copies if the Add'On is installed by accident on the GameData.
So the best way out of the mess is to implement a Loader for what is, now, KSPe.dll
(or 000_KSPe.dll
) and move it to This new Loader MUST:PluginData
, becoming an auxiliary DLL itself.
- Seamlessly insert itself in the toolchain:
Its Assembly will be namedKSPe
, and and the older one, now a new Auxiliary, will beKSPe.Main
.It will be loaded by KSP as usual, and so anything declaring a dependency onKSPe
will work fine - no cascading changes on the eco system.- It will have its own Versioning, apart from the rest of the toolchain.
- It will have no dependencies, with needed coded duplicated inside the DLL.
- Real duplication, no symlinks, otherwise I would need to bump the Version on every KSPe's change, defeating one of the main purposes of the thing.
This will give us the following benefits:
It will almost eliminate the need to reboot KSP whenKSPe.Main
is updated.Only when the Loader itself is updated this will be needed again, once the initial installation happens.
It will prevent the nowKSPe.Main
to be loaded twice at all circumstances.It will cause no impact on the current toolchainInstallChecker will work as usual, no changes needed.UserLand ditto.
EDIT: Requirements removed due a strategy change.
Solution implemented:
KSPe.InstallChecker
was moved intoGameData/000_KSPe.dll
- It is able to start up before anything else, preventing the race conditions that were screwing me up.
- It is able to update itself.
- It is still optional (i.e., can be removed from the distribution, but
KSPe
will not be the first thing loaded and will lose control of the whole installation).
KSPe
itself is now onGameData/001_KSPe.dll
The Loader can't have its own versioning, otherwise it will break clients using KSPAssmblyDependency
. The requirements were updated.
This is not going to fly.
In C++, we can write a Module Initialiser function easily, but on C# this is extremely convoluted unless you are using C# 9.
It doesn't worth the hassle - at least, for now.
More info: https://stackoverflow.com/questions/505237/net-running-code-when-assembly-is-loaded
The work done for this issue is preserved on the branch https://github.com/net-lisias-ksp/KSPe/tree/experimental/issue_50 for historical reference.
I had drawn myself to a corner. I will need to tackle this down somehow, or I will get screwed when updating CurseForge.
Thanks the problem:
Besides the idiosyncrasies on how KSP load things (and that got worse on KSP >= 1.8), I let a important detail pass trough: the KSPAssemblyVersion.
KSPe.UI
, KSPe.HMI
, etc, all of them rely on the KSPe
KSPAssembly directive to control dependency, not to mention the client add'ons that rely on it too.
On a CurseForge update, these Assemblies are bumped up but the 000_KSPe.dll
is still one version behind, and so they are not loaded, exploding Exceptions everywhere and preventing KSPe.InstallChecker
from doing it's job (what's ironic, as I had managed to solve the KSP's problems on it by now - #sigh).
So, now, no matter the trouble I'm going to have, I need to have this issue tackled down as it appears to be my only chance of doing a clean job.
This is going to hurt….
Part of the problem (running code when the DLL is loaded) is solved by using:
I'm not a huge fan of external dependencies (not to mention a brittle system as NuGet, creating a hard dependency on external repositories that can go dark at any moment), but at least on the short run this is working. I can reevaluate the dependencies later.
This is not going to fly the way I initially intended.
I can load an Assembly without problems on the ModuleInitializer
using System.Reflection.Assembly.Load(bytes[])
. What I can't do is loading a KSP Plugin (AssemblyLoader.LoadPlugin
) on the damned thing without a huge breakage on everybody later:
[EXC 19:16:28.043] NullReferenceException: Object reference not set to an instance of an object
KSPAPIExtensions.SystemUtils+<>c__DisplayClass1_0.<TypeElectionWinner>b__0 (.LoadedAssembly ass)
System.Linq.Enumerable+<CreateWhereIterator>c__Iterator1D`1[AssemblyLoader+LoadedAssembly].MoveNext ()
System.Linq.Enumerable+<CreateSelectIterator>c__Iterator10`2[AssemblyLoader+LoadedAssembly,<>f__AnonymousType0`2[AssemblyLoader+LoadedAssembly,System.Type]].MoveNext ()
System.Linq.Enumerable+<CreateWhereIterator>c__Iterator1D`1[<>f__AnonymousType0`2[AssemblyLoader+LoadedAssembly,System.Type]].MoveNext ()
System.Collections.Generic.List`1[<>f__AnonymousType0`2[AssemblyLoader+LoadedAssembly,System.Type]].AddEnumerable (IEnumerable`1 enumerable)
System.Collections.Generic.List`1[<>f__AnonymousType0`2[AssemblyLoader+LoadedAssembly,System.Type]]..ctor (IEnumerable`1 collection)
System.Linq.Enumerable.ToArray[<>f__AnonymousType0`2] (IEnumerable`1 source)
System.Linq.QuickSort`1[<>f__AnonymousType0`2[AssemblyLoader+LoadedAssembly,System.Type]]..ctor (IEnumerable`1 source, System.Linq.SortContext`1 context)
System.Linq.QuickSort`1+<Sort>c__Iterator21[<>f__AnonymousType0`2[AssemblyLoader+LoadedAssembly,System.Type]].MoveNext ()
System.Linq.Enumerable+<CreateSelectIterator>c__Iterator10`2[<>f__AnonymousType0`2[AssemblyLoader+LoadedAssembly,System.Type],System.Type].MoveNext ()
System.Collections.Generic.List`1[System.Type].AddEnumerable (IEnumerable`1 enumerable)
System.Collections.Generic.List`1[System.Type]..ctor (IEnumerable`1 collection)
System.Linq.Enumerable.ToArray[Type] (IEnumerable`1 source)
KSPAPIExtensions.SystemUtils.TypeElectionWinner (System.Type targetCls, System.String assemName)
KSPAPIExtensions.OnEditorUpdateUtility..cctor ()
Rethrow as TypeInitializationException: An exception was thrown by the type initializer for KSPAPIExtensions.OnEditorUpdateUtility
KSPAPIExtensions.OnEditorUpdateUtility_1_7_5_108..ctor ()
UnityEngine.GameObject:AddComponent(Type)
AddonLoader:StartAddon(LoadedAssembly, Type, KSPAddon, Startup)
AddonLoader:StartAddons(Startup)
<LoadObjects>c__Iterator1:MoveNext()
UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
<CreateDatabase>c__Iterator0:MoveNext()
UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
GameDatabase:StartLoad()
<LoadSystems>c__Iterator0:MoveNext()
UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
LoadingScreen:Start()
What kinda make sense, after all, as I was trying to load a plugin while a plugin was being loaded, I surelly broke something internal on the AssemblyLoader.LoadPlugin
.
So, nope. I'm not going to dynamically loading KSPe. KSPe.Loader
is dead, another approach is needed.
The experimental/issue_50
branch was updated with the most recent code.
Well… It ended not being exactly what I wanted, but it works (almost) the same. The ModuleInitializer
stunt guarantee that this code is run before anything else due a happy coincidence on how KSP load and start up things (TL;DR: I'm cheating the Assembly Loader/Resolver
by initialising the InstallChecker
before it initialises this little world).
This way, I managed to display a Modal asking the user to restart KSP before anything else could do it (including KSPe
itself).
Foreseeing that perhaps this stunt could need to be reversed by any reason, I'm also moving the KSPe.InstallChecker
to be loaded before KSPe
itself on GameData, so even by removing the ModuleInitializer
stunt, things still will work as intended in the future releases.
An emergencial release based on the 2.5.2.0 (2.5.2.1 and 2.5.2.2 were scrapped) will be issued, reducing to the minimum any changes on KSPe
to allow easier diagnosing if anything goes South on the damned thing.
Worked as intended on
- KSP 1.4.3
- KSP 1.12.5
So I'm assuming it will work fine on everything else (as long is supported).