diff --git a/Docs/README.add-ons b/Docs/README.add-ons index 1fabb7a1b..1596cf0f6 100644 --- a/Docs/README.add-ons +++ b/Docs/README.add-ons @@ -35,6 +35,8 @@ Contents 9. Nasal API +10. Add-on development; in-sim reload of Nasal code + Introduction ------------ @@ -827,6 +829,45 @@ Read-only data members (attributes) of addons.Maintainer objects: to a web page from which people can subscribe to that mailing-list (string) + +10. Add-on development; in-sim reload of Nasal code +--------------------------------------------------- + +!!! WARNING: +!!! The reload feature is meant for developers only, it should not be made +!!! visible to end users. Unexpected side effects may occur due to reload, +!!! if not implemented correctly. +!!! We really don't want users to send bug reports due to reload going wrong. + +To make development of add-ons less time consuming, you can reload the +Nasal part of your add-on without having to restart FlightGear. The +addons.nas module will track setlistener() and maketimer() calls for +your add-on and remove listeners and stop timers on reload for you (the +add-on's own namespace, has setlistener() and maketimer() wrappers that +shadow and call themselves the standard setlistener() and maketimer() +functions). + +For the time being, you have to track any other resources outside the +namespace of your add-on by yourself and clean them up in the unload() +function, e.g. delete canvas or close any files you opened. + +You can define this unload() function in the addon-main.nas file. When +your add-on is reloaded, its unload() function, if defined, will be +called with one argument: the addons.Addon object (a Nasal ghost) +corresponding to your add-on. unload() is run in the add-on's own +namespace, therefore it sees the aforementioned setlistener() and +maketimer() wrapper functions. + +The reload is triggered by setting /addon/by-id/yourAddonIDhere/reload +to true. A listener will react to that property, reset it to false and +attempt to reload the Nasal file (doing the cleanup before). You can add +a menu item to trigger the reload easily, but this should be removed +before publishing your add-on to users. + +Please have a look at the skeleton add-on at +https://sourceforge.net/p/flightgear/fgaddon/HEAD/tree/trunk/Addons/Skeleton/ + + Footnotes --------- diff --git a/Nasal/addons.nas b/Nasal/addons.nas index 5d3baeaaf..ac6b074f5 100644 --- a/Nasal/addons.nas +++ b/Nasal/addons.nas @@ -29,22 +29,132 @@ # # For more details, see $FG_ROOT/Docs/README.add-ons. +# hashes to store listeners and timers per addon ID +var _listeners = {}; +var _timers = {}; +var orig_setlistener = nil; +var orig_maketimer = nil; + +var getNamespaceName = func(a) { + return "__addon[" ~ a.id ~ "]__"; +} + +var load = func(a) { + var main_nas = a.basePath ~ "/addon-main.nas"; + var namespace = getNamespaceName(a); + var loaded = a.node.getNode("loaded", 1); + loaded.setBoolValue(0); + + if (globals[namespace] == nil) { + globals[namespace] = {}; + } + + _listeners[a.id] = []; + _timers[a.id] = []; + + # redirect setlistener() for addon + globals[namespace].setlistener = func(p, f, start=0, runtime=1) { + # listeners won't work on aliases so find real node + if (typeof(p) == "scalar") { + p = props.getNode(p, 1); + while (p.getAttribute("alias")) { + p = p.getAliasTarget(); + } + } + append(_listeners[a.id], orig_setlistener(p, f, start, runtime)); + print("#listeners for " ~ a.id ~ " " ~ size(_listeners[a.id])); + } + + # redirect maketimer for addon + globals[namespace].maketimer = func() { + if (size(arg) == 2) { + append(_timers[a.id], orig_maketimer(arg[0], arg[1])); + } elsif (size(arg) == 3) { + append(_timers[a.id], orig_maketimer(arg[0], arg[1], arg[2])); + } else { + print("Invalid number of arguments to maketimer()"); + return; + } + print("#timers for " ~ a.id ~ " " ~ size(_timers[a.id])); + return _timers[a.id][-1]; + } + + logprint(5, "+ Loading " ~ main_nas ~ " into " ~ namespace); + + if (io.load_nasal(main_nas, namespace)) { + logprint(5, " [OK] '" ~ a.name ~ "' (V. " ~ a.version.str() ~ + ") loaded."); + var addon_main = globals[namespace]["main"]; + var addon_main_args = [a]; + var errors = []; + call(addon_main, addon_main_args, errors); + if (size(errors)) { + debug.printerror(errors); + } else { + loaded.setBoolValue(1); + } + } else { + logprint(5, " [Failed] '" ~ a.name ~ "'"); + } +} + + +var remove = func(a) { + if (!a.node.getChild("loaded").getValue()) { + print("! ", a.id, " was not fully loaded."); + } + + print("- Removing ", a.id); + var namespace = getNamespaceName(a); + foreach (var id; _listeners[a.id]) { + print(" Remove listener " ~ id); + removelistener(id); + } + + print(" Stopping timers "); + foreach (var t; _timers[a.id]) { + if (typeof(t.stop) == "func") { + t.stop(); + print("."); + } + } + + # call clean up method if available + # addon shall release resources not handled by addon framework + if (globals[namespace]["unload"] != nil + and typeof(globals[namespace]["unload"]) == "func") { + globals[namespace].unload(a); + } + globals[namespace] = {}; +} + +var _reloadFlags = {}; + +var reload = func(a) { + addons.remove(a); + addons.load(a); +} + +var init = func { + foreach (var addon; addons.registeredAddons()) { + addons._reloadFlags[addon.id] = addon.node.getNode("reload", 1); + addons._reloadFlags[addon.id].setBoolValue(0); + var makeListener = func(a) { + return func(n) { + if (n.getValue()) { + n.setValue(0); + addons.reload(a); + } + }; + } + setlistener(addons._reloadFlags[addon.id], makeListener(addon)); + addons.load(addon); + } +} + var id = _setlistener("/sim/signals/fdm-initialized", func { removelistener(id); - - foreach (var addon; addons.registeredAddons()) { - var main_nas = addon.basePath ~ "/addon-main.nas"; - var namespace = "__addon" ~ "[" ~ addon.id ~ "]__"; - logprint(5, "Initializing addon '" ~ addon.name ~ - "' version " ~ addon.version.str() ~ " from " ~ main_nas ~ - " in " ~ namespace); - io.load_nasal( main_nas, namespace ); - - var addon_main = globals[namespace]["main"]; - var addon_main_args = [ addon ]; - call(addon_main, addon_main_args); #, object, namespace, error_vector); - - # Tell the world that the add-on is now loaded. - addon.node.getChild("loaded", 0, 1).setBoolValue(1); - } + orig_setlistener = setlistener; + orig_maketimer = maketimer; + addons.init(); })