diff --git a/.hunspell.en.dic b/.hunspell.en.dic index 617915391..425c55944 100644 --- a/.hunspell.en.dic +++ b/.hunspell.en.dic @@ -1521,3 +1521,19 @@ gitignored ObjCmd codeload integrations +evaluateModulerc +mhook +API +api +modnamevr +modulerc's +logRequestedLoad +auditRequestedLoad +blockModule +fd +lindex +hookEvents +runHooks +appdir +build3 +rsync diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a51b6c60e..38505f9ab 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -116,6 +116,7 @@ howtos are also available: * :ref:`add-new-sub-command` * :ref:`add-new-config-option` +* :ref:`add-new-hook-event` * :ref:`devel-testsuite` .. _running-the-tests: diff --git a/MIGRATING.rst b/MIGRATING.rst index 03d4489d4..f68f7e754 100644 --- a/MIGRATING.rst +++ b/MIGRATING.rst @@ -205,6 +205,47 @@ When values are later removed from an environment variable, it is automatically unset if its resulting value matches the configured initial value and no explicit reference counter is associated with it. +Hook API +^^^^^^^^ + +Site code that needs to run right before or after a modulefile or modulerc +evaluation could so far only be attached through the ``trace`` Tcl command or +by renaming an internal :file:`modulecmd.tcl` procedure, both of which bind +to implementation details that may change from one Modules version to the +next. + +A new ``add-hook`` siteconfig command is introduced to register a procedure +on one of four stable events without relying on such internal details: +:mhook:`before-modulefile-eval`, :mhook:`after-modulefile-eval`, +:mhook:`before-modulerc-eval` and :mhook:`after-modulerc-eval`. Several +procedures can be registered on the same event; they are then called in +their registration order. + +.. code-block:: tcl + + proc auditRequestedLoad {modfile modname modnamevr modspec mode requested} { + if {$requested && $mode eq {load}} { + set fd [open /var/log/modules-audit.log a] + puts $fd "[clock format [clock seconds]] $modnamevr" + close $fd + } + } + add-hook before-modulefile-eval auditRequestedLoad + +An error raised by a hook procedure is reported but does not abort the +running ``module`` command nor prevent other procedures registered on the +same event from running, since these hooks fire on nearly every modulefile +or modulerc evaluation. + +See the *Hooks* section of :manpage:`module(1)` man page for the full +argument contract of each event. The ``trace``/rename techniques remain +available for anything not (yet) covered by a hook event. + +These four events are only the first ones introduced through ``add-hook``; +additional hook events will be added in the future as real site needs for +them come up. See :ref:`add-new-hook-event` for the guide contributors can +follow to propose one. + v5.6 ---- diff --git a/NEWS.rst b/NEWS.rst index 7af7124cd..55ae9da5d 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -179,6 +179,17 @@ Modules 5.7.0 (not yet released) to 1024 characters, which could corrupt the system ``PATH`` when installing from a shell with an already long inherited ``PATH`` (e.g. a Visual Studio Developer Prompt). (fix issue #654) +* Doc: add :ref:`hook-api` design notes. +* Add the ``add-hook`` siteconfig command to register a procedure on one of + 4 stable hook events (:mhook:`before-modulefile-eval`, + :mhook:`after-modulefile-eval`, :mhook:`before-modulerc-eval`, + :mhook:`after-modulerc-eval`), so sites no longer need to rely on ``trace`` + or procedure renaming, which bind to internal implementation details, to + run code before or after a modulefile or modulerc evaluation. (fix issue + #607) +* Doc: add :ref:`add-new-hook-event` guide describing how to contribute a + new hook event. +* Doc: add :ref:`sync-remote-appdir` cookbook recipe. .. _5.6 release notes: diff --git a/doc/example/sync-remote-appdir/.module_appdir_map b/doc/example/sync-remote-appdir/.module_appdir_map new file mode 100644 index 000000000..0b137b3e1 --- /dev/null +++ b/doc/example/sync-remote-appdir/.module_appdir_map @@ -0,0 +1 @@ +foo/2.1 foo-2.1-build3 diff --git a/doc/example/sync-remote-appdir/initrc b/doc/example/sync-remote-appdir/initrc new file mode 100644 index 000000000..6e193ab02 --- /dev/null +++ b/doc/example/sync-remote-appdir/initrc @@ -0,0 +1,9 @@ +#%Module5.7 + +# give the 'remote' module tag its own abbreviation and color, and make +# sure it is not persisted onto a module once it gets loaded (see the "Sync +# remote application directories on first load" cookbook recipe) +module config tag_abbrev "+remote=R" +module config colors "+R=38;5;202" +module config tag_color_name +remote +module config non_exportable_tags +remote diff --git a/doc/example/sync-remote-appdir/modulefiles/.modulerc b/doc/example/sync-remote-appdir/modulefiles/.modulerc new file mode 100644 index 000000000..4efacaa3d --- /dev/null +++ b/doc/example/sync-remote-appdir/modulefiles/.modulerc @@ -0,0 +1,21 @@ +#%Module5.7 + +# tag every module listed in the remote application directory map as +# 'remote', unless its application directory has already been synced to +# local disk (see siteconfig.tcl for how the sync itself is triggered) +set mapfile [file join /remote_apps .module_appdir_map] +if {[file readable $mapfile]} { + set fid [open $mapfile r] + set fdata [split [read $fid] "\n"] + close $fid + foreach fline $fdata { + if {[llength $fline] != 2} { + continue + } + lassign $fline modnamevr appdir + set syncedfile [file join /local_apps ".$appdir.synced"] + if {![file exists $syncedfile]} { + module-tag remote $modnamevr + } + } +} diff --git a/doc/example/sync-remote-appdir/modulefiles/foo/2.1 b/doc/example/sync-remote-appdir/modulefiles/foo/2.1 new file mode 100644 index 000000000..1f5bc7ede --- /dev/null +++ b/doc/example/sync-remote-appdir/modulefiles/foo/2.1 @@ -0,0 +1,5 @@ +#%Module5.7 + +set appdir foo-2.1-build3 +prepend-path PATH /local_apps/$appdir/bin +prepend-path LD_LIBRARY_PATH /local_apps/$appdir/lib diff --git a/doc/example/sync-remote-appdir/siteconfig.tcl b/doc/example/sync-remote-appdir/siteconfig.tcl new file mode 100644 index 000000000..f5bd75bd3 --- /dev/null +++ b/doc/example/sync-remote-appdir/siteconfig.tcl @@ -0,0 +1,74 @@ +# +# siteconfig.tcl - Site specific configuration script that copies, the first +# time a module tagged 'remote' is loaded, its application directory from +# a remote network share to local disk with rsync, so this and every later +# load of the same module read from local disk instead of the network +# share. +# +# Author: Xavier Delaruelle +# Compatibility: Modules v5.7+ +# +# Installation: put this file in the 'etc' directory of your Modules +# installation. Refer to the "Modulecmd startup" section in the +# module(1) man page to get this location. + +# root of the mounted network share and of its local counterpart +set g_remoteAppDir /remote_apps +set g_localAppDir /local_apps + +# return the application directory basename mapped to given bare module name +# and version, or an empty string if this module has no mapped directory +proc getAppDirBasename {modname} { + set mapfile [file join $::g_remoteAppDir .module_appdir_map] + if {![file readable $mapfile]} { + return {} + } + set fid [open $mapfile r] + set fdata [split [read $fid] "\n"] + close $fid + foreach fline $fdata { + if {[llength $fline] == 2 && [lindex $fline 0] eq $modname} { + return [lindex $fline 1] + } + } + return {} +} + +# copy application directory from the remote network share to local disk, on +# the first load of a module tagged 'remote' (see the modulepath root +# .modulerc file for how this tag gets applied) +proc syncRemoteAppDir {modfile modname modnamevr modspec mode requested} { + if {$mode ne {load}} { + return + } + set itrp [getCurrentModfileInterpName] + if {![interp eval $itrp {module-info tags remote}]} { + return + } + + set appdir [getAppDirBasename $modname] + if {$appdir eq {}} { + return + } + + set syncedfile [file join $::g_localAppDir ".$appdir.synced"] + if {[file exists $syncedfile]} { + return + } + + report "Syncing '$appdir' application directory from remote share..." + file mkdir $::g_localAppDir + set srcdir [file join $::g_remoteAppDir $appdir] + set destdir [file join $::g_localAppDir $appdir] + if {[catch {exec rsync -a --delete $srcdir/ $destdir/} errMsg]} { + reportError "Failed to sync '$appdir' from remote share\n$errMsg" + return + } + + # mark this application directory as synced so it does not get copied + # again on a later load + close [open $syncedfile w] +} +add-hook before-modulefile-eval syncRemoteAppDir + +# vim:set tabstop=3 shiftwidth=3 expandtab autoindent: diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 1df47afb5..c15a37db3 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -831,6 +831,24 @@ user environment is already configured. Starting version ``5.5``, support for Windows *pwsh* shell is introduced. +Siteconfig hooks +"""""""""""""""" + +Starting version ``5.7``, the ``add-hook`` siteconfig command is introduced +to register a procedure to run on a hook event, as a replacement for the +``trace``/procedure-renaming techniques that bind to internal +implementation details. + +The following hook events appeared on Modules 5. + ++------------+-----------------------------------------------------------------+ +| Introduced | New hook events | +| in version | | ++============+=================================================================+ +| 5.7 | :mhook:`before-modulefile-eval`, :mhook:`after-modulefile-eval`,| +| | :mhook:`before-modulerc-eval`, :mhook:`after-modulerc-eval` | ++------------+-----------------------------------------------------------------+ + Command line switches """"""""""""""""""""" diff --git a/doc/source/conf.py b/doc/source/conf.py index ad66d8b7a..59aace231 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -379,6 +379,10 @@ def setup(app): objname='siteconfig variable', indextemplate='pair: %s; siteconfig variable', parse_node=parse_cmd_args_node) + app.add_object_type(directivename='mhook', rolename='mhook', + objname='hook event', + indextemplate='pair: %s; hook event', + parse_node=parse_cmd_args_node) app.add_object_type('instopt', 'instopt', objname='installation option', indextemplate='pair: %s; installation option', diff --git a/doc/source/cookbook/sync-remote-appdir.rst b/doc/source/cookbook/sync-remote-appdir.rst new file mode 100644 index 000000000..ba0c80b36 --- /dev/null +++ b/doc/source/cookbook/sync-remote-appdir.rst @@ -0,0 +1,173 @@ +.. _sync-remote-appdir: + +Sync remote application directories on first load +================================================= + +Some workstations and laptops mount a network share holding all kind of +software installations that may be used on this machine, alongside their own +local, faster disk. Reading application files directly from this network +share on every use works, but it is slower than reading them from local +disk, and it stops working entirely when the machine goes offline. + +This recipe describes how to keep modulefiles pointing at application +directories on a network share mounted under :file:`/remote_apps`, while +transparently copying, with the `rsync`_ tool, the directory of a given +application to :file:`/local_apps` the first time the corresponding module +is loaded. Every later load of the same module finds its application +directory already synced locally and skips the copy. + +.. _rsync: https://rsync.samba.org/ + +Implementation +-------------- + +A :file:`.module_appdir_map` file, located at the root of the +:file:`/remote_apps` network share, maps each module name and version to the +basename of its application directory under :file:`/remote_apps`: + +.. literalinclude:: ../../example/sync-remote-appdir/.module_appdir_map + :caption: /remote_apps/.module_appdir_map + +The :file:`.modulerc` file at the root of the modulepath reads this map file +and applies the ``remote`` tag, with the :mfcmd:`module-tag` modulefile +command, to every module listed in it whose application directory has not +been synced locally yet -- tracked by the presence of a +:file:`..synced` marker file under :file:`/local_apps`: + +.. literalinclude:: ../../example/sync-remote-appdir/modulefiles/.modulerc + :language: tcl + :caption: modulefiles/.modulerc + +The actual sync is performed by a :mhook:`before-modulefile-eval` hook, +registered with the ``add-hook`` siteconfig command (see the :ref:`hook-api` +design notes and the *Hooks* section of :manpage:`module(1)` man page). The +hook procedure returns immediately if the evaluation mode is not ``load``, +or if the module being evaluated is not tagged ``remote`` -- checked with +:mfcmd:`module-info tags`, run in the modulefile Tcl +interpreter reached through ``getCurrentModfileInterpName``, exactly as +already documented for a hook procedure that needs to run modulefile +commands. Otherwise, it looks up the application directory basename mapped +to the module in :file:`.module_appdir_map`, and skips the sync if this +directory was already marked as synced by a previous load. Otherwise it +copies the application directory with ``rsync`` and, on success, touches +the :file:`..synced` marker file so later loads skip the copy. + +Because the modulepath root :file:`.modulerc` tags a module ``remote`` ahead +of the sync actually happening, this tag would otherwise still be recorded +as applying to the module once loaded, which is misleading once its +application directory has just been synced locally. The ``remote`` tag is +therefore added to the :mconfig:`non_exportable_tags` configuration option, +introduced in Modules v5.7 together with the hook API, so it is dropped from +the tag list persisted once a module is loaded, without affecting how it is +reported beforehand, for instance on an :subcmd:`avail` listing. The +``remote`` tag is also given its own abbreviation and color, so modules +whose application directory has not been synced yet stand out on an +:subcmd:`avail` or :subcmd:`spider` listing. These ``module config`` calls +are set in |file etcdir_initrc|, evaluated once when the ``module`` shell +function initializes with :subcmd:`autoinit`, the only context ``module +config`` is usable from within a file evaluated by Modules itself: + +.. literalinclude:: ../../example/sync-remote-appdir/initrc + :language: tcl + :caption: initrc + +The sync itself is triggered by the hook procedure, defined and registered +in :file:`siteconfig.tcl`: + +.. literalinclude:: ../../example/sync-remote-appdir/siteconfig.tcl + :language: tcl + :caption: siteconfig.tcl + +Since the hook fires before the modulefile itself is evaluated, and the sync +is performed with a blocking ``exec`` call, ``module load`` waits for the +copy to complete before the modulefile that relies on the now-local +application directory gets evaluated. A hook procedure cannot abort the +evaluation it wraps, so a sync failure is reported as an error but does not +prevent the module from loading afterward, even though its application +directory may still be missing locally in that case. + +**Compatible with Modules v5.7+** + +Installation +------------ + +Create site-specific configuration directory if it does not exist yet: + +.. parsed-literal:: + + $ mkdir \ |etcdir| + +Copy the site-specific configuration script and initialization file of this +recipe: + +.. parsed-literal:: + + $ cp example/sync-remote-appdir/siteconfig.tcl \ |etcdir|\ / + $ cp example/sync-remote-appdir/initrc \ |etcdir|\ / + +.. note:: + + Defined location for the site-specific configuration script may vary from + one installation to another. To determine the expected location for this + file on your setup, check the value of the ``siteconfig`` configuration + option: + + .. parsed-literal:: + + :ps:`$` module config siteconfig + +Adapt :file:`modulefiles/.modulerc` to your modulepath, and copy it at its +root, next to the modulefiles it applies to. Finally, create +:file:`/remote_apps/.module_appdir_map` on the network share, with one +`` `` entry +per line for each application directory that should be synced this way. + +Usage example +------------- + +The application directory of ``foo/2.1`` has not been synced locally yet, so +it shows up tagged ``remote`` on an :subcmd:`avail` listing, using the +abbreviation and color configured for this tag: + +.. parsed-literal:: + + :ps:`$` module avail foo + --------------- :sgrdi:`/path/to/modulefiles` --------------- + foo/2.1 + + Key: + =remote + +Loading it triggers the sync, then proceeds with the load once the copy +completes: + +.. parsed-literal:: + + :ps:`$` module load foo/2.1 + Syncing 'foo-2.1-build3' application directory from remote share... + Loading :sgrhi:`foo/2.1` + +Once loaded, the module no longer carries the ``remote`` tag, since its +application directory now lives on local disk: + +.. parsed-literal:: + + :ps:`$` module list + Currently Loaded Modulefiles: + 1) foo/2.1 + +A later load, after the module has been unloaded, finds the +:file:`.foo-2.1-build3.synced` marker file and skips the sync entirely, +and the module no longer shows up tagged ``remote`` on :subcmd:`avail` +either: + +.. parsed-literal:: + + :ps:`$` module unload foo/2.1 + :ps:`$` module avail foo + --------------- :sgrdi:`/path/to/modulefiles` --------------- + foo/2.1 + :ps:`$` module load foo/2.1 + Loading :sgrhi:`foo/2.1` + +.. vim:set tabstop=2 shiftwidth=2 expandtab autoindent: diff --git a/doc/source/design/hook-api.rst b/doc/source/design/hook-api.rst new file mode 100644 index 000000000..480158a2d --- /dev/null +++ b/doc/source/design/hook-api.rst @@ -0,0 +1,223 @@ +.. _hook-api: + +Hook API +======== + +It is sometimes desired to execute specific site procedures at the beginning +or at the end of a modulefile or modulerc evaluation. + +This is currently possible with either the ``trace`` Tcl command or by +superseding one of the internal Tcl procedures (like ``execute-modulefile``), +as described in the *Hooks* section of :manpage:`module(1)` man page. Both +techniques bind site code to internal implementation details: procedure +names and call signatures that may change from one Modules version to the +next. Sites relying on them have to re-review this internal code on every +upgrade to determine whether their hook code or its connection point still +applies. + +This document defines a stable, documented ``add-hook`` API to replace this +need for most cases, without removing the lower-level ``trace``/rename +techniques, which remain available for anything not covered by a hook +event. This first change introduces 4 events; the API is designed so more +can be added later without changing how existing ones behave. + +The ``add-hook`` command +------------------------ + +A new ``add-hook`` command is introduced, usable from :file:`siteconfig.tcl` +(and any script it sources). Like ``trace`` or ``rename``, it is only +meaningful in the main Tcl interpreter: :file:`siteconfig.tcl` is sourced +once at startup, before any modulefile or modulerc sub-interpreter is +created, so hook procedures execute in that same main-interpreter context — +exactly like ``trace``-based hooks do today. A hook procedure that needs to +act on the modulefile or modulerc currently being evaluated reaches it the +same way an existing ``trace``-based hook would: through +``getCurrentModfileInterpName`` and ``interp eval``, as already documented +for the current hook technique. + +Syntax:: + + add-hook + +* ``event`` must be one of the event names defined below. An unknown event + name is a configuration mistake, so it raises an error immediately, at the + time ``add-hook`` is called — the same treatment already given to other + siteconfig configuration mistakes (for instance an odd-length + :sitevar:`modulefile_extra_vars` list). +* ``procedure`` is the name of a Tcl procedure, defined in + :file:`siteconfig.tcl` or a script it sources, that will be called when + the event occurs. It is looked up by name at call time, not validated at + registration time. + +Several procedures can be registered on the same event. They are called in +their registration order. This lets independent site concerns (say, +logging and environment tuning) each register their own procedure on the +same event rather than being forced to share a single procedure body — a +constraint the existing ``ModulesHelp``/``ModulesDisplay``/``ModulesTest`` +fixed-proc-name convention would have imposed if reused here. + +First hook events and their argument contract +--------------------------------------------- + +The first 4 events introduced cover the beginning and the end of +modulefile and modulerc evaluation. Each one calls the registered +procedure with a fixed, positional argument list — this argument contract +*is* the stable API: it is what lets a hook procedure survive a Modules +upgrade unchanged. + +``before-modulefile-eval`` + Called immediately before a modulefile is evaluated. + + ``proc {modfile modname modnamevr modspec mode requested} {...}`` + +``after-modulefile-eval`` + Called immediately after a modulefile has been evaluated. + + ``proc {modfile modname modnamevr modspec mode requested status} {...}`` + +``before-modulerc-eval`` + Called immediately before a modulerc (or ``.version`` file) is evaluated. + + ``proc {modfile modname} {...}`` + +``after-modulerc-eval`` + Called immediately after a modulerc (or ``.version`` file) has been + evaluated. + + ``proc {modfile modname status} {...}`` + +Argument meaning, common to these events: + +* ``modfile`` — absolute path of the modulefile or modulerc being evaluated. +* ``modname`` — module name and version being evaluated, without its variant + specification. +* ``modnamevr`` (``*-modulefile-eval`` events only) — module name, version + and variant currently resolved for this evaluation. Modulerc evaluation + is not tied to one resolved module the way modulefile evaluation is, so + this argument is omitted for the two modulerc events, which only get the + bare ``modname``. +* ``modspec`` (``*-modulefile-eval`` events only) — module specification as + it was passed to the internal evaluation call, prior to any resolution: + typically what the user typed on the command line (or the raw entry read + from a collection or loaded-modules list), which may still differ from + ``modnamevr`` (for instance a partial version, or a variant specification + merged in as a separate list element). Modulerc evaluation has no + equivalent notion, so this argument is also omitted for the two modulerc + events. +* ``mode`` — current evaluation mode (``load``, ``unload``, ``display``, + ``help``, ``test``, ``whatis``, ``refresh``, ``scan`` or ``dep``). + Modulerc evaluation is not mode-specific the way modulefile evaluation is, + so this argument is omitted for the two modulerc events. +* ``requested`` — boolean, true if this module was directly requested by the + user, false if it is being evaluated as a side effect (for instance an + auto-loaded dependency). Modulerc evaluation has no equivalent notion, so + this argument is also omitted for the two modulerc events. +* ``status`` (``after-*`` events only) — ``0`` if evaluation succeeded, + ``1`` if it raised an error, mirroring the return-code convention already + used internally around modulefile/modulerc evaluation. + +The before/after events fire around the actual evaluation step, skipped +whenever interpretation itself is skipped (for instance when interpretation +is inhibited during a dependency-resolution pass) — so a hook only fires for +evaluations that really read and interpret the target file. +``before-modulefile-eval`` and ``before-modulerc-eval`` fire after the +target modulefile's or modulerc's dedicated Tcl sub-interpreter has been +created and reset to its initial state, so a hook procedure can already +reach it (see ``getCurrentModfileInterpName`` in :manpage:`module(1)` man +page) to run modulefile commands there. + +These hooks are informational for this first iteration: the value a hook +procedure returns is ignored, and it cannot cancel or otherwise alter the +evaluation it wraps — unlike :mfcmd:`module-forbid` or :mfcmd:`module-warn`, +which are dedicated mechanisms for that purpose. See `Open questions and +future work`_ below. + +Error handling +-------------- + +If a hook procedure raises a Tcl error, that error is caught and reported, +but does not abort the ``module`` command being run. In particular: + +* when several procedures are registered on the same event, one procedure + raising an error does not prevent the other procedures registered on that + event from running: each remaining procedure in the list is still called, + in its registration order; +* the modulefile or modulerc evaluation being wrapped by the hook still + proceeds normally. + +This differs from how a load-time error in :file:`siteconfig.tcl` itself is +handled today (which is fatal). The distinction is deliberate: a +``before-``/``after-*-eval`` hook fires on essentially every modulefile or +modulerc evaluation, so letting a single buggy hook procedure abort every +``module`` invocation site-wide would be far more disruptive than letting a +malformed :file:`siteconfig.tcl` fail once, loudly, at startup. + +Relation to existing trace/rename-based hooks +--------------------------------------------- + +The ``trace``/rename techniques described in :manpage:`module(1)` man page +keep working unchanged — nothing is removed. The *Hooks* section of that man +page is restructured to present ``add-hook`` as the primary, recommended +mechanism for the events it supports, and the ``trace``/rename techniques +as an advanced, lower-level fallback for connection points ``add-hook`` +does not (yet) cover. + +Documentation +------------- + +Hook events are documented with a new Sphinx object type, ``mhook`` +(``.. mhook::``/``:mhook:``), defined the same way existing ``mfcmd``, +``mfvar`` and ``sitevar`` object types are in ``doc/source/conf.py``. A +distinct name from the generic "hook" is used to avoid ambiguity with the +other unrelated uses of that word already in the docs (git commit hooks in +``CONTRIBUTING.rst``, and the existing ``trace``-based hook technique +described in ``module.rst``). + +Touch points for implementation +------------------------------- + +This section lists the files a future implementation is expected to touch; +it is not itself an implementation plan. + +* ``tcl/interp.tcl.in`` — hook registry (populated by ``add-hook``), a small + dispatch helper, and the four call sites added around the existing + ``evaluateModulefile``/``evaluateModulerc`` calls inside + ``execute-modulefile``/``execute-modulerc``. +* ``siteconfig.tcl`` (root template, and its installed copy) — a new + commented example of how to register a hook with ``add-hook``, pointing + at :manpage:`module(1)` man page for the current list of events, next to + the existing commented-out + :sitevar:`modulefile_extra_vars`/:sitevar:`modulefile_extra_cmds`/ + :sitevar:`modulerc_extra_vars`/:sitevar:`modulerc_extra_cmds` examples. +* ``doc/source/conf.py`` — register the new ``mhook`` object type. +* ``doc/source/module.rst`` — restructure the *Hooks* section: document + ``add-hook`` and the four ``mhook`` entries, demote the ``trace``/rename + description to the advanced fallback case. +* ``doc/source/changes.rst`` and ``NEWS.rst`` — new entry under the + in-development ``Modules 5.7.0`` section (appended at the end of its + bullet list, per project convention). +* Nagelfar syntax db (``share/``) — check whether ``make testlint`` requires + it to be regenerated for the new ``add-hook`` command to lint cleanly in + :file:`siteconfig.tcl` examples. +* Testsuite — a new, dedicated ``.exp`` file (kept separate from + ``testsuite/modules.50-cmds/560-siteconfig-interp.exp``, which already + covers the older extra_vars/extra_cmds mechanism), with a matching new + ``testsuite/example/siteconfig.tcl-N`` fixture, following the existing + naming and numbering pattern in that directory. + +Open questions and future work +------------------------------ + +* A ``remove-hook`` command, and/or a way to introspect which procedures are + currently registered on a given event, is not part of this first pass. + The registry design should not preclude adding one later. +* Future hook events (for instance around module load/unload completion, or + collection save/restore) will be exposed through this same ``add-hook`` + mechanism, once real use cases for them emerge. +* Future hook events may be designed to influence the evaluation they wrap -- + altering argument values, changing the execution flow, or raising an error + that aborts it -- but only where the event is purposely built for that and + the calling code is written to expect it, unlike the events introduced + here. + +.. vim:set tabstop=2 shiftwidth=2 expandtab autoindent: diff --git a/doc/source/devel/add-new-hook-event.rst b/doc/source/devel/add-new-hook-event.rst new file mode 100644 index 000000000..5082dddde --- /dev/null +++ b/doc/source/devel/add-new-hook-event.rst @@ -0,0 +1,136 @@ +.. _add-new-hook-event: + +Add new hook event +================== + +This document is a guide for Modules developers that wish to introduce a new +hook event for the ``add-hook`` siteconfig command. See the :ref:`hook-api` +design notes for the rationale behind this mechanism, and the *Hooks* +section of :manpage:`module(1)` man page for the events already available. + +Core code +--------- + +A hook event is a name registered in ``g_hookEvents``, plus a ``runHooks`` +call placed at the exact point in the internal code where site procedures +registered on that event should run. + +#. Declare the new event name in the ``g_hookEvents`` list. + + - File to edit: :file:`tcl/init.tcl.in` + + Event names are lower-case, dash-separated, and usually paired as + ``before-``/``after-`` when they bracket an + operation, following the existing ``before-modulefile-eval``/ + ``after-modulefile-eval`` and ``before-modulerc-eval``/ + ``after-modulerc-eval`` events. + +#. Add a ``runHooks ...`` call at the point in the + internal code where the event should fire. + + - File to edit: depends on the touch point the event relates to. The + four existing events all fire from :file:`tcl/interp.tcl.in`, around + the ``evaluateModulefile``/``evaluateModulerc`` calls inside + ``execute-modulefile``/``execute-modulerc``. A hook event covering a + different touch point (for instance module load/unload completion, or + collection save/restore, both mentioned as future candidates in the + :ref:`hook-api` design notes) would instead be added next to the + internal code implementing that operation. + + Decide on a fixed, positional argument list for the event: this argument + contract *is* the stable API a hook procedure relies on, so it should be + descriptive enough on its own (an absolute file path rather than a + relative one, a resolved value rather than an internal handle, etc.) and + should not change once released. Look at the argument list of the + existing events for the kind of information usually passed: paths, + resolved names, current mode, and a trailing ``status`` argument on + ``after-*`` events reporting whether the wrapped operation succeeded. + + ``runHooks`` catches an error raised by a hook procedure, reports it, and + keeps going: neither the other procedures registered on the same event + nor the operation the hook wraps are interrupted. This is the right + default for an event that is purely informational, like the four + existing ones, since a buggy hook procedure must not be able to lock + site-wide command usage. If the new event is deliberately designed to let + a hook procedure influence the operation it wraps -- aborting it, or + altering a value used afterward -- that is a different contract that + ``runHooks`` does not provide as-is; the call site needs its own code to + read back a value or catch a raised error and act on it, and this + divergence from the informational-only default should be spelled out + clearly in the event's documentation. + +Documentation +------------- + +Man page and other user documentation have to be updated to describe the +introduced event. + +Files that should be edited: + +- :file:`doc/source/module.rst` (module manpage) + + - add event description with ``mhook`` anchor under the *Hooks* + subsection, following the pattern of the existing four events: argument + list in the directive signature, prose describing each argument, and a + note on whether the value a registered procedure returns is used or + ignored + +- :file:`doc/source/changes.rst` + + - add the event to the table under the *Siteconfig hooks* subsection of + the current Modules major version, next to the version it is introduced + in + +- :file:`NEWS.rst` + + - add an entry under the in-development version's bullet list, at the end + of it + +- :file:`MIGRATING.rst` + + - the *Hook API* entry already announces that more hook events will be + added over time, so a single new event usually does not need its own + highlight there; consider adding or extending an entry if the new event + opens up a use case significant enough to be worth a dedicated + highlight (a new category of touch point, for instance, rather than + another modulefile/modulerc-evaluation event) + +- :file:`doc/source/design/hook-api.rst` + + - a new event covering the same kind of touch point as the existing ones + (modulefile/modulerc evaluation) can usually just be documented in + :file:`module.rst` as above; a new event opening up a different touch + point (see the *Open questions and future work* section of this design + document) is significant enough to warrant its own design notes first, + following this document as a template + +Testsuite +--------- + +Non-regression testsuite must be adapted to check the behavior of the added +event and ensure overall code coverage does not drop. + +#. Register a procedure on the new event, following the existing + ``switch --`` cases already used to select which hook scenario a test + run exercises. + + - File to edit: :file:`testsuite/example/siteconfig.tcl-1` + +#. Craft tests that validate the event fires at the right point with the + correct arguments, on both a successful and a failing operation if + applicable. + + - File to edit: :file:`testsuite/modules.50-cmds/740-hook.exp` if the + new event relates to modulefile/modulerc evaluation like the existing + ones, a new dedicated ``.exp`` file otherwise, following the existing + naming and numbering pattern of the directory the new touch point + belongs to + + The generic ``add-hook`` behaviors (unknown event name, several + procedures registered on the same event, a hook procedure error not + interrupting the wrapped operation, wrong argument count) are already + covered for the existing events and do not need to be duplicated; focus + new tests on what is specific to the new event: its firing point and its + argument values. + +.. vim:set tabstop=2 shiftwidth=2 expandtab autoindent: diff --git a/doc/source/module.rst b/doc/source/module.rst index 15bdbab71..ba386eea7 100644 --- a/doc/source/module.rst +++ b/doc/source/module.rst @@ -3949,23 +3949,161 @@ An additional siteconfig script may be specified through the exists the extra siteconfig is sourced by :file:`modulecmd.tcl` right after main siteconfig script. +.. _Hooks: + Hooks """"" +The ``add-hook`` command lets siteconfig register a procedure to be called +on one of the fixed set of hook events listed below, without having to know +anything about the internal implementation of :file:`modulecmd.tcl`. This is +the recommended way to run site-specific code before or after a modulefile +or modulerc evaluation. It is called this way:: + + add-hook event procedure + +``event`` must be one of the hook events listed below; an unrecognized event +name is a siteconfig error, raised immediately when ``add-hook`` is called. +Several procedures may be registered on the same event: they are then all +called, in their registration order, every time the event occurs. + +If ``procedure`` raises an error, this error is reported but does not stop +the other procedures registered on the same event from being called, and +does not interrupt the modulefile or modulerc evaluation the event relates +to. + +Unless stated otherwise, hook procedures execute in the context of the main +Tcl interpreter -- the same one siteconfig itself runs in -- never in the +modulefile- or modulerc-specific sub-interpreter the event relates to. See +the second example below for how a hook procedure can still reach that +sub-interpreter when it needs to run modulefile or modulerc commands. + +.. only:: html or latex + + .. versionadded:: 5.7 + +.. mhook:: before-modulefile-eval modfile modname modnamevr modspec mode\ + requested + + Called right before a modulefile is evaluated, with the path of the + modulefile (``modfile``), the module name and version being evaluated, + without its variant specification (``modname``), the module name, version and + variant (``modnamevr``), the module specification as it was passed to the + internal evaluation call, prior to resolution -- typically what the user + typed on the command line (``modspec``), the current evaluation mode + (``mode``, see :mfcmd:`module-info`) and whether this modulefile evaluation + was directly requested by the user or triggered as a side effect, for + instance an automatically-loaded dependency (``requested``, a boolean). + The value ``procedure`` returns, if any, is ignored. + + .. only:: html or latex + + .. versionadded:: 5.7 + +.. mhook:: after-modulefile-eval modfile modname modnamevr modspec mode\ + requested status + + Called right after a modulefile has been evaluated, with the same + arguments as :mhook:`before-modulefile-eval` plus ``status``: ``0`` if the + modulefile evaluated without error, ``1`` otherwise. The value + ``procedure`` returns, if any, is ignored. + + .. only:: html or latex + + .. versionadded:: 5.7 + +.. mhook:: before-modulerc-eval modfile modname + + Called right before a modulerc or :file:`.version` file is evaluated, with + the path of the file (``modfile``) and the modulerc short name (``modname``). + Modulerc evaluation is not tied to a single evaluation mode nor to a specific + request the way modulefile evaluation is, so neither a ``mode`` nor a + ``requested`` argument is provided here. The value ``procedure`` returns, if + any, is ignored. + + This event, and :mhook:`after-modulerc-eval`, only apply to a + :file:`.modulerc` or :file:`.version` file resolved while looking up a + module under a modulepath. The RC files read once at startup + (:mconfig:`rcfile` and the other sources listed there) are not modulerc + files: they are evaluated through + :mhook:`before-modulefile-eval`/:mhook:`after-modulefile-eval` instead, in + ``load`` mode, exactly like any file passed to the :subcmd:`source` + sub-command. + + .. only:: html or latex + + .. versionadded:: 5.7 + +.. mhook:: after-modulerc-eval modfile modname status + + Called right after a modulerc or :file:`.version` file has been evaluated, + with the same arguments as :mhook:`before-modulerc-eval` plus ``status``: + ``0`` if the file evaluated without error, ``1`` otherwise. The value + ``procedure`` returns, if any, is ignored. + + .. only:: html or latex + + .. versionadded:: 5.7 + +The following example uses ``add-hook`` to record every directly-requested +modulefile load to a site-defined audit file. This is a different need +than the built-in :mconfig:`logger` integration (see channel argument of +:mfcmd:`puts`), which only forwards messages a modulefile explicitly sends +to it: + +.. code-block:: tcl + + proc auditRequestedLoad {modfile modname modnamevr modspec mode requested} { + if {$requested && $mode eq {load}} { + set fd [open /var/log/modules-audit.log a] + puts $fd "[clock format [clock seconds]] $modnamevr" + close $fd + } + } + add-hook before-modulefile-eval auditRequestedLoad + +If a hook procedure running in the context of the main interpreter needs to +execute modulefile commands (for example, to define environment variables), +these commands should be run through the current modulefile Tcl interpreter. +This ensures that the commands behave consistently with the current modulefile +evaluation mode. + +.. code-block:: tcl + + proc hook_procedure {modfile modname modnamevr modspec mode requested} { + # get the name of the current modulefile Tcl interpreter + set modfile_interp [getCurrentModfileInterpName] + + # execute a modulefile command in the current interpreter context + interp eval $modfile_interp setenv MYVAR value + } + add-hook before-modulefile-eval hook_procedure + +Advanced hooks +"""""""""""""" + Siteconfig relies on the ability of the Tcl language to overwrite previously -defined variables and procedures. Sites may deploy their own Tcl code in -siteconfig to adapt :file:`modulecmd.tcl` to their specific needs. The -``trace`` Tcl command may especially be used to define hooks that are run when -entering or leaving a given procedure, or when a variable is read or written. -See :manpage:`trace(n)` man page for detailed information. The following -example setup a procedure that is executed before each modulefile evaluation: +defined variables and procedures. For needs ``add-hook`` does not cover -- +for instance rejecting a modulefile evaluation outright, something a hook +procedure cannot do since its return value is ignored -- sites may still +deploy their own Tcl code in siteconfig to adapt :file:`modulecmd.tcl` to +their specific needs. The ``trace`` Tcl command may especially be used to +define hooks that are run when entering or leaving a given procedure, or +when a variable is read or written. See :manpage:`trace(n)` man page for +detailed information. The following example blocks the evaluation of a +specific modulefile by raising an error from an ``enter`` trace on the +internal ``execute-modulefile`` procedure, something that cannot be +expressed through :mhook:`before-modulefile-eval`: .. code-block:: tcl - proc beforeEval {cmdstring code result op} { - # code to run right before each modulefile evaluation + proc blockModule {cmdstring op} { + # third word of cmdstring is the resolved module name and version + if {[lindex $cmdstring 2] eq {foo/1.0}} { + error "foo/1.0 is blocked by site policy" + } } - trace add execution execute-modulefile enter beforeEval + trace add execution execute-modulefile enter blockModule Another possibility is to override the definition of an existing procedure by first renaming its original version then creating a new procedure that will add @@ -3983,20 +4121,9 @@ adds a new query option to the :mfcmd:`module-info` modulefile command: } } -If a hook procedure needs to execute modulefile commands (for example, to -define environment variables), these commands should be run through the -current modulefile Tcl interpreter. This ensures that the commands behave -consistently with the current modulefile evaluation mode. - -.. code-block:: tcl - - proc hook_procedure {value} { - # get the name of the current modulefile Tcl interpreter - set modfile_interp [getCurrentModfileInterpName] - - # execute a modulefile command in the current interpreter context - interp eval $modfile_interp setenv MYVAR $value - } +These techniques bind to :file:`modulecmd.tcl` internal procedure names and +call signatures, which may change across versions; prefer ``add-hook`` +when one of its events covers the need. Siteconfig hook variables """"""""""""""""""""""""" diff --git a/doc/source/other-implementations.rst b/doc/source/other-implementations.rst index cdd591b89..094333f70 100644 --- a/doc/source/other-implementations.rst +++ b/doc/source/other-implementations.rst @@ -45,9 +45,9 @@ table highlights features that are unique to each implementation. * `Path entry priorities`_ * ``--regexp`` search option * `settarg`_ - * `Hook functions`_ * |LMOD_FILE_IGNORE_PATTERNS|_ environment variable * `MarkDown support in module help and whatis`_ + * `Load dot hidden modules without leading dot`_ - * Integration with *cmd* and *pwsh* shells and *Tcl* language * :ref:`Automated module handling` * :ref:`Advanced module version specifiers` @@ -83,6 +83,7 @@ table highlights features that are unique to each implementation. .. |LMOD_FILE_IGNORE_PATTERNS| replace:: ``LMOD_FILE_IGNORE_PATTERNS`` .. _LMOD_FILE_IGNORE_PATTERNS: https://lmod.readthedocs.io/en/latest/090_configuring_lmod.html#setting-environment-variables-or-cosmic-assign-at-startup .. _MarkDown support in module help and whatis: https://lmod.readthedocs.io/en/latest/106_markdown_help.html +.. _Load dot hidden modules without leading dot: https://lmod.readthedocs.io/en/latest/079_hidden_modules.html#dot-leading-version-directories-and-lmod-dot-hidden-load-alias The following table highlights ``module`` sub-commands that are exclusive to either Lmod or Modules. In some cases, similar functionality exists under @@ -94,7 +95,7 @@ at the end of this section to map these equivalents. * - |lmod_version| - |modules_version| - * - ``category``, ``overview``, ``tablelist`` + * - ``category``, ``last-error``, ``overview``, ``tablelist`` - :subcmd:`aliases`, :subcmd:`append-path`, :subcmd:`cachebuild`, :subcmd:`cacheclear`, :subcmd:`clear`, :subcmd:`config`, :subcmd:`edit`, :subcmd:`info-loaded`, :subcmd:`initadd`, @@ -120,10 +121,9 @@ the end of this section to map these equivalents. - |modules_version| * - ``module-forbid-regex``, ``module-hide-regex``, ``remove-property`` - :mfcmd:`getvariant`, :mfcmd:`is-saved`, :mfcmd:`is-used`, - :mfcmd:`lsb-release`, :mfcmd:`module-tag`, :mfcmd:`module-virtual`, - :mfcmd:`module-warn`, :mfcmd:`modulepath-label`, :mfcmd:`provide`, - :mfcmd:`reportWarning`, :mfcmd:`uncomplete`, :mfcmd:`variant`, - :mfcmd:`x-resource` + :mfcmd:`lsb-release`, :mfcmd:`module-tag`, :mfcmd:`module-warn`, + :mfcmd:`modulepath-label`, :mfcmd:`provide`, :mfcmd:`reportWarning`, + :mfcmd:`uncomplete`, :mfcmd:`variant`, :mfcmd:`x-resource` See the :ref:`Compatibility with Lmod Tcl modulefile` section for details on how the implementation of the Tcl modulefile commands differ between Lmod and @@ -153,8 +153,7 @@ implementation. * - Lmod + `XALT`_ - :ref:`Logging activity` * - `Hook functions`_ - - :ref:`Override any internal procedures or set trace hook` + - :ref:`Hooks` * - `Module hierarchy`_ - :ref:`Requiring via module` * - `Autoswap`_ @@ -293,4 +292,4 @@ If you're aware of a ``module``-related project missing from this list, feel free to :ref:`contact us` so we can add it. .. |modules_version| replace:: Modules 5.7.0 (not yet released) -.. |lmod_version| replace:: Lmod 9.2.5 +.. |lmod_version| replace:: Lmod 9.3.0 diff --git a/siteconfig.tcl b/siteconfig.tcl index d178e4ef5..cfc8574c9 100644 --- a/siteconfig.tcl +++ b/siteconfig.tcl @@ -31,3 +31,13 @@ # defined in this file #set modulerc_extra_cmds {command1 procedure1 command2 procedure2} +# register a procedure to run on a hook event with the add-hook command. +# see 'Hooks' section in module(1) manpage for the current list of events +# and their argument contract. several procedures can be registered on the +# same event: they are then all called, in their registration order, every +# time the event occurs +#proc myHookProcedure {modfile modname modnamevr modspec mode requested} { +# # code to run right before each modulefile evaluation +#} +#add-hook before-modulefile-eval myHookProcedure + diff --git a/tcl/init.tcl.in b/tcl/init.tcl.in index 66571f6a7..2616b57d2 100644 --- a/tcl/init.tcl.in +++ b/tcl/init.tcl.in @@ -179,6 +179,11 @@ array set g_config_defs [list\ wa_277 {MODULES_WA_277 @wa277@ 0 b {0 1}}\ ] +# List of events on which a site-specific procedure can be registered through +# the add-hook command +set g_hookEvents {before-modulefile-eval after-modulefile-eval\ + before-modulerc-eval after-modulerc-eval} + # Get state value proc getState {state {valifundef {}} {catchinitproc 0}} { if {![info exists ::g_states($state)]} { @@ -930,6 +935,32 @@ proc commandAbortOnError {{command {}}} { $abort_command_list}] } +# Register a site-specific procedure to be called when given hook event +# occurs. Several procedures may be registered on the same event, they are +# then called in their registration order +proc add-hook {event proc_name} { + if {$event ni $::g_hookEvents} { + knerror "Unknown hook event '$event'" + } + lappend ::g_hooks($event) $proc_name +} + +# Call every procedure registered on given hook event with provided +# arguments. An error raised by a hook procedure is reported but does not +# interrupt the other procedures registered on this event nor the +# modulefile or modulerc evaluation being wrapped +proc runHooks {event args} { + if {[info exists ::g_hooks($event)]} { + foreach hookproc $::g_hooks($event) { + if {[catch {$hookproc {*}$args} errMsg]} { + reportError "Hook procedure '$hookproc' registered on\ + '$event' event failed\n[formatErrStackTrace $::errorInfo\ + $hookproc {}]" + } + } + } +} + # ;;; Local Variables: # ;;; Mode: tcl-mode # ;;; tcl-indent-level: 3 diff --git a/tcl/interp.tcl.in b/tcl/interp.tcl.in index 9629c254a..44c42a8cb 100644 --- a/tcl/interp.tcl.in +++ b/tcl/interp.tcl.in @@ -276,6 +276,9 @@ proc execute-modulefile {modfile modname modnamevrvar modspec requested\ g_modfileUntrackVars g_modfileProcs $aliasesVN $aliasesPassArgVN\ $tracesVN g_modfileRenameCmds $dumpCommandsVN + runHooks before-modulefile-eval $modfile $modname $modnamevr $modspec\ + $mode $requested + set vr_spec_list [getVariantListFromVersSpec $modnamevr] set failed_eval [catch {evaluateModulefile $itrp $modfile $vr_spec_list}\ errorMsg] @@ -283,6 +286,9 @@ proc execute-modulefile {modfile modname modnamevrvar modspec requested\ set eval_return_code [renderModulefileEvalError $itrp $mode $modfile\ $failed_eval $errorMsg] + runHooks after-modulefile-eval $modfile $modname $modnamevr $modspec\ + $mode $requested $eval_return_code + if {$mode eq {load} && ![isStateDefined rc_running]} { if {[catch {checkModuleConflict $modname $modnamevr} errorMsg]} { reportError $errorMsg @@ -610,9 +616,14 @@ proc execute-modulerc {modfile modname modspec} { g_modrcUntrackVars g_modrcProcs g_modrcAliases g_modrcAliasesPassArg\ g_modrcAliasesTraces g_modrcRenameCmds g_modrcCommands + runHooks before-modulerc-eval $modfile $modname + set failed_eval [catch {evaluateModulerc $itrp $modfile} errorMsg] - renderModulercEvalError $itrp $modfile $failed_eval $errorMsg + set eval_return_code [renderModulercEvalError $itrp $modfile $failed_eval\ + $errorMsg] + + runHooks after-modulerc-eval $modfile $modname $eval_return_code # default version set via ModulesVersion variable in .version file # override previously defined default version for modname diff --git a/testsuite/example/siteconfig.tcl-1 b/testsuite/example/siteconfig.tcl-1 index f98d5edca..d50e768a6 100644 --- a/testsuite/example/siteconfig.tcl-1 +++ b/testsuite/example/siteconfig.tcl-1 @@ -720,4 +720,125 @@ if {[info exists env(TESTSUITE_ENABLE_SITECONFIG_INITCONFINITENVVARS)]} { report [string length [getConf init_envvars]] } +# add-hook command and hook event tests +if {[info exists env(TESTSUITE_ENABLE_SITECONFIG_HOOK)]} { + switch -- $env(TESTSUITE_ENABLE_SITECONFIG_HOOK) { + unknownevent { + add-hook not-an-event testsuite_hook_unknown + } + modulefile { + proc testsuite_hook_before_modulefile {modfile modname\ + modnamevr modspec mode requested} { + report "HOOK before-modulefile-eval $modfile $modname\ + $modnamevr $modspec $mode $requested" + } + proc testsuite_hook_after_modulefile {modfile modname modnamevr\ + modspec mode requested status} { + report "HOOK after-modulefile-eval $modfile $modname\ + $modnamevr $modspec $mode $requested $status" + } + add-hook before-modulefile-eval testsuite_hook_before_modulefile + add-hook after-modulefile-eval testsuite_hook_after_modulefile + } + modulerc { + proc testsuite_hook_before_modulerc {modfile modname} { + report "HOOK before-modulerc-eval $modfile $modname" + } + proc testsuite_hook_after_modulerc {modfile modname status} { + report "HOOK after-modulerc-eval $modfile $modname $status" + } + add-hook before-modulerc-eval testsuite_hook_before_modulerc + add-hook after-modulerc-eval testsuite_hook_after_modulerc + } + multiple { + proc testsuite_hook_multi1 {modfile modname modnamevr modspec\ + mode requested} { + report "HOOK1 $modname" + } + proc testsuite_hook_multi2 {modfile modname modnamevr modspec\ + mode requested} { + report "HOOK2 $modname" + } + add-hook before-modulefile-eval testsuite_hook_multi1 + add-hook before-modulefile-eval testsuite_hook_multi2 + } + error { + proc testsuite_hook_error {modfile modname modnamevr modspec\ + mode requested} { + error "hook failure for $modname" + } + proc testsuite_hook_after_error {modfile modname modnamevr\ + modspec mode requested} { + report "HOOK survived after error for $modname" + } + add-hook before-modulefile-eval testsuite_hook_error + add-hook before-modulefile-eval testsuite_hook_after_error + } + arity_missing { + proc testsuite_hook_arity_missing {modfile modname} { + report "HOOK should not be reached for $modname" + } + add-hook before-modulefile-eval testsuite_hook_arity_missing + } + arity_extra { + proc testsuite_hook_arity_extra {modfile modname modnamevr\ + modspec mode requested extra} { + report "HOOK should not be reached for $modname" + } + add-hook before-modulefile-eval testsuite_hook_arity_extra + } + codeerror { + proc testsuite_hook_codeerror {modfile modname modnamevr modspec\ + mode requested} { + testsuite_hook_undefined_command $modname + } + add-hook before-modulefile-eval testsuite_hook_codeerror + } + error_second { + proc testsuite_hook_first_ok {modfile modname modnamevr modspec\ + mode requested} { + report "HOOK1 ok for $modname" + } + proc testsuite_hook_second_error {modfile modname modnamevr\ + modspec mode requested} { + error "second hook failure for $modname" + } + add-hook before-modulefile-eval testsuite_hook_first_ok + add-hook before-modulefile-eval testsuite_hook_second_error + } + docexamples { + # replicates the auditRequestedLoad example from module.rst + # (using report instead of writing to a real file, for test + # purposes) + proc testsuite_hook_audit_requested {modfile modname modnamevr\ + modspec mode requested} { + if {$requested && $mode eq {load}} { + report "would audit: $modnamevr" + } + } + add-hook before-modulefile-eval testsuite_hook_audit_requested + + # replicates the hook_procedure example from module.rst + proc testsuite_hook_setenv {modfile modname modnamevr modspec\ + mode requested} { + set modfile_interp [getCurrentModfileInterpName] + interp eval $modfile_interp setenv MYVAR value + } + add-hook before-modulefile-eval testsuite_hook_setenv + } + blockmodule { + # replicates the blockModule example from module.rst (Advanced + # hooks): a low-level trace-based hook that can reject a + # modulefile evaluation outright, unlike add-hook + proc testsuite_hook_block_module {cmdstring op} { + if {[lindex $cmdstring 2] eq {foo/1.0}} { + error "foo/1.0 is blocked by site policy" + } + } + trace add execution execute-modulefile enter\ + testsuite_hook_block_module + } + } +} + } diff --git a/testsuite/modules.50-cmds/740-hook.exp b/testsuite/modules.50-cmds/740-hook.exp new file mode 100644 index 000000000..80f1864f1 --- /dev/null +++ b/testsuite/modules.50-cmds/740-hook.exp @@ -0,0 +1,496 @@ +############################################################################## +# Modules Revision 3.0 +# Providing a flexible user environment +# +# File: modules.50-cmds/%M% +# Revision: %I% +# First Edition: 2026/08/05 +# Last Mod.: %U%, %G% +# +# Authors: Xavier Delaruelle, xavier.delaruelle@cea.fr +# +# Description: Testuite testsequence +# Command: load, unload, avail +# Modulefiles: err, foo, puts, scan +# Sub-Command: +# +# Comment: %C{ +# Test add-hook command and hook events +# }C% +# +############################################################################## + +skip_if_quick_mode + +set mp $modpath.4 +set mpre $modpathre.4 +setenv_path_var MODULEPATH $mp + +# no global RC file evaluation should interfere with hook checks below +unsetenv_var MODULERCFILE + +# check expected siteconfig file is installed +set is_stderr_tty [siteconfig_isStderrTty] +if {$is_stderr_tty} { + +# unknown hook event is a fatal siteconfig error +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK unknownevent +set tserr [escre "$error_msgs: Site configuration source failed + Unknown hook event 'not-an-event'"] +append tserr {(\n.*)*} +testouterr_cmd_re sh {avail foo} ERR $tserr +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# before/after-modulefile-eval hooks fire with correct arguments on load +# and unload +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK modulefile + +set tserr "HOOK before-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0\ +load 1 +HOOK after-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0 load 1 0" +set ans [list] +lappend ans [list set _LMFILES_ $mp/foo/2.0] +lappend ans [list set LOADEDMODULES foo/2.0] +testouterr_cmd sh {load foo/2.0} $ans $tserr + +setenv_loaded_module [list foo/2.0] [list $mp/foo/2.0] +set tserr "HOOK before-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0\ +unload 1 +HOOK after-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0 unload 1 0" +set ans [list] +lappend ans [list unset _LMFILES_] +lappend ans [list unset LOADEDMODULES] +testouterr_cmd sh {unload foo/2.0} $ans $tserr +unsetenv_loaded_module + +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# before/after-modulerc-eval hooks fire with correct arguments, for both +# the modulepath root modulerc and the per-directory one +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK modulerc + +set tserr "HOOK before-modulerc-eval $mp/.modulerc .modulerc +HOOK after-modulerc-eval $mp/.modulerc .modulerc 0 +HOOK before-modulerc-eval $mp/puts/.modulerc puts/.modulerc +HOOK after-modulerc-eval $mp/puts/.modulerc puts/.modulerc 0" +set ans [list] +lappend ans [list set __MODULES_LMALTNAME puts/2&as|puts/default&as|puts/latest] +lappend ans [list set _LMFILES_ $mp/puts/2] +lappend ans [list set LOADEDMODULES puts/2] +testouterr_cmd sh {load puts/2} $ans $tserr + +setenv_loaded_module [list puts/2] [list $mp/puts/2] +unsetenv_loaded_module +unsetenv_var __MODULES_LMALTNAME + +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# after-modulerc-eval hook reports a non-zero status when modulerc +# evaluation fails, and evaluation of the module continues afterward +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK modulerc +setenv_var TESTSUITE_PUTS log_rc_and_unk + +set tserr [escre "HOOK before-modulerc-eval $mp/.modulerc .modulerc +HOOK after-modulerc-eval $mp/.modulerc .modulerc 0 +HOOK before-modulerc-eval $mp/puts/.modulerc puts/.modulerc"] +append tserr {(\n.*)*} +append tserr [escre "HOOK after-modulerc-eval $mp/puts/.modulerc puts/.modulerc 1"] +set ans [list] +lappend ans [list set __MODULES_LMALTNAME puts/2&as|puts/default&as|puts/latest] +lappend ans [list set _LMFILES_ $mp/puts/2] +lappend ans [list set LOADEDMODULES puts/2] +testouterr_cmd_re sh {load puts/2} [shell_out sh $ans] $tserr + +setenv_loaded_module [list puts/2] [list $mp/puts/2] +unsetenv_loaded_module +unsetenv_var __MODULES_LMALTNAME + +unsetenv_var TESTSUITE_PUTS +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# several procedures registered on the same event are called in their +# registration order +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK multiple +set tserr "HOOK1 foo/2.0 +HOOK2 foo/2.0" +set ans [list] +lappend ans [list set _LMFILES_ $mp/foo/2.0] +lappend ans [list set LOADEDMODULES foo/2.0] +testouterr_cmd sh {load foo/2.0} $ans $tserr +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# an error raised by a hook procedure is reported but does not prevent the +# other procedures registered on the same event from running, nor the +# wrapped modulefile evaluation from proceeding +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK error +set tserr [escre "HOOK survived after error for foo/2.0 + +Loading foo/2.0 + ERROR: Hook procedure 'testsuite_hook_error' registered on'before-modulefile-eval' event failed + hook failure for foo/2.0"] +append tserr {(\n.*)*} +set ans [list] +lappend ans [list set _LMFILES_ $mp/foo/2.0] +lappend ans [list set LOADEDMODULES foo/2.0] +lappend ans [list ERR] +testouterr_cmd_re sh {load foo/2.0} [shell_out sh $ans] $tserr +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# a registered hook procedure declared with fewer arguments than the event +# provides is reported as a wrong-number-of-arguments error, non-fatally +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK arity_missing +set tserr [escre "Loading foo/2.0 + ERROR: Hook procedure 'testsuite_hook_arity_missing' registered on'before-modulefile-eval' event failed + wrong # args: should be \"testsuite_hook_arity_missing modfile\ +modname\""] +append tserr {(\n.*)*} +set ans [list] +lappend ans [list set _LMFILES_ $mp/foo/2.0] +lappend ans [list set LOADEDMODULES foo/2.0] +lappend ans [list ERR] +testouterr_cmd_re sh {load foo/2.0} [shell_out sh $ans] $tserr +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# a registered hook procedure declared with more (required) arguments than +# the event provides is reported as a wrong-number-of-arguments error, +# non-fatally +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK arity_extra +set tserr [escre "Loading foo/2.0 + ERROR: Hook procedure 'testsuite_hook_arity_extra' registered on'before-modulefile-eval' event failed + wrong # args: should be"] +append tserr {.*(\n.*)*} +set ans [list] +lappend ans [list set _LMFILES_ $mp/foo/2.0] +lappend ans [list set LOADEDMODULES foo/2.0] +lappend ans [list ERR] +testouterr_cmd_re sh {load foo/2.0} [shell_out sh $ans] $tserr +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# a Tcl code issue (call to an undefined command) in a registered hook +# procedure is reported non-fatally, same as an explicit error +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK codeerror +set tserr [escre "Loading foo/2.0 + ERROR: Hook procedure 'testsuite_hook_codeerror' registered on'before-modulefile-eval' event failed + invalid command name \"testsuite_hook_undefined_command\""] +append tserr {(\n.*)*} +set ans [list] +lappend ans [list set _LMFILES_ $mp/foo/2.0] +lappend ans [list set LOADEDMODULES foo/2.0] +lappend ans [list ERR] +testouterr_cmd_re sh {load foo/2.0} [shell_out sh $ans] $tserr +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# an error raised by the second registered procedure does not undo the +# result (its report output) already produced by the first registered +# procedure +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK error_second +set tserr [escre "HOOK1 ok for foo/2.0 + +Loading foo/2.0 + ERROR: Hook procedure 'testsuite_hook_second_error' registered on'before-modulefile-eval' event failed + second hook failure for foo/2.0"] +append tserr {(\n.*)*} +set ans [list] +lappend ans [list set _LMFILES_ $mp/foo/2.0] +lappend ans [list set LOADEDMODULES foo/2.0] +lappend ans [list ERR] +testouterr_cmd_re sh {load foo/2.0} [shell_out sh $ans] $tserr +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# +# hooks fire with the correct 'mode' argument across the different module +# evaluation modes, on both a successful and a failing evaluation +# + +set sepline [string repeat - 67] +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK modulefile + +# help, successful evaluation (mode help, requested) +set tserr "$sepline +Module Specific Help for $mp/foo/2.0: + +HOOK before-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0 help 1 +WARNING: Unable to find ModulesHelp in $mp/foo/2.0. +HOOK after-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0 help 1 0 +$sepline" +testouterr_cmd sh {help foo/2.0} OK $tserr + +# display, successful evaluation (mode display, requested) +set tserr "$sepline +$mp/foo/2.0: + +HOOK before-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0 display 1 +HOOK after-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0 display 1 0 +$sepline" +testouterr_cmd sh {display foo/2.0} OK $tserr + +# show, successful evaluation (same as display, mode display, requested) +testouterr_cmd sh {show foo/2.0} OK $tserr + +# whatis, successful evaluation (mode whatis, not requested) +set tserr "HOOK before-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0\ +whatis 0 +HOOK after-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0 whatis 0 0" +testouterr_cmd sh {whatis foo/2.0} OK $tserr + +# search, successful evaluation (mode whatis, not requested); search +# evaluates every module in the modulepath so only check the lines +# relating to the targeted module are present, tolerating the rest +set tserr [escre "HOOK before-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0\ +foo/2.0 whatis 0 +HOOK after-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0 whatis 0 0"] +testouterr_cmd_re sh {search foo/2.0} {.*} "(.*\n)*${tserr}(\n.*)*" + +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# for the failing evaluation checks below, only the before-hook line and +# the after-hook line with its status are matched exactly; the internal +# Tcl error text in between (whose formatting/indentation differs depending +# on whether the command holds its output for later display) is tolerated +# with a wildcard, as it is already covered by dedicated non-hook tests +# elsewhere in the testsuite + +# help, failing evaluation (status 1, overall command fails) +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK modulefile +setenv_var TESTSUITE_ABORT_ON_ERROR error +set tserr [escre "$sepline +Module Specific Help for $mp/err/1.0: + +HOOK before-modulefile-eval $mp/err/1.0 err/1.0 err/1.0 err/1.0 help 1"] +append tserr {.*(\n.*)*} +append tserr [escre "HOOK after-modulefile-eval $mp/err/1.0 err/1.0 err/1.0\ +err/1.0 help 1 1 +$sepline"] +testouterr_cmd_re sh {help err/1.0} ERR $tserr + +# display, failing evaluation (status 1, overall command fails) +set tserr [escre "$sepline +$mp/err/1.0: + +HOOK before-modulefile-eval $mp/err/1.0 err/1.0 err/1.0 err/1.0 display 1"] +append tserr {.*(\n.*)*} +append tserr [escre "HOOK after-modulefile-eval $mp/err/1.0 err/1.0 err/1.0\ +err/1.0 display 1 1 +$sepline"] +testouterr_cmd_re sh {display err/1.0} ERR $tserr + +# show, failing evaluation (same as display) +testouterr_cmd_re sh {show err/1.0} ERR $tserr + +# whatis, failing evaluation (status 1, but overall command still succeeds) +set tserr [escre\ + "HOOK before-modulefile-eval $mp/err/1.0 err/1.0 err/1.0 err/1.0 whatis 0"] +append tserr {.*(\n.*)*} +append tserr [escre\ + "HOOK after-modulefile-eval $mp/err/1.0 err/1.0 err/1.0 err/1.0 whatis 0\ +1"] +testouterr_cmd_re sh {whatis err/1.0} {} $tserr + +# search, failing evaluation (status 1, overall command still succeeds); +# only check the lines relating to the targeted module are present +set tserr [escre\ + "HOOK before-modulefile-eval $mp/err/1.0 err/1.0 err/1.0 err/1.0 whatis 0"] +append tserr {.*(\n.*)*} +append tserr [escre\ + "HOOK after-modulefile-eval $mp/err/1.0 err/1.0 err/1.0 err/1.0 whatis 0\ +1"] +testouterr_cmd_re sh {search err/1.0} {.*} "(.*\n)*${tserr}(\n.*)*" + +unsetenv_var TESTSUITE_ABORT_ON_ERROR +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# +# scan mode: modulefiles evaluated during an extra-match search triggered +# by a variant-aware avail query +# + +setenv_var MODULES_ADVANCED_VERSION_SPEC 1 +setenv_var MODULES_AVAIL_INDEPTH 1 +setenv_var MODULES_AVAIL_TERSE_OUTPUT alias:dirwsym:sym:tag:variant +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK modulefile + +# avail listing itself is written to stderr, not stdout, like the hook +# messages, so both are checked together as anserr, in the order they are +# produced: hook messages first, then the listing +set listing "scan/1.0\nscan/1.1\nscan/2.0\{foo=val1,val2,val3\}\n" +append listing "scan/2.1\{foo=val1,val2,val3\}" + +# successful scan evaluation (mode scan, not requested) +setenv_var TESTSUITE_SCAN hooktest +set tserr "HOOK before-modulefile-eval $mp/scan/1.0 scan/1.1 scan/1.1\ +scan/1.1 scan 0 +HOOK after-modulefile-eval $mp/scan/1.0 scan/1.1 scan/1.1 scan/1.1 scan 0 0 +HOOK before-modulefile-eval $mp/scan/2.0 scan/2.0 scan/2.0 scan/2.0 scan 0 +HOOK after-modulefile-eval $mp/scan/2.0 scan/2.0 scan/2.0 scan/2.0 scan 0 0 +HOOK before-modulefile-eval $mp/scan/2.0 scan/2.1 scan/2.1 scan/2.1 scan 0 +HOOK after-modulefile-eval $mp/scan/2.0 scan/2.1 scan/2.1 scan/2.1 scan 0 0 +HOOK before-modulefile-eval $mp/scan/1.0 scan/1.0 scan/1.0 scan/1.0 scan 0 +HOOK after-modulefile-eval $mp/scan/1.0 scan/1.0 scan/1.0 scan/1.0 scan 0 0 +$listing" +testouterr_cmd sh {avail -t scan} {} $tserr +unsetenv_var TESTSUITE_SCAN + +# failing scan evaluation (status 1, but errors are silently inhibited and +# overall command still succeeds, consistent with scan mode tolerating +# broken modulefiles during a background scan) +setenv_var TESTSUITE_SCAN unk1 +set tserr "HOOK before-modulefile-eval $mp/scan/1.0 scan/1.1 scan/1.1\ +scan/1.1 scan 0 +HOOK after-modulefile-eval $mp/scan/1.0 scan/1.1 scan/1.1 scan/1.1 scan 0 1 +HOOK before-modulefile-eval $mp/scan/2.0 scan/2.0 scan/2.0 scan/2.0 scan 0 +HOOK after-modulefile-eval $mp/scan/2.0 scan/2.0 scan/2.0 scan/2.0 scan 0 0 +HOOK before-modulefile-eval $mp/scan/2.0 scan/2.1 scan/2.1 scan/2.1 scan 0 +HOOK after-modulefile-eval $mp/scan/2.0 scan/2.1 scan/2.1 scan/2.1 scan 0 0 +HOOK before-modulefile-eval $mp/scan/1.0 scan/1.0 scan/1.0 scan/1.0 scan 0 +HOOK after-modulefile-eval $mp/scan/1.0 scan/1.0 scan/1.0 scan/1.0 scan 0 1 +$listing" +testouterr_cmd sh {avail -t scan} {} $tserr +unsetenv_var TESTSUITE_SCAN + +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK +unsetenv_var MODULES_ADVANCED_VERSION_SPEC +unsetenv_var MODULES_AVAIL_INDEPTH +unsetenv_var MODULES_AVAIL_TERSE_OUTPUT + + +# before/after-modulefile-eval hooks also fire for the global and user RC +# files sourced ahead of the requested module: these files are evaluated +# through execute-modulefile (mode load), not execute-modulerc, same as any +# other modulefile passed to the 'source' sub-command +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK modulefile +setenv_var MODULERCFILE "$env(TESTSUITEDIR)/etc/modulerc:$env(TESTSUITEDIR)/etc/rc" + +set globalrc $env(TESTSUITEDIR)/etc/modulerc +set userrc $env(TESTSUITEDIR)/etc/rc +set tserr "HOOK before-modulefile-eval $globalrc $globalrc $globalrc\ +$globalrc load 1 +HOOK after-modulefile-eval $globalrc $globalrc $globalrc $globalrc load 1 0 +HOOK before-modulefile-eval $userrc $userrc $userrc $userrc load 1 +HOOK after-modulefile-eval $userrc $userrc $userrc $userrc load 1 0 +HOOK before-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0 load 1 +HOOK after-modulefile-eval $mp/foo/2.0 foo/2.0 foo/2.0 foo/2.0 load 1 0" +set ans [list] +lappend ans [list set _LMFILES_ $mp/foo/2.0] +lappend ans [list set LOADEDMODULES foo/2.0] +testouterr_cmd sh {load foo/2.0} $ans $tserr + +unsetenv_var MODULERCFILE +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# hooks fire with the resolved variant specification in modnamevr while +# modname stays the bare module name +set mp3 $modpath.3 +setenv_path_var MODULEPATH $mp3 +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK modulefile +setenv_var TESTSUITE_VARIANT_TAG default + +set tserr "HOOK before-modulefile-eval $mp3/variant/8.0 variant/8.0\ +variant/8.0 foo=val1 variant/8.0 foo=val1 load 1 +HOOK after-modulefile-eval $mp3/variant/8.0 variant/8.0 variant/8.0\ +foo=val1 variant/8.0 foo=val1 load 1 0" +set ans [list] +lappend ans [list set __MODULES_LMVARIANT\ + variant/8.0&foo|val1|0|0&bar|2|0|2] +lappend ans [list set _LMFILES_ $mp3/variant/8.0] +lappend ans [list set LOADEDMODULES variant/8.0] +testouterr_cmd sh {load variant/8.0 foo=val1} $ans $tserr + +unsetenv_var TESTSUITE_VARIANT_TAG +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK +setenv_path_var MODULEPATH $mp + + +# +# validate the add-hook examples given in the module.rst documentation +# + +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK docexamples + +# auditRequestedLoad-equivalent example only reports on a directly +# requested load, and the hook_procedure-equivalent example successfully +# defines MYVAR through the modulefile interpreter, proving a +# before-modulefile-eval hook can now reach it (it fires after the +# interpreter has been created and reset) +set tserr {would audit: foo/2.0} +set ans [list] +lappend ans [list set _LMFILES_ $mp/foo/2.0] +lappend ans [list set LOADEDMODULES foo/2.0] +lappend ans [list set MYVAR value] +testouterr_cmd sh {load foo/2.0} $ans $tserr + +# not a direct load request: neither example produces anything +testouterr_cmd sh {whatis foo/2.0} OK {} + +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# the "Advanced hooks" blockModule example rejects loading foo/1.0 +# specifically, through a low-level trace add-hook cannot express, while +# leaving every other module -- including foo/1.0 itself in a listing -- +# unaffected +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK blockmodule + +set tserr [escre "Loading foo/1.0 + ERROR: foo/1.0 is blocked by site policy"] +append tserr {(\n.*)*} +testouterr_cmd_re sh {load foo/1.0} ERR $tserr + +set ans [list] +lappend ans [list set _LMFILES_ $mp/foo/2.0] +lappend ans [list set LOADEDMODULES foo/2.0] +testouterr_cmd sh {load foo/2.0} $ans {} + +set ans "$mp:\nfoo/1.0\nfoo/2.0\nfoo/9.0" +testouterr_cmd sh {avail -t foo} OK $ans + +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + + +# modulerc hooks do not fire for the global/user RC files sourced ahead of +# a requested module (see the before/after-modulefile-eval hooks test +# above, which shows those same files instead trigger the modulefile +# events): only the modulerc hooks for the real .modulerc files resolved +# under the modulepath are produced +setenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK modulerc +setenv_var MODULERCFILE "$env(TESTSUITEDIR)/etc/modulerc:$env(TESTSUITEDIR)/etc/rc" + +set tserr "HOOK before-modulerc-eval $mp/.modulerc .modulerc +HOOK after-modulerc-eval $mp/.modulerc .modulerc 0 +HOOK before-modulerc-eval $mp/puts/.modulerc puts/.modulerc +HOOK after-modulerc-eval $mp/puts/.modulerc puts/.modulerc 0" +set ans [list] +lappend ans [list set __MODULES_LMALTNAME puts/2&as|puts/default&as|puts/latest] +lappend ans [list set _LMFILES_ $mp/puts/2] +lappend ans [list set LOADEDMODULES puts/2] +testouterr_cmd sh {load puts/2} $ans $tserr + +setenv_loaded_module [list puts/2] [list $mp/puts/2] +unsetenv_loaded_module +unsetenv_var __MODULES_LMALTNAME + +unsetenv_var MODULERCFILE +unsetenv_var TESTSUITE_ENABLE_SITECONFIG_HOOK + +} elseif {$verbose} { + send_user "\tSkip tests relying on an excepted siteconfig file installed\n" +} + + +# +# Cleanup +# + +reset_test_env