Skip to content

Commit a27d416

Browse files
Tighten sinteractive header and harden runtime
- Rewrite the header as a clean-room reimplementation; drop the stale 2017 version line and credit the original NSC/CU Boulder work as inspiration rather than ancestry. - Replace the fixed pre-connect sleep with a poll for the tmux session (single SSH that retries on the node), removing the slow-node race. - Normalize parse_time carries (90m -> 1h30m, 25h -> 1d1h) so every field stays in range for Slurm's [D-]HH:MM:SS format. - Add `set -u`; guard every optional value. This fixes a latent infinite loop on a trailing valueless option (e.g. `sinteractive -n`) by adding explicit argument-presence checks. - Clean up the list_sessions tmpdir on interrupt via an EXIT trap. - Document the duplicate-name guard as best-effort; egrep -> grep -E; fix two comments that drifted from the 1-day default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7e35127 commit a27d416

1 file changed

Lines changed: 65 additions & 23 deletions

File tree

scripts/sinteractive

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
#!/bin/bash
22
# -*- coding: utf-8 -*-
3-
# Original Author: Pär Andersson (National Supercomputer Centre, Sweden)
4-
# Updated by: Jonathon Anderson (University of Colorado Boulder)
5-
# Updated by: Jay Hesselberth
6-
# Version: 2017-09-15
73
#
8-
# This will submit a batch script that starts tmux on a node. Then
9-
# ssh is used to connect to the node and attach the tmux session. The
10-
# result is similar to an interactive shell in PBS (qsub -I)
4+
# sinteractive — start an interactive shell on a Slurm compute node.
5+
#
6+
# Submits a batch job that launches a detached tmux session on the
7+
# allocated node, then SSHes in and attaches to it. Because the shell
8+
# lives in tmux, the session survives SSH drops and can be reattached
9+
# later (see --attach / --list).
10+
#
11+
# Clean-room reimplementation, inspired by the original sinteractive by
12+
# Pär Andersson (NSC, Sweden) and the later CU Boulder adaptation by
13+
# Jonathon Anderson. Maintained by Jay Hesselberth.
14+
15+
# Treat unset variables as errors; the argument handling below guards every
16+
# optional value explicitly, so this only fires on genuine bugs.
17+
set -u
1118

1219
TMUX_BIN='/usr/local/bin/tmux'
1320
JOB_ID=''
@@ -50,6 +57,13 @@ function parse_time {
5057
return
5158
fi
5259

60+
# Normalize carries so e.g. 90m -> 1h30m and 25h -> 1d1h, keeping each
61+
# field within range for SLURM's [D-]HH:MM:SS format.
62+
hours=$((hours + minutes / 60))
63+
minutes=$((minutes % 60))
64+
days=$((days + hours / 24))
65+
hours=$((hours % 24))
66+
5367
if ((days > 0)); then
5468
printf '%d-%02d:%02d:00\n' "$days" "$hours" "$minutes"
5569
else
@@ -82,7 +96,7 @@ sbatch ntasks instead of -n.
8296
8397
All other options are passed directly to sbatch. Examples:
8498
85-
sinteractive # 8h session, 2 CPUs, 8G memory
99+
sinteractive # 1-day session, 2 CPUs, 8G memory
86100
sinteractive -n myproj # named session
87101
sinteractive --node compute01 # run on a specific node
88102
sinteractive --time=2h -p rna # 2h session on rna partition
@@ -107,18 +121,18 @@ EOF
107121
}
108122

