1
0
Fork 0

Add reload support to addons.nas; add tracking of listeners and timers

This commit is contained in:
Henning Stahlke 2018-12-05 12:33:58 +01:00 committed by Florent Rougon
parent e6fd70bd63
commit 5e2756b410
2 changed files with 167 additions and 16 deletions

View file

@ -35,6 +35,8 @@ Contents
9. Nasal API 9. Nasal API
10. Add-on development; in-sim reload of Nasal code
Introduction Introduction
------------ ------------
@ -827,6 +829,45 @@ Read-only data members (attributes) of addons.Maintainer objects:
to a web page from which people can subscribe to to a web page from which people can subscribe to
that mailing-list (string) 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 Footnotes
--------- ---------

View file

@ -29,22 +29,132 @@
# #
# For more details, see $FG_ROOT/Docs/README.add-ons. # 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 { var id = _setlistener("/sim/signals/fdm-initialized", func {
removelistener(id); removelistener(id);
orig_setlistener = setlistener;
foreach (var addon; addons.registeredAddons()) { orig_maketimer = maketimer;
var main_nas = addon.basePath ~ "/addon-main.nas"; addons.init();
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);
}
}) })