1
0
Fork 0

Merge /u/jsb1685/fgdata/ branch next into next

https://sourceforge.net/p/flightgear/fgdata/merge-requests/164/
This commit is contained in:
Stuart Buchanan 2020-02-12 19:38:43 +00:00
commit 4b0d08975c
5 changed files with 357 additions and 84 deletions

5
Nasal/README.txt Normal file
View file

@ -0,0 +1,5 @@
FGDATA/Nasal/ contains nasal core modules (*.nas).
All .nas files in this directory will be loaded automatically while Flightgear starts.
The first level sub-directories will be scanned for .nas files and this files will be
also loaded while Flightgear starts unless disabled in defaults.xml.

View file

@ -30,110 +30,35 @@
# 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 _modules = {};
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));
logprint(2, "setlistener " ~ p.getPath() ~ " (" ~
size(_listeners[a.id]) ~ " listener(s) tracked for " ~
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 {
logprint(5, "Invalid number of arguments to maketimer()");
return;
}
logprint(2, size(_timers[a.id]) ~ " timer(s) tracked for " ~ a.id);
return _timers[a.id][-1];
}
if (io.load_nasal(main_nas, namespace)) {
var module = modules.Module.new(a.id, namespace, a.node);
module.setFilePath(a.basePath);
module.setMainFile("addon-main.nas");
if (module.load(a) != nil) {
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);
}
logprint(3, "Tracked resources after running the main() function of " ~
a.id ~ ":");
logprint(3, "#listeners: " ~ size(_listeners[a.id]));
logprint(3, "#timers: " ~ size(_timers[a.id]));
logprint(3, "Use log level DEBUG to see all calls to the " ~
"setlistener() and maketimer() wrappers.");
module.printTrackedResources();
} else {
logprint(5, "Failed loading addon-main.nas for " ~ a.id);
}
_modules[a.id] = module;
}
var remove = func(a) {
if (!a.node.getChild("loaded").getValue()) {
logprint(5, "! ", a.id, " was not fully loaded.");
}
logprint(5, "- Removing add-on ", a.id);
var namespace = getNamespaceName(a);
foreach (var id; _listeners[a.id]) {
logprint(3, "Removing listener " ~ id);
removelistener(id);
}
_listeners[a.id] = [];
logprint(3, "Stopping timers ");
foreach (var t; _timers[a.id]) {
if (typeof(t.stop) == "func") {
t.stop();
logprint(2, " .");
}
}
_timers[a.id] = [];
# 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] = {};
_modules[a.id].unload();
}
var _reloadFlags = {};

333
Nasal/modules.nas Normal file
View file

