.. _65-lua:
The embedded *Lua* interpreter
==============================
As seen for :ref:`tasks <40-tasks-lua>` and :ref:`conditions <50-conditions-lua>`, an embedded
*Lua* interpreter can be used to run scripts to respectively execute structured tasks and
verify complex conditions without the need to execute external commands. This is generally
quicker for small operations, because the interpreter is already available while **whenever**
is running, it only needs to be initialized and provided the script that, in turn, has been
loaded at startup. Tasks (or checks) that would last even seconds, can be performed in a small
fraction of the time.
The embedded interpreter makes the standard *Lua* library available for scripts, which includes:
* string manipulation
* mathematics
* io and file operations
* access to OS information and facilities.
The `Lua manual `__ is the primary source for information about the
capabilities of the standard library.
Moreover, the embedded interpreter provides an isolation level that is comparable to the one
that can be achieved by running external commands: every time that a script has to be run, a new
interpreter is initialized, that normally works without interaction with other scripts that may
have run before, or may be running aside.
While the *Lua* language is supported in its entirety, the embedded interpreter has both some
limitations and some enhancements, that come from its awareness of being part of **whenever**.
.. tip::
While many of the features and configuration options described here may not be very useful
for casual scripts configured directly by editing the configuration file, they are intended
to be helpful when implementing specific tasks or conditions in a frontend.
.. _65-lua-configuration:
Configuration
-------------
Both *tasks* and *conditions* based on *Lua* scripts can configure the interpreter with
common parameters. Apart from the ``script`` parameter, that should contain the script to
be run as a plain string, the following configuration parameters are available:
* ``init_script_path``, the optional path to an initialization script, that can be used
to fine-tune the interpreter, for example by setting the module search path and other
specific options;
* ``variables_to_set``, an optional map that associates identifiers to values that the
interpreter will find already set when it starts running: the identifiers are plain
TOML keys and as such they will be available in the script as usual *Lua* identifiers,
and the values can only be of *boolean*, *numeric*, and *string* type;
* ``expected_results``, a map that associates identifiers (plain TOML keys here as well)
to simple values that can be of *boolean*, *numeric*, and *string* type: although not
mandatory, at least an expected result is necessary for *Lua* script based conditions
to perform a test that can either succeed or fail.
.. tip::
The script has to be provided as a literal using the ``script`` parameter (TOML
`literal strings `__, especially in the *multiline*
flavor, come in handy for this purpose), however, in case the execution of a script is
needed, that is available as a separate file, it is always possible to use the
``require`` and ``dofile()`` *Lua* functions to achieve this.
Both in *tasks* and *conditions*, the dictionary of espected result is checked when the
script finishes running. Depending on the value of the ``expect_all`` parameter (which is
*false* by default) the script is considered to have succeeded as follows:
* when ``expect_all`` is *true* and all the identifiers specified in the map hold exactly
the specified values,
* when ``expect_all`` is *false* and at least one of the identifiers holds the specified
value.
Because of the limitations in the types of values that can be provided in the configuration
file, the tests that **whenever** can perform after the script has finished running are
actually quite simple. However, of course, nothing forbids to perform more articulated tests
in the script itself and assign, for instance, a value to a boolean variable that depends
on the outcome of such tests.
.. note::
Since an independent interpreter is initialized for every *Lua* based item, the above
mentioned parameters are set specifically for every item at each run; at the moment there
is no way to change the default values for these parameters.
Even though every script gets its own, isolated *Lua* interpreter, **whenever** offers a
way for possibly different scripts to interoperate by sharing simple values, and to store
intermediate results for subsequent runs of the script associated to an item. Details on
this capability can be found below in the :ref:`Enhancements <65-lua-enhancements>`
section.
The Lua interpreter is initialized at each run by
* setting the additional variables, including the ones implicitly provided by **whenever**,
* implementing the extra functionalities, and
* executing the startup script,
exactly in this order.
.. _65-lua-initscript:
Initialization script
---------------------
For more fine-tuned initialization, that can even be common to several instances of the
interpreter, an initialization script in *Lua* can be specified in each item configuration
using the ``init_script_path`` parameter: the value should be set to the full path of the
script itself. It can be used to configure the *Lua* interpreter, for example by setting
the ``package.path``\ [#fn-1]_ to a directory where pure *Lua* modules can be found, or by
preloading modules.
.. _65-lua-limitations:
Limitations
-----------
The prominent limitation is that the *Lua* interpreter in **whenever** *can not* load binary
modules.\ [#fn-2]_ This means that many popular libraries, mostly used in proper applications,
are not (and cannot be made) available to the scripts that this application can execute: trying
to ``require`` such libraries, even in case they can be found in the library search path, just
results in an error.
.. _65-lua-enhancements:
Enhancements
------------
The non-standard features that come with this version of the interpreter cover the following
topics:
* knowing the reason why a script is run
* sending messages to the **whenever** log
* performing basic network operations, namely simple HTTP requests
* sharing data with other scripts within the same **whenever** session.
All of these features are accessed each through specific module-like interfaces, variables, or
*Lua tables*.
The *reason* for running a script can be accessed through two predefined variables:
* ``whenever_task`` which is a string reporting the name of the task which defines the script:
it is obviously available in *Lua* based tasks only,
* ``whenever_condition``, is another string that in the case of *Lua* based condition reports
the name of the condition being checked, and for tasks reports the condition that triggered
the tsk itself.
The ``log`` module exposes commands that allow to forward messages to the **whenever** log:
this can be useful for debugging, of course, but also for frontends that need to communicate
with specially crafted *Lua* based items. Messages can be sent at every supported log level
(\ *error*, *warn*, *info*, *debug*, *trace*\ ). The library functions are the following:
* ``log.error(message)``
* ``log.warn(message)``
* ``log.info(message)``
* ``log.debug(message)``
* ``log.trace(message)``
and take a single string (``message``) as their argument. Each of these functions issues a
log message at the respective log level.
.. warning::
The message is not issued on its own, but prefixed by the standard log information (such
as the date, the application and the log level) plus extra information that identify the
condition and/or the task to which the script that issued the message belongs.
There is no need to ``require`` the ``log`` module, as it is available to the interpreter
by default.
An example for both the ``log`` module and the ``whenever_...`` variables can be found in the
*TRACE* task shown in the README file:
.. code-block:: toml
[[task]]
type = "lua"
name = "TRACE"
script = '''log.warn("Trace: *** VERIFIED CONDITION *** `" .. whenever_condition .. "`")'''
that is, a task that can be used to debug conditions.
.. tip::
Triple single quotes are used in TOML to denote *literal multiline strings*: *literal* means
that everything betwen the two occurrences of the triple single quotes is interpreted as it
is, including quotes of any type and backslashes. This is particularly useful in this case,
allowing a short script to be provided in the configuration without losing readability.
The remaining additional features are more complex, and deserve dedicated sub-paragraphs.
.. _65-lua-enhancements-http:
HTTP queries (optional)
~~~~~~~~~~~~~~~~~~~~~~~
The embedded *Lua* interpreter offers the possibility to query network services for
responses, using the HTTP protocol, both encrypted (HTTPS) and unencrypted. This feature
is provided via a built in module named ``http``. The requests can be performed using
both the *GET* and the *POST* methods, respectively using
* ``http.get(url [, headers])``, and
* ``http.post(url [, body [, headers]])``
where the optional parameters are expected to be in the ``url`` argument for *GET* queries
and in the ``body`` argument (with appropriate headers if necessary) for *POST* queries.
These functions return the response body as it has been received if successful, or an
error if the query could not be performed. Along with the response body, the interpreter
receives the response *status code* as a *Lua* integer number.
.. warning::
A request error, as every other error, causes always the script to **fail**: if this
is not the intended behavior, it can be useful to enclose the code that sends the
request in a ``pcall()`` statement in order to catch the error.
In a *Lua* script, the two utilities can be exploited as follows to retrieve information
from an HTTP server:
.. code-block:: lua
-- script parameters
success = true
url = "https://httpbin.org/get?param1=42¶m2=some"
check1 = '"param1": "42"'
check2 = '"param2": "some"'
-- retrieve data and check it: `httpbin.org` answers in the JSON
-- format by default, and the checks are substrings of the response
resp = http.get(url)
success = success and string.find(resp, check1, 1, true)
success = success and string.find(resp, check2, 1, true)
and the answer that *httpbin.org* provides is similar to the following:
.. code-block:: json
{
"args": {
"param1": "42",
"param2": "some"
},
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"X-Amzn-Trace-Id": "Root=1-69c6bb80-08f03a13145c13042443a168"
},
"origin": "1.2.3.4",
"url": "https://httpbin.org/get?param1=42¶m2=some"
}
so, if the ``expected_results`` parameter in the item configuration had been set to
``{ success = true }``, the script execution is considered successful -- as long as the server
was reachable and could respond.
As said before, a similar script can be used, that uses the *POST* method:
.. code-block:: lua
-- script parameters
success = true
url = "https://httpbin.org/post"
-- set the headers: here we use mixed case only for aesthetical reasons
headers = {}
headers["Content-Type"] = "application/x-www-form-urlencoded"
body = "param1=42¶m2=some"
check1 = '"param1": "42"'
check2 = '"param2": "some"'
-- perform the request and check the results
txt = http.post(url, body, headers)
success = success and string.find(txt, check1, 1, true)
success = success and string.find(txt, check2, 1, true)
by constructing the appropriate headers and body, and providing everything to the server.
.. caution::
While the URL and the request body are simple strings, the ``headers`` parameter must
be provided as a *Lua table*, with suitable strings for both keys (the header *names*,
or *fields*) and values. The *table* can be built in *Lua* in the usual way, as in the
provided example.
While the examples above only capture the *body* of the HTTP response, both ``http.get()``
and ``http.post()`` have three return values, in the following order:
* the response *body* as a string,
* the HTTP *status code* as an integer, and
* the response *headers* as a table that maps strings to strings.
Therefore a complete HTTP request has the following form:
.. code-block:: lua
body, status, headers = http.get("https://httpbin.org/get?param1=42¶m2=some")
where, for instance, ``headers["host"] == "httpbin.org"`` holds *true*. Please note that *all
header names are converted to lowercase*: per RFC 2616 header names are case insensitive, and
just using lowercase is a way to standardize access when treated as structured data.
Of course, *Lua* allows to capture just one or two, or all of the three return values.
These utilities cover a great range of what can be useful for network access in a *Lua*
script from within **whenever**: for more complex needs, external commands can be used
anyway.
.. note::
The *http* module is only available when the ``lua_httpreq`` feature is enabled; however,
both the standard configurations and the provided binaries come with the feature enabled.
.. _65-lua-persistence:
Private and shared states (optional)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As said at the beginning of this chapter, even though the *Lua* interpreter is freshly
reinitialized at each run, there is the possibility to "remember" intermediate results across
subsequent sessions of the same script, or even to exchange data with other scripts. This is
made possible through *private* and *shared states*.
A *private state* is simply a *Lua table* that is available to every script. This table, named
``state``, is available to every script, and cannot be accessed by other scripts. Values in the
table can be set as follows:
.. code-block:: lua
state.message = "Have a nice day!"
state.answer = 42
state.been_here = true
and a subsequent run of the *same* script in the *same* task or condition, is able to retrieve
those values:
.. code-block:: lua
success = state.answer == 42
thus becoming aware of results that may have been obtained in previous runs. The state is saved
when the script finishes running, even if the outcome is unsuccessful for any reason.
.. warning::
The entries of the ``state`` table can only contain values of type *boolean*, *numeric*, or
*string*. Any other type of value, including *nil*, will cause an error at the end of the
script and the state will *not* be saved.
Since the private state is only accessible by the item it is associated to, it is normally safe
to access and modify it freely, without any type of synchronization: there is only one case in
which what happens is hardly predictable, that is when more than one condition activate the same
*Lua* based task, almost at the same time -- or, at least, an instance of the script is started
while another instance of the *same* script, in the *same* task is already running: no matter
whether or not the first instance already stored something into the private state, the second
instance will access the same values as the first instance, unmodified. Moreover, the values
that the instance that ends first saves to the private state, will be simply overwritten by the
one that the instance that finishes next saves in turn: attempting to use the synchronization
utilities does not help, since the state is read at once *before* the script runs, and saved
*after* it finished running.
*Shared states*, on the other side, are accessible to *all* *Lua* scripts, and behave
differently: they need to be explicitly loaded and saved by a script, respectively using the
``sharedstate.load(name)`` and ``sharedstate.save(name, table)`` functions.
When a shared state is saved, it becomes immediately available for access and modification to
other scripts, or to the same script in a second moment: it can be loaded using the name that
it has been saved with. Shared state names, although strings, must have the form of an
identifier, that is, start with an underscore or a letter followed by alphanumeric characters
and underscores. And, just like identifiers, state names are case sensitive. The tables that
can be saved can only contain values of the types allowed in private states: *booleans*,
*numbers*, and *strings*.
Reading and writing shared states are protected from concurrency, that is, it is impossible
for a script to access a shared state that is accessed by another script: therefore it is
safe to avoid synchronization when loading or saving a shared state. Nevertheless, an example
of shared state that uses synchronization is provided below, in the paragraph dedicated to
the synchronization utilities.
A method is provided also to remove an existing shared state, ``sharedstate.remove(name)``,
where ``name`` is the name of the state to delete.
.. note::
The *state* table and the *sharedstate* module are only available when the ``lua_sync``
feature is enabled; however, both the standard configurations and the provided binaries
come with the feature enabled.
.. _65-lua-synchronization:
Synchronization utilities (optional)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The synchronization utilities are provided in order to avoid that *Lua* scripts, that run at
the same time, concurrently access a resource (for instance, a file), possibly causing an
avoidable script error. The possibility to activate a lock, and to wait for a lock to be
released, synchronizes the access to resources in a safe way.
The utilities to acquire and release a lock are, respectively, ``sync.lock(name [, timeout])``
and ``sync.release(name)`` where ``name`` is a string which has the form of an identifier, and
the optional ``timeout`` parameter is the amount of time, in seconds, that the calling script
should wait for the lock to be released before giving up: in case the lock is not acquired the
return value is *false*. When the timeout is omitted, ``lock()`` will wait forever. The
``timeout`` parameter can also be a fractional number, and the precision is in milliseconds.
Although not mandatory, the script that acquires a lock should *explicitly* release it as soon
as possible: these synchronization utilities are made available to simplify exclusive access
in the most flexible way, so that the ``lock()`` function just puts a lock on a resource, and
``release()`` just releases it, independently from which script calls the former or the latter.
In other words, ``lock()`` can be called from a script and ``release()`` from another, although
it is not encouraged.
A ``sync.sleep(seconds)`` function is also provided, that stops the script for the provided
amount of time in seconds. The ``seconds`` parameter is a number, can be fractional, and the
precision is, in this case as well, in milliseconds.
In the following example, two scripts contend a shared state: it can be useful to use locks
for shared states too, if the scripts that contend it should wait for each other to release it.
First script:
.. code-block:: lua
sleep_seconds = math.random() * 10
log.warn("LOCK:A waiting to lock shared state")
l = sync.lock("SharedState1_LOCK")
if l then
log.warn("LOCK:A shared state LOCKED")
sync.sleep(sleep_seconds)
sst = sharedstate.load("SharedState1")
v = sst.v
if v == nil then
v = 0
end
v = v + 1
sst.v = v
sharedstate.save("SharedState1", sst)
sync.release("SharedState1_LOCK")
log.warn("LOCK:A shared state UNLOCKED: now v = " .. tostring(v))
else
log.error("LOCK:A shared state NOT LOCKED")
end
and second:
.. code-block:: lua
sleep_seconds = math.random() * 10
log.warn("LOCK:B waiting to lock shared state")
l = sync.lock("SharedState1_LOCK", 2)
if l then
log.warn("LOCK:B shared state LOCKED")
sync.sleep(sleep_seconds)
sst = sharedstate.load("SharedState1")
v = sst.v
if v == nil then
v = 0
end
v = v * 2
sst.v = v
sharedstate.save("SharedState1", sst)
sync.release("SharedState1_LOCK")
log.warn("LOCK:B shared state UNLOCKED: now v = " .. tostring(v))
else
log.error("LOCK:B shared state NOT LOCKED")
end
The examples use the logging system to notify the outcome of the synchronization operations.
Only the second script forces the ``lock()`` utility to wait at most two seconds to acquire
the lock, after which it will just log an error. Both scripts access a shared state both to
read and to write a single entry, ``v``, which is a number that each script manipulates. The
random sleep time has been introduced in order to randomly cause contentions.
.. caution::
Locks are never destroyed: they are intended to be shared among scripts, so once a lock is
created it remains in memory until **whenever** exits. Even though locks do not require a
significant amount of resources, it is adviced not to instance them with dynamic names, that
is, with names that are built up from variable elements, because indefinite proliferation of
locks might end up in unwanted resource occupation.
.. note::
The *sync* module is only available when the ``lua_sync`` feature is enabled. In fact, the
feature is active by default in the standard configurations and in the provided binaries.
.. _65-lua-conclusion:
Conclusion
----------
The embedded *Lua* interpreter can be intended, especially in the case of scripts manually
created by the user, as a quick replacement for external scripts and commands: the interpreter
initialization is very fast, while calling an external command is slower and resource hungry.
*Lua* is a very expressive language, quite common as a scripting engine for applications for
many reasons, one of which is that it has been *expressedly* created for this purpose, and
another is that it is very compact.
In **whenever**, the internal *Lua* interpreter can also be used as a building block for
complex workflows: thanks to the recent additions, it acts as a stateful engine where scripts
activated as both *tasks* and *conditions* can cooperate. Its usefulness becomes even more
evident in the case of frontends that create specific tasks and conditions that should be aware
of each other.
.. [#fn-1] The ``package.cpath`` setting can be modified as well, however it generally has no
effect due to the fact that loading binary modules is normally disabled.
.. [#fn-2] There is actually the possibility to recompile **whenever** with support for binary
*Lua* modules: this is disabled by default and in the standard configurations for the
supported platforms. The tests on this option are still unsatisfactory, and in any
case it comes with the risk of corrupting the application state.