temporary plugins AND/OR don't choke on 'The plugin x is already loaded.'

Posted by Fiendish on Tue 26 Apr 2011 09:03 AM — 17 posts, 73,278 views.

USA Global Moderator #0
So here's my back story...

I've decided that I want to make a dynamic z-ordering system for a bunch of plugins that all create miniwindows so that players can raise any miniwindow to the top of all other miniwindows (or at least the ones that subscribe to this system). In order to do this properly, I (unless I can figure out a better system) need to have a master table somewhere that keeps track of which (of 100 possible) positions are currently being used so that new plugins that get loaded that want to be in a position that is currently in use can be put into a free spot as appropriate.

But plugins can't set global world variables.
So I created a new plugin that has as its primary purpose in life, aside from containing supporting code which could have gone into a .lua file, the storage of this table. And rather than calling .lua functions directly from all the miniwindow plugins, I use CallPlugin. Ok, so far so good.

But now I have a strict dependency and I need to make sure that this monitor plugin is always loaded first, before any other plugin tries to register with it, for obvious reasons. So as a precaution I throw in:

if not IsPluginInstalled ("462b665ecb569efbf261422f") then
   LoadPlugin(GetPluginInfo(GetPluginID(),20).."aard_miniwindow_z_order_monitor.xml")

end

And that works too. Except that if a player ever goes to the Plugins... dialog and for some bonheaded-user reason reloads that plugin, when the world file saves it will have this extra bit in it that says

<include name="aard_miniwindow_z_order_monitor.xml" plugin="y" />

And THAT really messes things up, because that line will fail the next time the world is loaded, because the plugins before it have already loaded the plugin that it is trying to load. And for some reason that's a horrible catastrophe of world-ending proportions according to MUSHclient.

So for this convoluted reason, I'd like to see a possibility for flagging plugins as temporary (i.e. don't save to the world file ever) and/or make MUSHclient not choke on itself when trying to load plugins from the world file if they are already loaded.

Or let plugins set temporary global variables. Actually I'd really like that as well. I'm not a fan of brick walls in the code that I'm trying to interface with.
Amended on Tue 26 Apr 2011 09:08 AM by Fiendish
USA #1
I wrote a library called PPI that lets you handle plugin dependencies pretty well. I use it for plugins that use the GMCP helper plugin, for example. It uses OnPluginListChanged to see when a plugin's been added, removed, or reloaded, and executes a callback with a proxy object so you can communicate with a plugin.

I personally don't like LoadPlugin() because it causes race conditions (so to speak) and whatnot if you have multiple plugins using it. Just add the plugin once to the plugins list. I've never had anyone tell me they had a problem with doing this, and certainly a lot of people seem to use and like my plugins, so...

Here's the code you'd add to a "user" plugin.
PPI.OnLoad("plugin ID here", function(plugin)
  plugin.YourMethodHere()
end)

OnPluginListChanged = function()
  PPI.Refresh()
end


In the "provider" plugin you do something like this to expose methods:
PPI.Register("YourMethodHere", function()
  -- ...
end)


You can grab it from any of my plugins (like Roomname[1]) if you want to use it.

[1] http://jonathan.com/achaea/plugins/RoomName.plugin.zip (direct link)
Amended on Tue 26 Apr 2011 09:47 AM by Twisol
USA Global Moderator #2
I don't see how this mechanism will solve my original problem, which is that the very first thing my plugins need to do after loading is call a function from another plugin which means that the plugin gets loaded before the world file then tries (and fails verbosely) to load it again.

Honestly I MUSHclient should just silently ignore the attempt to load any plugin in the world file that is already loaded by the time it processes that line.

And (please please please) let me set global world variables from plugins so that I don't have to jump through these kinds of ridiculous hoops, because the restriction against it honestly doesn't make sense. The only given reason that I can see is that plugin authors *might* step on each others' variables. But plugin authors might step on each others' UIDs as well, and that doesn't seem to be a concern at all. Diligence in naming prevents all problems.

I guess maybe I could try using a text file as global variable storage instead...But what if I accidentally step on someone else's text file? Ugh.
Amended on Tue 26 Apr 2011 08:18 PM by Fiendish
USA #3
What if you wait for OnPuginListChanged to be called before calling LoadPlugin? It's called only after all plugins have been loaded, unlike OnPluginInstall, and continues to be called as plugins are added or removed.