@ -0,0 +1,333 @@
#-------------------------------------------------------------------------------
# modules.nas - Nasal module helper for Add-ons and re-loadable modules
# author: jsb
# created: 12/2019
#-------------------------------------------------------------------------------
# modules.nas allowes to load and unload Nasal modules at runtime (e.g. without
# restarting Flightgear as a whole). It implements resource tracking for
# setlistener and maketimer to make unloading easier
#-------------------------------------------------------------------------------
# Example - generic module load:
#
# if (modules.isAvailable("foo_bar")) {
# modules.load("foo_bar");
# }
#
# Example - create an aircraft nasal system as module
# (e.g. for rapid reload while development)
#
# var my_foo_sys = modules.Module.new("my_aircraft_foo");
# my_foo_sys.setDebug(1);
# my_foo_sys.setFilePath(getprop("/sim/aircraft-dir")~"/Nasal");
# my_foo_sys.setMainFile("foo.nas");
# my_foo_sys.load();
#-------------------------------------------------------------------------------
var MODULES_DIR = getprop("/sim/fg-root")~"/Nasal/Modules/";
var MODULES_NODE = nil;
var _modules_available = {};
var _instances = {};
# Class to handle a nasal module at runtime
var Module = {
_orig_setlistener: setlistener,
_orig_maketimer: maketimer,
_orig_settimer: settimer,
# id: must be a string without special characters or spaces
# ns: optional namespace name
# node: optional property node for module management
new: func(id, ns = "", node = nil) {
if (!id) {
id = "_module_manager";
}
if (_instances[id] != nil) {
return _instances[id];
}
var obj = {
parents: [me],
_listeners: [],
_timers: [],
_debug: 0,
id: id,
version: 1,
file_path: MODULES_DIR,
main_file: "main.nas",
namespace: ns ? ns : id,
node: nil,
};
if (isa(node, props.Node)) {
obj.node = node
} else {
obj.node = MODULES_NODE.getNode(id, 1);
}
obj.reloadN = obj.node.initNode("reload", 0, "BOOL");
obj.loadedN = obj.node.initNode("loaded", 0, "BOOL");
obj.lcountN = obj.node.initNode("listeners", 0, "INT");
obj.tcountN = obj.node.initNode("timers", 0, "INT");
obj.lhitN = obj.node.initNode("listener-hits", 0, "INT");
obj.reloadL = setlistener(obj.reloadN, func(n) {
if (n.getValue()) {
n.setValue(0);
logprint(3, "Reload listener for ", obj.id, " triggered (",
obj.reloadL, ")");
obj.reload();
}
});
_instances[id] = obj;
return obj;
},
getNode: func { return me.node; },
getReloadNode: func { return me.reloadN; },
getNamespaceName: func { return me.namespace; },
getNamespace: func { return globals[me.namespace]; },
getFilePath: func { return me.file_path; },
#return variable from module namespace
get: func(var_name) {
return globals[me.namespace][var_name];
},
setDebug: func (debug = 1) {
me._debug = debug;
logprint(4, "Module "~me.id~" debug = "~debug);
},
setFilePath: func(path) {
if (io.is_directory(path)) {
if (substr(path, -1) != "/")
path ~= "/";
me.file_path = path;
return 1;
}
return 0;
},
setMainFile: func(filename) {
if (typeof(filename) == "scalar") {
me.main_file = filename;
}
else {
logprint("4", "setMainFile() needs a string parameter");
}
},
setNamespace: func(ns) {
if (typeof(ns) == "scalar") {
me.namespace = ns;
}
else {
logprint("4", "setNamespace() needs a string parameter");
}
},
# load module
# if no arguments are given, the Module object will be passed to main()
load: func(myargs...) {
me.loadedN.setBoolValue(0);
if (globals[me.namespace] == nil) {
globals[me.namespace] = {};
}
logprint(5, "Module.load() ", me.id);
me.lcountN.setIntValue(0);
me.tcountN.setIntValue(0);
me.lhitN.setIntValue(0);
me._redirect_setlistener();
me._redirect_maketimer();
me._redirect_settimer();
var filename = me.file_path~"/"~me.main_file;
if (io.load_nasal(filename, me.namespace)) {
var main = globals[me.namespace]["main"];
if (typeof(main) == "func") {
var module_args = [];
if (size(myargs) == 0) module_args = [me];
else module_args = myargs;
var errors = [];
call(main, module_args, errors);
if (size(errors)) {
debug.printerror(errors);
} else {
me.loadedN.setBoolValue(1);
}
} else {
me.loadedN.setBoolValue(1);
}
return me;
}
else { return nil; }
},
#-- unload a module and its tracked resources --
unload: func() {
if (!me.loadedN.getValue()) {
logprint(5, "! ", me.id, " was not fully loaded.");
}
if (globals[me.namespace] != nil
and typeof(globals[me.namespace]) == "hash")
{
logprint(5, "- Removing module ", me.id);
if (globals[me.namespace]["setlistener"] != nil)
globals[me.namespace]["setlistener"] = func {};
foreach (var id; me._listeners) {
logprint(3, "Removing listener "~id);
if (removelistener(id)) {
me.lcountN.setValue(me.lcountN.getValue() - 1);
}
}
me._listeners = [];
logprint(3, "Stopping timers ");
if (globals[me.namespace]["maketimer"] != nil)
globals[me.namespace]["maketimer"] = func {};
foreach (var t; me._timers) {
if (typeof(t.stop) == "func") {
t.stop();
me.tcountN.setValue(me.tcountN.getValue() - 1);
logprint(2, " .");
}
}
me._timers = [];
# call clean up method if available
# module shall release resources not handled by this framework
if (globals[me.namespace]["unload"] != nil
and typeof(globals[me.namespace]["unload"]) == "func") {
var errors = [];
call(globals[me.namespace].unload, [me], errors);
if (size(errors)) {
debug.printerror(errors);
}
}
me.loadedN.setBoolValue(0);
#kill namespace (replace with empty hash and hope GC cleans up behind us)
globals[me.namespace] = nil;
}
},
reload: func() {
me.unload();
me.load();
},
printTrackedResources: func(loglevel = 3) {
logprint(loglevel, "Tracked resources after running the main() function of " ~
me.id~":");
logprint(loglevel, "#listeners: "~size(me._listeners));
logprint(loglevel, "#timers: "~size(me._timers));
logprint(loglevel, "Use log level DEBUG to see all calls to the " ~
"setlistener() and maketimer() wrappers.");
},
# redirect setlistener() for module
_redirect_setlistener: func() {
globals[me.namespace].setlistener = func(p, f, start=0, runtime=0) {
if (!isa(p, props.Node)) {
p = props.getNode(p, 1).resolveAlias();
}
if (me._debug) {
var f_debug = func {
me.lhitN.setValue(me.lhitN.getValue() + 1);
if (int(me._debug) > 1) {
print("Listener hit for: ", p.getPath());
}
call(f, arg);
};
append(me._listeners, Module._orig_setlistener(p, f_debug, start, runtime));
var c = caller(1);
if (c != nil) {
print(sprintf("setlistener for %s called from %s:%s",
p.getPath(), io.basename(c[2]), c[3]));
};
} else {
append(me._listeners, Module._orig_setlistener(p,
f, start, runtime));
}
me.lcountN.setValue(me.lcountN.getValue() + 1);
}
me.setlistener = globals[me.namespace].setlistener;
},
# redirect maketimer for module
_redirect_maketimer: func() {
globals[me.namespace].maketimer = func() {
if (size(arg) == 2) {
append(me._timers, Module._orig_maketimer(arg[0], arg[1]));
} elsif (size(arg) == 3) {
append(me._timers,
Module._orig_maketimer(arg[0], arg[1], arg[2]));
} else {
logprint(5, "Invalid number of arguments to maketimer()");
return;
}
if (me._debug) {
var c = caller(1);
if (c != nil) {
print(sprintf("maketimer called from %s:%s",
io.basename(c[2]), c[3]));
};
}
me.tcountN.setValue(me.tcountN.getValue() + 1);
return me._timers[-1];
}
me.maketimer = globals[me.namespace].maketimer;
},
_redirect_settimer: func() {
globals[me.namespace].settimer = func() {
var c = caller(1);
logprint(5, sprintf("\n\Unsupported settimer() call from %s:%s. "~
"Use maketimer() instead.",
io.basename(c[2]), c[3]));
}
},
};
var isAvailable = func(name) {
return contains(_modules_available, name);
}
var _getInstance = func(name) {
if (isAvailable(name) and _instances[name] == nil) {
var m = Module.new(name);
m.setFilePath(MODULES_DIR~name);
}
return _instances[name];
}
var setDebug = func(name, debug=1) {
if (isAvailable(name)) {
var module = _getInstance(name);
module.setDebug(debug);
}
}
var load = func(name, ns="") {
var m = _getInstance(name);
if (m != nil) {
if (ns) { m.setNamespace(ns); }
return m.load();
}
else return 0;
}
# scan MODULES_DIR for subdirectories; it is assumed, that only well-formed
# modules are stored in that directories, so no further checks right here
var _findModules = func() {
var module_dirs = io.subdirectories(MODULES_DIR);
_modules_available = {};
foreach (var name; module_dirs) {
_modules_available[name] = 1;
}
}
#props.getNode is available only after nasal dir has been initialized
_setlistener("sim/signals/nasal-dir-initialized", func {
MODULES_NODE = props.getNode("/nasal/modules", 1);
_findModules();
});

View file

@ -0,0 +1,6 @@
The folder FGDATA/Nasal/modules contains optional nasal modules.
This modules will not be loaded automatically by Flightgear but can be
loaded on demand when needed, e.g. by an aircraft developer.
For more information see FGDATA/Nasal/modules.nas and the Flightgear wiki at
http://wiki.flightgear.org/Modules.nas

4
Nasal/modules/dummy.nas Normal file
View file

@ -0,0 +1,4 @@
#------------------------------------------
# dummy.nas
# empty file to avoid error message
#------------------------------------------