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
1219TMUX_BIN=' /usr/local/bin/tmux'
1320JOB_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
8397All 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
109123function 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