As for how PPI would be helpful, my point stands that I dislike LoadPlugin. Are your plugins useless without the z-order plugin, or can they continue operating, just without the handy feature? If the latter, just enable your extras when the helper plugin is loaded. If the former, use Note() or error() during OnPluginListChanged if the plugin hasn't been loaded yet, telling the user to install it.


I do agree that it would be good to ignore duplicates if the plugin has the same filename, since that means it's already loaded. But I think that simply patching this leaves you open to other problems, like what happens when that plugin is removed, or if that plugin is deleted wholesale from the disk. If you design plugin dependencies to be pessimistic ("this plugin probably isn't there, but I hope it is"), things work out much better. And that's what PPI is for.
USA Global Moderator #4
Twisol said:
But I think that simply patching this leaves you open to other problems, like what happens when that plugin is removed, or if that plugin is deleted wholesale from the disk. If you design plugin dependencies to be pessimistic ("this plugin probably isn't there, but I hope it is"), things work out much better.

If I could just set global temporary variables there wouldn't need to be any plugin dependencies. :\
Amended on Tue 26 Apr 2011 08:31 PM by Fiendish
USA #5
Fiendish said:
If I could just set global temporary variables there wouldn't need to be any plugin dependencies. :\

That's gross for the same reason global variables in normal programs are gross. =/
USA Global Moderator #6
Quote:
But I think that simply patching this leaves you open to other problems, like what happens when that plugin is removed, or if that plugin is deleted wholesale from the disk
Eh, CallPlugin already has a provision for displaying an appropriate message if the called plugin is not present. I don't see how PPI helps much with that. If the plugin is just removed, the next launch of MUSHclient should solve that problem because other plugins re-load it. Except that right now it doesn't work properly because that makes MUSHclient spit out error messages that it really doesn't need to show.

For my purposes it's honestly even simpler than that. The data stored in this plugin is only used while plugins are loading. Anything that happens later is unimportant. So the player can remove it and nothing bad will happen at all. But see above.

I really am just intending to use this as a global data storage module. Because that's the only way to make this work right without N^2 message passing and array comparisons. It would be super nice if I didn't have to keep beating MUSHclient with the "let me do what I want, damnit!" stick.

Twisol said:

Fiendish said:
If I could just set global temporary variables there wouldn't need to be any plugin dependencies. :\

That's gross for the same reason global variables in normal programs are gross. =/

I disagree completely. First, global state absolutely has its place in normal programs and anyone who teaches otherwise is being harmfully dogmatic. The singleton pattern is hugely useful in practical coding. Second, I'm trying to construct something that just does not fit into the original idea of how plugins were never supposed to interact. IMO it was a foolish decision. There is no window manager here, so I don't have any other way of cleanly doing what I want to do. And your only objection is some hand-wavy "it's gross" without qualification. Well I say that having to, because I don't have any semblance of control over plugin load order, muck about with circuitous code paths all over the place that have to pass dozens of messages back and forth saying "Are you there? No? How about now? No? Damn. Now? Oh no too late! Abort!" is "gross". Having to even consider using the PPI library, even though it doesn't actually solve this problem, is "gross". Judiciously using a global array in the absence of an ability to guarantee that a certain plugin is always loaded first as a singleton seems far less gross.
Amended on Tue 26 Apr 2011 09:01 PM by Fiendish
USA #7
Fiendish said:
First, global state absolutely has its place in normal programs and anyone who teaches otherwise is being harmfully dogmatic.

Of course. But it shouldn't be the first choice - there are usually better ways to design an architecture. I offered mine, and unless it's shown to be flawed, I can't personally condone global variables.

Fiendish said:
Second, I'm trying to construct something that just does not fit into the original idea of how plugins were never supposed to interact. IMO it was a foolish decision. There is no window manager here, so I don't have any other way of cleanly doing what I want to do. And your only objection is some hand-wavy "it's gross" without qualification. Well I say that having to, because I don't have any semblance of control over plugin load order, muck about with circuitous code paths all over the place that have to pass dozens of messages back and forth saying "Are you there? No? How about now? No? Damn. Now? Oh no too late! Abort!" is "gross".

Eh, no, sorry, that's not how it works. It's meant to behave similarly to MUSHclient's current event-based model. You register a listener for an event (in this case, a particular plugin being loaded), and you act when it happens. On the other plugin's side, it tells PPI what functions to expose over the interface. PPI handles everything from there and it's a relatively seamless communications layer.

