diff --git a/rt/termWidthFallback/err.txt b/rt/termWidthFallback/err.txt new file mode 100644 index 000000000..19ba1c863 --- /dev/null +++ b/rt/termWidthFallback/err.txt @@ -0,0 +1,33 @@ +=========================== +step 1 +lua ProjectDIR/src/lmod.in.lua shell --regression_testing avail +=========================== +ProjectDIR/rt/termWidthFallback/mf + alpha/1.0 beta/1.0 gamma/1.0 +If the avail list is too long consider trying: +"module --default avail" or "ml -d av" to just list the default modules. +"module overview" or "ml ov" to display the number of modules for each name. +Use "module spider" to find all possible modules and extensions. +Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys". +=========================== +step 2 +lua ProjectDIR/src/lmod.in.lua shell --regression_testing avail +=========================== +ProjectDIR/rt/termWidthFallback/mf + alpha/1.0 beta/1.0 gamma/1.0 +If the avail list is too long consider trying: +"module --default avail" or "ml -d av" to just list the default modules. +"module overview" or "ml ov" to display the number of modules for each name. +Use "module spider" to find all possible modules and extensions. +Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys". +=========================== +step 3 +lua ProjectDIR/src/lmod.in.lua shell --regression_testing avail +=========================== +ProjectDIR/rt/termWidthFallback/mf + alpha/1.0 beta/1.0 gamma/1.0 +If the avail list is too long consider trying: +"module --default avail" or "ml -d av" to just list the default modules. +"module overview" or "ml ov" to display the number of modules for each name. +Use "module spider" to find all possible modules and extensions. +Use "module keyword key1 key2 ..." to search for all possible modules matching any of the "keys". diff --git a/rt/termWidthFallback/mf/alpha/1.0.lua b/rt/termWidthFallback/mf/alpha/1.0.lua new file mode 100644 index 000000000..7a1ad1771 --- /dev/null +++ b/rt/termWidthFallback/mf/alpha/1.0.lua @@ -0,0 +1 @@ +setenv("TW_ALPHA", "1") diff --git a/rt/termWidthFallback/mf/beta/1.0.lua b/rt/termWidthFallback/mf/beta/1.0.lua new file mode 100644 index 000000000..7a9b3cb71 --- /dev/null +++ b/rt/termWidthFallback/mf/beta/1.0.lua @@ -0,0 +1 @@ +setenv("TW_BETA", "1") diff --git a/rt/termWidthFallback/mf/gamma/1.0.lua b/rt/termWidthFallback/mf/gamma/1.0.lua new file mode 100644 index 000000000..31d69bfc7 --- /dev/null +++ b/rt/termWidthFallback/mf/gamma/1.0.lua @@ -0,0 +1 @@ +setenv("TW_GAMMA", "1") diff --git a/rt/termWidthFallback/out.txt b/rt/termWidthFallback/out.txt new file mode 100644 index 000000000..abee034bc --- /dev/null +++ b/rt/termWidthFallback/out.txt @@ -0,0 +1,30 @@ +=========================== +step 1 +lua ProjectDIR/src/lmod.in.lua shell --regression_testing avail +=========================== +__LMOD_REF_COUNT_MODULEPATH=ProjectDIR/rt/termWidthFallback/mf:1; +export __LMOD_REF_COUNT_MODULEPATH; +MODULEPATH=ProjectDIR/rt/termWidthFallback/mf; +export MODULEPATH; +_ModuleTable_='_ModuleTable_={MTversion=3,depthT={},family={},mT={},mpathA={"ProjectDIR/rt/termWidthFallback/mf",},systemBaseMPATH="ProjectDIR/rt/termWidthFallback/mf",}'; +export _ModuleTable_; +=========================== +step 2 +lua ProjectDIR/src/lmod.in.lua shell --regression_testing avail +=========================== +MODULEPATH=ProjectDIR/rt/termWidthFallback/mf; +export MODULEPATH; +_ModuleTable_='_ModuleTable_={MTversion=3,depthT={},family={},mT={},mpathA={"ProjectDIR/rt/termWidthFallback/mf",},systemBaseMPATH="ProjectDIR/rt/termWidthFallback/mf",}'; +export _ModuleTable_; +=========================== +step 3 +lua ProjectDIR/src/lmod.in.lua shell --regression_testing avail +=========================== +MODULEPATH=ProjectDIR/rt/termWidthFallback/mf; +export MODULEPATH; +_ModuleTable_='_ModuleTable_={MTversion=3,depthT={},family={},mT={},mpathA={"ProjectDIR/rt/termWidthFallback/mf",},systemBaseMPATH="ProjectDIR/rt/termWidthFallback/mf",}'; +export _ModuleTable_; +scenario-C wedged-stty completed: yes +scenario-C elapsed seconds: 1 +scenario-A probe invocations: 0 +scenario-B probe invocations: nonzero diff --git a/rt/termWidthFallback/termWidthFallback.tdesc b/rt/termWidthFallback/termWidthFallback.tdesc new file mode 100644 index 000000000..52fdaf38d --- /dev/null +++ b/rt/termWidthFallback/termWidthFallback.tdesc @@ -0,0 +1,116 @@ +-- -*- lua -*- +local testName = "termWidthFallback" + +testdescript = { + owner = "rtm", + product = "modules", + description = [[ + Verify that TermWidth.lua honors $COLUMNS instead of probing + stty/tput, and that the stty/tput probes themselves are bounded + by SIGALRM so a wedged PTY cannot hang module commands. + Regression coverage for GH-832. + ]], + keywords = { testName }, + + active = true, + testName = testName, + job_submit_method = "INTERACTIVE", + + runScript = [[ + + . $(projectDir)/rt/common_funcs.sh + + unsetMT + initStdEnvVars + export MODULEPATH=$(testDir)/mf + unset LMOD_TERM_WIDTH + + # Install fake stty/tput on PATH that report a sentinel width (999) + # and log every invocation. When $COLUMNS is honored these MUST NOT + # be called. Sentinel-vs-COLUMNS lets us tell the two paths apart + # in the debug output without depending on real terminal sizing. + mkdir -p _fakebin + cat > _fakebin/stty <<'FAKE' +#!/bin/sh +echo "stty $*" >> "$TW_PROBE_LOG" +[ "$1" = "size" ] && { echo "24 999"; exit 0; } +exec /usr/bin/stty "$@" 2>/dev/null || true +FAKE + cat > _fakebin/tput <<'FAKE' +#!/bin/sh +echo "tput $*" >> "$TW_PROBE_LOG" +[ "$1" = "cols" ] && { echo "999"; exit 0; } +exec /usr/bin/tput "$@" 2>/dev/null || true +FAKE + chmod +x _fakebin/stty _fakebin/tput + export PATH="$PWD/_fakebin:$PATH" + export TW_PROBE_LOG="$PWD/probe.log" + + remove_generated_lmod_files + + # Scenario A: $COLUMNS set -> probes MUST NOT be invoked. + : > "$TW_PROBE_LOG" + export COLUMNS=120 + runLmod avail 2>/dev/null # 1 + if [ -s "$TW_PROBE_LOG" ]; then + echo "scenario-A probe invocations: nonzero" > _stdout.998 + else + echo "scenario-A probe invocations: 0" > _stdout.998 + fi + + # Scenario B: $COLUMNS unset -> probes run, sentinel 999 is used. + : > "$TW_PROBE_LOG" + unset COLUMNS + export TERM=xterm + runLmod avail 2>/dev/null # 2 + if [ -s "$TW_PROBE_LOG" ]; then + echo "scenario-B probe invocations: nonzero" > _stdout.999 + else + echo "scenario-B probe invocations: 0" > _stdout.999 + fi + + # Scenario C: wedged stty size -> alarm must return without hanging. + cat > _fakebin/stty <<'FAKE' +#!/bin/sh +echo "stty $*" >> "$TW_PROBE_LOG" +[ "$1" = "size" ] && sleep 30 +exec /usr/bin/stty "$@" 2>/dev/null || true +FAKE + chmod +x _fakebin/stty + : > "$TW_PROBE_LOG" + unset COLUMNS + export TERM=xterm + SECONDS=0 + runLmod avail 2>/dev/null # 3 + elapsedC=$SECONDS + if [ "$elapsedC" -lt 5 ]; then + echo "scenario-C wedged-stty completed: yes" > _stdout.997 + else + echo "scenario-C wedged-stty completed: no" > _stdout.997 + fi + echo "scenario-C elapsed seconds: $elapsedC" >> _stdout.997 + + HOME=$ORIG_HOME + + cat _stdout.[0-9][0-9][0-9] > _stdout.orig + joinBase64Results -bash _stdout.orig _stdout.new + cleanUp _stdout.new out.txt + + cat _stderr.[0-9][0-9][0-9] > _stderr.orig + cleanUp _stderr.orig err.txt + + rm -f results.csv + wrapperDiff --csv results.csv $(testDir)/out.txt out.txt + wrapperDiff --csv results.csv $(testDir)/err.txt err.txt + testFinish -r $(resultFn) -t $(runtimeFn) results.csv + ]], + + blessScript = [[ + # perform what is needed + ]], + + tests = { + { id='t1'}, + }, + +} diff --git a/tools/TermWidth.lua b/tools/TermWidth.lua index ac940bfc8..3c8ca82c8 100644 --- a/tools/TermWidth.lua +++ b/tools/TermWidth.lua @@ -34,33 +34,137 @@ require("strict") -------------------------------------------------------------------------- +_G._DEBUG = false -- Required by luaposix 33 +local posix = require("posix") +local unistd = posix.unistd +local wait = posix.sys.wait +-- Pull the signal API from the posix.signal submodule. On the merged `posix` +-- table `posix.signal` is the signal() *function* (e.g. luaposix 35 on EL9), so +-- `posix.signal.kill` errors at load time and Lmod fails to start; requiring the +-- submodule gives a table with kill/signal and the SIG* constants on every +-- supported luaposix. +local psignal = require("posix.signal") +local kill = psignal.kill +local SIGALRM = psignal.SIGALRM +local SIGKILL = psignal.SIGKILL +local setenv_posix = posix.setenv + require("haveTermSupport") -require("capture") -local capture = capture or function (s) return nil end local getenv = os.getenv local term = false local s_width = false local min = math.min local s_DFLT = 80 +-- Bound stty/tput probes below. A wedged PTY (ioctl(TIOCGWINSZ) that +-- never returns) would otherwise leave `module` blocked forever inside +-- io.popen :read("*all") -- see GH-832. Kept local to TermWidth so +-- general capture() callers are unaffected. +local s_termWidthTimeout = 1 + +local function l_withCaptureEnv(fn) + local cosmic = require("Cosmic"):singleton() + local newT = {} + local envT = {} + local env_ldT = { + LMOD_LD_LIBRARY_PATH = "LD_LIBRARY_PATH", + LMOD_LD_PRELOAD = "LD_PRELOAD", + } + + for k, v in pairs(env_ldT) do + local value = cosmic:get(k, "") + if (value ~= "") then + envT[v] = value + end + end + + for k, v in pairs(envT) do + newT[k] = getenv(k) or false + setenv_posix(k, v, true) + end + + local result = fn() + + for k, myValue in pairs(newT) do + local v = myValue + if (v == false) then v = nil end + setenv_posix(k, v, true) + end + + return result +end + +local function l_timedCapture(cmd, sec) + return l_withCaptureEnv(function () + local r, w = unistd.pipe() + if (not r) then + return nil + end + + local pid = unistd.fork() + if (pid == 0) then + unistd.close(r) + unistd.dup2(w, 1) + unistd.close(w) + os.execute("sh -c " .. string.format("%q", cmd)) + os.exit(0) + end + + unistd.close(w) + + local timed = false + local cpid = pid + local oldSig = psignal.signal(SIGALRM, function () + timed = true + kill(cpid, SIGKILL) + end) + + unistd.alarm(sec) + + local outA = {} + while (true) do + local chunk = unistd.read(r, 4096) + if (not chunk or chunk == "") then + break + end + outA[#outA + 1] = chunk + end + + unistd.alarm(0) + psignal.signal(SIGALRM, oldSig) + unistd.close(r) + wait.wait(cpid) + + if (timed) then + return nil + end + + return table.concat(outA) + end) +end + ------------------------------------------------------------------------ -- Ask system for width. local function l_askSystem(width) -- try stty size - local r_c = capture("stty size 2> /dev/null") - local i, j, rows, columns = r_c:find('(%d+)%s+(%d+)') - if (i) then - return tonumber(columns) + local r_c = l_timedCapture("stty size 2> /dev/null", s_termWidthTimeout) + if (r_c) then + local i, j, rows, columns = r_c:find('(%d+)%s+(%d+)') + if (i) then + return tonumber(columns) + end end -- Try tput cols if (getenv("TERM")) then - local result = capture("tput cols 2> /dev/null") - i, j, columns = result:find("^(%d+)") - if (i) then - return tonumber(columns) + local result = l_timedCapture("tput cols 2> /dev/null", s_termWidthTimeout) + if (result) then + local i, j, columns = result:find("^(%d+)") + if (i) then + return tonumber(columns) + end end end @@ -80,10 +184,16 @@ function TermWidth() s_width = ltw return s_width end - s_DFLT = ltw or s_DFLT - s_width = s_DFLT - if (haveTermSupport()) then - s_width = l_askSystem(s_width) + + local cols = tonumber(getenv("COLUMNS")) + if (cols) then + s_width = cols + else + s_DFLT = ltw or s_DFLT + s_width = s_DFLT + if (haveTermSupport()) then + s_width = l_askSystem(s_width) + end end local maxW = ltw or math.huge