diff --git a/Nasal/README.txt b/Nasal/README.txt new file mode 100644 index 000000000..c576e4338 --- /dev/null +++ b/Nasal/README.txt @@ -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. diff --git a/Nasal/addons.nas b/Nasal/addons.nas index cb0cc52bd..fa6f32db6 100644 --- a/Nasal/addons.nas +++ b/Nasal/addons.nas @@ -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 = {}; diff --git a/Nasal/modules.nas b/Nasal/modules.nas new file mode 100644 index 000000000..f41e9e9f0 --- /dev/null +++ b/Nasal/modules.nas @@ -0,0 +1,322 @@ +#------------------------------------------------------------------------------- +# 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"); + + setlistener(obj.reloadN, func(n) { + if (n.getValue()) { + n.setValue(0); + obj.reload(); + } + }); + 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._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); + removelistener(id); + } + 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(); + 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); + 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); + _instances[name] = m; + } + 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; + } +} + +_setlistener("sim/signals/nasal-dir-initialized", func { + MODULES_NODE = props.getNode("/nasal/modules", 1); + _findModules(); +}); diff --git a/Nasal/modules/README-modules.txt b/Nasal/modules/README-modules.txt new file mode 100644 index 000000000..894c5970e --- /dev/null +++ b/Nasal/modules/README-modules.txt @@ -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 diff --git a/Nasal/modules/dummy.nas b/Nasal/modules/dummy.nas new file mode 100644 index 000000000..a70ad347a --- /dev/null +++ b/Nasal/modules/dummy.nas @@ -0,0 +1,4 @@ +#------------------------------------------ +# dummy.nas +# empty file to avoid error message +#------------------------------------------