Also, I said it's gross for the same reason global variables are gross. I didn't offer that as my reason, I just ditto'd the argument against global state without stating it outright. But since you asked, it's gross because it's extremely easy to change the value from a location you never anticipated. In other words, I code defensively (i.e. I make my functions local instead of global so other plugins can't call them), and global state makes it very hard to code defensively.

Fiendish said:
Having to even consider using the PPI library, even though it doesn't actually solve this problem, is "gross". Judiciously using a global array in the absence of an ability to guarantee that a certain plugin is always loaded first as a singleton seems far less gross.

My approach is founded on the idea that your helper plugin is "merely" a z-order tool, and miniwindows can work just fine without an explicit z-order. (They certainly have to date.) It should be possible to make the z-order benefit an optional feature, only enabled when the helper plugin has been loaded. This is a good thing: it's defensive coding.

[EDIT]
Fiendish said:
Eh, CallPlugin already has a provision for displaying an appropriate message if the called plugin is not present.

Meh. I prefer permission over forgiveness; that's another part of my defensive coding tactic.
Amended on Tue 26 Apr 2011 09:03 PM by Twisol
USA Global Moderator #8
Twisol said:

But since you asked, it's gross because it's extremely easy to change the value from a location you never anticipated.

Other programmers changing things that you never expected is good! It's so disheartening to see people who get extremely myopic about their code like this. "My intent is the only one that matters." does nothing other than block people later on from doing more imaginative things with your code.