109123
function main {
110-
if [[ "$1" == 'tmux' ]]; then
124+
if [[ "${1:-}" == 'tmux' ]]; then
111125
shift
112126
start_tmux "$@"
113127
return
114-
elif [[ "$1" == 'attach' ]]; then
128+
elif [[ "${1:-}" == 'attach' ]]; then
115129
shift
116130
attach_tmux "$@"
117131
return
118132
fi
119133

120134
# Don't allow launching sinteractive from inside an existing session
121-
if [[ -n "$TMUX" ]] && [[ "$TMUX" == *sinteractive* ]]; then
135+
if [[ -n "${TMUX:-}" ]] && [[ "$TMUX" == *sinteractive* ]]; then
122136
echo 1>&2 "Error: Already inside an sinteractive session. Exit this session first."
123137
exit 1
124138
fi
@@ -136,7 +150,7 @@ function main {
136150
list_sessions
137151
;;
138152
-a | --attach)
139-
if [[ -z "$2" || "$2" == -* ]]; then
153+
if [[ -z "${2:-}" || "$2" == -* ]]; then
140154
echo 1>&2 "Error: $1 requires a JOBID or NAME argument."
141155
echo 1>&2 "Run 'sinteractive --list' to see available sessions."
142156
exit 1
@@ -154,6 +168,7 @@ function main {
154168
shift
155169
;;
156170
-n | --name)
171+
[[ $# -ge 2 ]] || { echo 1>&2 "Error: $1 requires a NAME argument."; exit 1; }
157172
session_name="$2"
158173
shift 2
159174
;;
@@ -162,6 +177,7 @@ function main {
162177
shift
163178
;;
164179
--node)
180+
[[ $# -ge 2 ]] || { echo 1>&2 "Error: --node requires a NODE argument."; exit 1; }
165181
args+=(--nodelist="$2")
166182
shift 2
167183
;;
@@ -170,6 +186,7 @@ function main {
170186
shift
171187
;;
172188
--time)
189+
[[ $# -ge 2 ]] || { echo 1>&2 "Error: --time requires a TIME argument."; exit 1; }
173190
args+=(--time="$(parse_time "$2")")
174191
shift 2
175192
;;
@@ -178,10 +195,12 @@ function main {
178195
shift
179196
;;
180197
-t)
198+
[[ $# -ge 2 ]] || { echo 1>&2 "Error: -t requires a TIME argument."; exit 1; }
181199
args+=(-t "$(parse_time "$2")")
182200
shift 2
183201
;;
184202
--partition)
203+
[[ $# -ge 2 ]] || { echo 1>&2 "Error: --partition requires a PARTITION argument."; exit 1; }
185204
args+=(--partition="$2")
186205
shift 2
187206
;;
@@ -190,6 +209,7 @@ function main {
190209
shift
191210
;;
192211
-j | --threads)
212+
[[ $# -ge 2 ]] || { echo 1>&2 "Error: $1 requires a CPU-count argument."; exit 1; }
193213
args+=(--cpus-per-task="$2")
194214
shift 2
195215
;;
@@ -198,6 +218,7 @@ function main {
198218
shift
199219
;;
200220
-m)
221+
[[ $# -ge 2 ]] || { echo 1>&2 "Error: -m requires a SIZE argument."; exit 1; }
201222
args+=(--mem="$2")
202223
shift 2
203224
;;
@@ -217,6 +238,10 @@ function main {
217238
echo 1>&2 "Error: --name must contain only letters, digits, '.', '_', '-'"
218239
exit 1
219240
fi
241+
# Best-effort duplicate-name guard: the Comment marker that records the
242+
# name is only set after submission (see below), so two near-simultaneous
243+
# launches with the same name can both pass here. Reattach-by-name
244+
# resolves the resulting ambiguity by erroring on multiple matches.
220245
local existing
221246
existing=$(squeue --me --states RUNNING,PENDING --noheader \
222247
--Format=JobID:12,Comment:80 2>/dev/null |
@@ -237,7 +262,7 @@ function main {
237262
set -- --job-name="sint-${session_name}" "$@"
238263
fi
239264

240-
# Default to 8 hours if no --time/-t specified
265+
# Default to 1 day if no --time/-t specified
241266
if ! printf '%s\n' "$@" | grep -qE '^(--time|-t)'; then
242267
set -- --time=24:00:00 "$@"
243268
fi
@@ -280,7 +305,7 @@ function main {
280305
output=$(sbatch --output=/dev/null --error=/dev/null "$@" "${BASH_SOURCE[0]}" tmux)
281306
sbatch_ec=$?
282307

283-
JOB_ID=$(<<<"${output}" egrep -o -e '\b[0-9]+$')
308+
JOB_ID=$(grep -Eo -e '\b[0-9]+$' <<<"${output}")
284309
if [[ -z "${JOB_ID}" ]]; then
285310
echo 1>&2 "$0: unable to find job id in sbatch output"
286311
exit "$sbatch_ec"
@@ -316,12 +341,30 @@ function main {
316341
echo 1>&2 ''
317342
echo 1>&2 'Connecting to sinteractive session, please wait...'
318343

319-
sleep 8s
344+
# A RUNNING job only means the allocation exists; the batch script still
345+
# has to bring up the tmux server. Poll for the session (a single SSH that
346+
# retries on the node) instead of guessing a fixed sleep, which races on a
347+
# slow or loaded node.
348+
local sock="sinteractive-${JOB_ID}"
349+
local connected=0
350+
if ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
351+
"${batchhost}" \
352+
"for i in \$(seq 1 30); do ${TMUX_BIN} -L '${sock}' has-session -t '${sock}' 2>/dev/null && exit 0; sleep 1; done; exit 1"; then
353+
connected=1
354+
fi
320355

321-
# sinteractive job is running and we're about to connect. From this
322-
# point, detaching or disconnecting should NOT cancel the job.
356+
# The session is up (or we gave up waiting); from this point a detach or
357+
# disconnect should NOT cancel the job.
323358
trap - SIGINT SIGTERM EXIT
324359

360+
if ((connected == 0)); then
361+
echo 1>&2 ''
362+
echo 1>&2 "$0: tmux session did not come up on ${batchhost} within 30s."
363+
echo 1>&2 "Job ${JOB_ID} may still be starting. Reconnect with:"
364+
echo 1>&2 " sinteractive --attach ${JOB_ID}"
365+
exit 1
366+
fi
367+
325368
ssh -X -t "${batchhost}" "${BASH_SOURCE[0]}" attach "sinteractive-${JOB_ID}"
326369

327370
# Give the batch script a moment to finish shutting down
@@ -417,6 +460,7 @@ function list_sessions {
417460
# Query cwd for each session in parallel; results land in $tmpdir/<jobid>
418461
local tmpdir
419462
tmpdir=$(mktemp -d)
463+
trap 'rm -rf "$tmpdir"' EXIT
420464
while read -r jobid comment node elapsed limit; do
421465
(get_session_cwd "$node" "$jobid" >"$tmpdir/$jobid") &
422466
done <<<"$lines"
@@ -436,8 +480,6 @@ function list_sessions {
436480
"$jobid" "$name" "$node" "$elapsed" "$limit" "$cwd"
437481
done <<<"$lines"
438482

439-
rm -rf "$tmpdir"
440-
441483
echo ''
442484
echo 'To reattach: sinteractive --attach JOBID (or NAME if set)'
443485
exit 0
@@ -452,7 +494,7 @@ function get_partition_from_args {
452494
return
453495
;;
454496
-p)
455-
echo "$2"
497+
echo "${2:-}"
456498
return
457499
;;
458500
*)
@@ -474,11 +516,11 @@ function extract_time_from_args {
474516
return 0
475517
;;
476518
--time)
477-
echo "$2"
519+
echo "${2:-}"
478520
return 0
479521
;;
480522
-t)
481-
echo "$2"
523+
echo "${2:-}"
482524
return 0
483525
;;
484526
-t=*)
@@ -832,7 +874,7 @@ function attach_tmux {
832874
local SESSION=$1
833875
local sock="$SESSION"
834876

835-
if [[ "$DISPLAY" != '' ]]; then
877+
if [[ "${DISPLAY:-}" != '' ]]; then
836878
$TMUX_BIN -L "$sock" setenv -t "$SESSION" DISPLAY "$DISPLAY"
837879
fi
838880

0 commit comments

Comments
 (0)