Quote:
In other words, I code defensively (i.e. I make my functions local instead of global so other plugins can't call them), and global state makes it very hard to code defensively.
In other words you do everything that I've always hated about dogmatic OO programmers. ;p

I loathe when a programmer decides to code against other programmers who know just as well what they're doing. I've struggled with foolishly incomplete PIMPL classes for years because someone decided that privacy is a pristine programming paradigm that one should always strive for. Only you can never realize the full potential ahead of time of what someone else later on would be able to do with your library if only you'd given them proper access. I'm trying to create something new here, and you're saying "Eh, it wasn't designed that way. Work around my dogmatism.".

Twisol said:

My approach is founded on the idea that your helper plugin is "merely" a z-order tool, and miniwindows can work just fine without an explicit z-order. (They certainly have to date.) It should be possible to make the z-order benefit an optional feature, only enabled when the helper plugin has been loaded. This is a good thing: it's defensive coding.
It's not a good thing to be forced to design around the possibility of a user doing something stupid (reloading a plugin accidentally) when you should be able to just make it so that the user can't do anything stupid unintentionally (not having to have a plugin in the first place). We're talking about plugins made by me trying to use a system made by me. It is not merely a tool. It is the framework for a window management system that really shouldn't have to be made into a plugin except for one single globally accessible array. A system that I want to build into all of my plugins. Except you're saying "but plugins don't work that way!" And I'm saying "Yes, well the current plugin boundaries are insufficient to house my creativity without a significant amount of time spent bashing against brick walls looking for a crack. But with a minor change or two it would all work just fine."

I thought the point of plugins was to save effort, not require more of it.

Amended on Tue 26 Apr 2011 09:39 PM by Fiendish
USA #9
Fiendish said:
Other programmers changing things that you never expected is good! It's so disheartening to see people who get extremely myopic about their code like this. "My intent is the only one that matters." does nothing other than block people later on from doing more imaginative things with your code.

...no, actually, it's about consistent state. I'm one of the more vocal protesters against some of MUSHclient's API decisions for exactly the reason of "why limit it?", but I don't suggest breaking down the walls that guard the data. Data needs to stay consistent. Global data is harder to keep consistent. I could care less if someone used my data to do something I didn't intend, unless it causes inconsistencies (which usually turn out to be bugs).

Fiendish said:
In other words you do everything that I've always hated about dogmatic OO programmers. ;p

I code against bugs, not programmers. Defensive drivers drive against accidents, not other drivers. Please.

Fiendish said:
We're talking about plugins made by me trying to use a system made by me.

And I'm talking about outside influences affecting your shared state, causing your plugins to fail in unexpected ways. Global state is global, not "private to these particular plugins". And world variables are visible to the user in their variables list. They're gonna think, "wtf is this? *delete*".

Fiendish said:
I'm saying "Yes, well the current plugin boundaries are insufficient to house my creativity without a significant amount of time spent bashing against brick walls looking for a crack. But with a minor change or two it would all work just fine."

And I can appreciate that. I already voiced my support for not choking on loading the same plugin twice. I'm just trying to suggest an alternative which I truly think is better.

Also, MUSHclient is made for everyone, not just you. (This isn't an attack, it's a fact.) Nick has to keep in mind what's best for the client, not what one other user believes is best. Enabling shared global state without data control, I believe, is not something that's best for the client. A plugin can erect checks around its data to ensure consistency.

Fiendish said:
I thought the point of plugins was to save effort, not require more of it.

Do you want me to make a proof of concept using PPI? I will, I really will if I have to. :P
Amended on Tue 26 Apr 2011 09:41 PM by Twisol
Australia Forum Administrator #10
Whoa!

Fiendish said:

But now I have a strict dependency and I need to make sure that this monitor plugin is always loaded first, before any other plugin tries to register with it, for obvious reasons.


Right, you have created a strict dependency, and now it is tripping you up. Why not have a design that doesn't require an exact loading order?

Back into the mists of time for a moment, I thought this sounded familiar. For one thing, my ATCP mapper needed the ATCP plugin before it would work. And, closer to what you are attempting, when I did a proof-of-concept of a MUD caching system, I wanted to have multiple plugins access a single lot of shared data (in-memory cache <-- from disk cache <-- server cache).

So, how did I do that without getting bogged down in arguments about OOP methods?

Well it hinges on one of Twisol's ideas - that is that the plugin needs to know when all the plugins have been loaded (eg. at world startup), or when the plugins list has changed (eg. the user adds/deletes/reloads stuff).

So in ATCP_Mapper.xml we have this:


function OnPluginListChanged ()
  do_plugin_check_now ("85f72d0e263d75df7bde6f00", "ATCP_NJG")    -- check we have ATCP plugin
end -- OnPluginListChanged


So once all plugins have been loaded, and the order doesn't matter, we now check that we have all the plugins we want.

This particular function, in checkplugin.lua does this:


function do_plugin_check_now (id, name)

  if IsPluginInstalled (id) then
    return  -- all is well
  end -- plugin is installed

  ColourNote ("white", "green", "Plugin '" .. name .. "' not installed. Attempting to install it...") 
  LoadPlugin (GetPluginInfo(GetPluginID (), 20) .. name .. ".xml") 

  if IsPluginInstalled (id) then
    ColourNote ("white", "green", "Success!") 
    return  -- all is well ... now
  end -- plugin is installed
    
  ColourNote ("white", "red", string.rep ("-", 80))
  ColourNote ("white", "red", "Plugin '" .. name .. "' not installed. Please download and install it.") 
  ColourNote ("white", "red", "It is required for the correct operation of the " ..
              GetPluginName () .. " plugin.")
  ColourNote ("white", "red", string.rep ("-", 80))

end -- do_plugin_check_now


So it tries to load the required plugin, and if that fails, then it gives up and notifies the user.

Similarly my (original) mapper does this:


function OnPluginListChanged ()
  item_ppi = load_ppi ("928dc37b201539cd14239ff0", "Item_Cache_Helper")
end -- OnPluginListChanged


This "item cache helper" is effectively your "global variables" plugin, except that they are just the variables needed for this subsystem.

If you can see a major flaw, please let me know. But basically instead of expecting to be able to do everything in the "install" part you wait for the "plugin list changed" part, with a couple of flags to let you know whether this is the first time through or not.
USA Global Moderator #11
Nick Gammon said:

Whoa!

Heh, sorry. I got a bit carried away.

Anyway, this will probably work. But it's still a bunch of code that works around the problem that MUSHclient throws an error when trying to load a plugin from the world file that is already loaded, when it seems like just skipping those and not throwing an error would be a good thing.
Amended on Wed 27 Apr 2011 06:26 AM by Fiendish
USA Global Moderator #12
I just hit this issue again. And I still think that MUSHclient should silently ignore already loaded plugins.
Australia Forum Administrator #13
Remember, plugins have a sequence parameter now.

And when you say "MUSHclient should" do you mean the Plugins dialog box, or the LoadPlugin function? Or something else?
USA Global Moderator #14
Ah, sorry. I mean referring to this statement in my last post from 2011...
Quote:
MUSHclient throws an error when trying to load a plugin from the world file that is already loaded, when it seems like just skipping those and not throwing an error would be a good thing.
Australia Forum Administrator #15
I had a look at that for the latest release of MUSHclient. It is fiendishly hard to do what you want.

Either:

* The new plugin succeeds in loading, and takes its place in the list; or

* An error is thrown, and the new plugin is not added to the list


For a plugin to be ignored, but not throw an error, is quite fiddly.
USA Global Moderator #16
Ok. No biggie.