Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,30 @@ and this project adheres to

## [Unreleased]

## [0.5.0] - 2026-08-26

### Added

- Colour in the launch and teardown narration. Four roles rather than a
rainbow: teal for identifiers (job ids, node names), echoing the `#2DBFB8`
the status bar already uses for the same things so the two agree about what
an identifier is; dim for progress, which leaves `✓ Session … is ready` the
only bright line in the block; yellow for keys and warnings; red for errors.

`SINTERACTIVE_COLOR` takes `auto`/`always`/`never` (default `auto`), and
`NO_COLOR` is honoured whatever its value. The `auto` test is on **standard
error**, not standard output, because that is where the narration goes — the
two differ in exactly the case that matters, since `--detach ... > file`
should still narrate to the terminal while `2>log` must stay free of
escapes. With colour off every variable is empty rather than being guarded
at each use, so one set of format strings serves both and there is no
second, less-tested path.

Two messages were tightened in passing: `Interactive job with ID N
submitted, please wait` is now `Submitted job N, waiting for it to start`,
and the detach block's prose `To reconnect:` / `To cancel the job:` became an
aligned `Reconnect:` / `Cancel:` pair matching the one shown at launch.

- Storage quota in the notice line. A session shows a red `OVER QUOTA` warning
above its status line, with the overage, while the user is past their hard
limit, re-checked every `SINTERACTIVE_QUOTA_POLL` seconds (default 600).
Expand Down Expand Up @@ -449,7 +471,8 @@ First tagged release.
installers (user, system-wide, and per-node fan-out), and a
`bodhi-compute` Claude Code skill.

[Unreleased]: https://github.com/rnabioco/sinteractive/compare/v0.4.0...HEAD
[Unreleased]: https://github.com/rnabioco/sinteractive/compare/v0.5.0...HEAD
[0.5.0]: https://github.com/rnabioco/sinteractive/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/rnabioco/sinteractive/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/rnabioco/sinteractive/compare/v0.2.2...v0.3.0
[0.2.2]: https://github.com/rnabioco/sinteractive/compare/v0.2.1...v0.2.2
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ Set personal defaults in your `~/.bashrc`; explicit flags always win.
| `SINTERACTIVE_MEM` | Default memory (e.g. `16G`) | `8G` |
| `SINTERACTIVE_MOUSE` | `on`/`1`/`true`/`yes` enables mouse support | off |
| `SINTERACTIVE_TMUX` | Path to the `tmux` binary on the compute node | `/usr/local/bin/tmux` |
| `SINTERACTIVE_COLOR` | `auto`/`always`/`never` for launch and teardown output; `NO_COLOR` also honoured | `auto` |
| `SINTERACTIVE_QUOTA_POLL` | Seconds between storage-quota checks | `600` |
| `SINTERACTIVE_QUOTA_FILE` | Pipe-delimited file of hard quotas | `/cluster/scripts/quota_current.txt` |
| `SINTERACTIVE_QUOTA_HOSTS` | Quota daemons to sum usage across | Bodhi's `172.20.8.110-118` |
| `SINTERACTIVE_QUOTA_PORT` | Port those daemons listen on | `9878` |

```bash
# Example: always use mouse mode and a bigger default allocation
Expand Down
2 changes: 1 addition & 1 deletion docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ script there, so a copy may be executing while you install.

```bash
make nodes-check
# compute00 sinteractive=0.4.0 assets=yes tmux 3.7c
# compute00 sinteractive=0.5.0 assets=yes tmux 3.7c
# compute01 sinteractive=unknown assets=no tmux 3.7b
# compgpu01 unreachable
```
Expand Down
20 changes: 19 additions & 1 deletion man/sinteractive.1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.TH SINTERACTIVE 1 "August 2026" "sinteractive 0.4.0" "User Commands"
.TH SINTERACTIVE 1 "August 2026" "sinteractive 0.5.0" "User Commands"
.SH NAME
sinteractive \- start an interactive tmux session on a Slurm compute node
.SH SYNOPSIS
Expand Down Expand Up @@ -622,6 +622,24 @@ Port those daemons listen on. Default: 9878.
.TP
.B SINTERACTIVE_QUOTA_TIMEOUT
Seconds to wait for each daemon's reply. Default: 5.
.TP
.B SINTERACTIVE_COLOR
.BR auto | always | never
for the launch and teardown narration. Default:
.BR auto ,
which colours only when standard error is a terminal and
.B TERM
is neither
.B dumb
nor empty. The test is on standard error rather than standard output because
that is where the narration goes: a redirect of stdout still narrates to your
terminal, and
.B 2>log
stays free of escapes.
.B NO_COLOR
(any value, per no\-color.org) disables colour;
.B SINTERACTIVE_COLOR=always
overrides both, for piping into something that renders escapes itself.
.PP
Exported inside a session:
.B SINTERACTIVE_JOB_ID
Expand Down
2 changes: 1 addition & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[workspace]
name = "sinteractive"
# Keep in sync with VERSION in `sinteractive` and the release tag (vX.Y.Z).
version = "0.4.0"
version = "0.5.0"
description = "Documentation site for sinteractive"
channels = ["conda-forge"]
platforms = ["linux-64", "osx-arm64"]
Expand Down
101 changes: 68 additions & 33 deletions sinteractive
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,48 @@ set -u
TMUX_BIN="${SINTERACTIVE_TMUX:-/usr/local/bin/tmux}"
JOB_ID=''
# Reported by --version; keep in sync with the release tag (vX.Y.Z).
VERSION='0.4.0'
VERSION='0.5.0'
# Basename for error messages ($0 is the full install path — noisy).
PROG="${0##*/}"

# ---------------------------------------------------------------------------
# Colour for the launch and teardown narration.
#
# Everything here is written to stderr, so the decision hangs on whether
# *stderr* is a terminal — not stdout. The two differ in exactly the case that
# matters: `sinteractive --detach ... > file` still narrates to a terminal, and
# `sinteractive ... 2>log` must not write escapes into the log.
#
# When colour is off every variable is the empty string rather than being
# guarded at each use, so one set of printf strings serves both cases and
# there is no second, less-tested code path.
#
# SINTERACTIVE_COLOR takes always/never/auto (default auto); NO_COLOR is
# honoured whatever its value, per no-color.org, and TERM=dumb opts out too.
# An explicit SINTERACTIVE_COLOR=always wins over both, for piping into
# something that renders escapes itself (`less -R`, CI logs that colourise).
# ---------------------------------------------------------------------------
C_RST='' C_BOLD='' C_DIM='' C_OK='' C_WARN='' C_ERR='' C_ID='' C_KEY=''
function init_colors {
local want="${SINTERACTIVE_COLOR:-auto}"
case "$want" in
never | no | 0) return 0 ;;
always | yes | 1) ;;
*)
[[ -n "${NO_COLOR+x}" ]] && return 0
[[ -t 2 ]] || return 0
[[ "${TERM:-}" == dumb || -z "${TERM:-}" ]] && return 0
;;
esac
C_RST=$'\e[0m' C_BOLD=$'\e[1m' C_DIM=$'\e[2m'
C_OK=$'\e[32m' C_WARN=$'\e[33m' C_ERR=$'\e[31m'
# Teal for identifiers — job ids, node names — echoing the #2DBFB8 the
# status bar uses for the same things, so the launch narration and the
# session you land in agree about what is an identifier.
C_ID=$'\e[1;36m' C_KEY=$'\e[1;33m'
}
init_colors

# Convert time shorthand (e.g. 8h, 30m, 2d, 1d12h, 1h30m) to HH:MM:SS
function parse_time {
local input="$1"
Expand Down Expand Up @@ -536,8 +574,8 @@ function main {
fitted=$(seconds_to_slurm_time "$maint_fit")
mapfile -d '' -t rewritten < <(replace_time_in_args "$fitted" "$@")
set -- "${rewritten[@]}"
echo "Maintenance (${maint_name}) starts $(date -d "@$maint_start" '+%a %b %-d %H:%M')."
echo "Shortened the request from ${maint_orig} to ${fitted} so the session ends before it."
echo "${C_WARN}${C_BOLD}!${C_RST} ${C_WARN}Maintenance (${maint_name}) starts $(date -d "@$maint_start" '+%a %b %-d %H:%M').${C_RST}"
echo " ${C_DIM}Shortened the request from${C_RST} ${maint_orig} ${C_DIM}to${C_RST} ${C_BOLD}${fitted}${C_RST} ${C_DIM}so the session ends before it.${C_RST}"
echo ''
fi

Expand Down Expand Up @@ -565,12 +603,12 @@ function main {
if [[ -n "$rname" ]]; then
echo 1>&2 "Note: you already have a running session named '${rname}' (job ${rjob} on ${rnode})."
else
echo 1>&2 "Note: you already have a running session (job ${rjob} on ${rnode})."
echo 1>&2 "${C_DIM}Note: you already have a running session (job ${rjob} on ${rnode}).${C_RST}"
fi
echo 1>&2 " Reattach with 'sinteractive --attach ${rname:-${rjob}}'; starting a new session..."
echo 1>&2 "${C_DIM} Reattach with 'sinteractive --attach ${rname:-${rjob}}'; starting a new session...${C_RST}"
else
echo 1>&2 "Note: you already have ${n_running} running sessions ('sinteractive --list' shows them)."
echo 1>&2 " Starting a new session..."
echo 1>&2 "${C_DIM}Note: you already have ${n_running} running sessions ('sinteractive --list' shows them).${C_RST}"
echo 1>&2 "${C_DIM} Starting a new session...${C_RST}"
fi
fi

Expand Down Expand Up @@ -625,7 +663,7 @@ function main {
fi
scontrol update JobId="${JOB_ID}" Comment="${job_comment}"

echo 1>&2 "Interactive job with ID ${JOB_ID} submitted, please wait."
echo 1>&2 "${C_DIM}Submitted job${C_RST} ${C_ID}${JOB_ID}${C_RST}${C_DIM}, waiting for it to start.${C_RST}"

# While the job is pending, show why (squeue's pend reason) and Slurm's
# estimated start time. The scheduler is polled every 5 seconds; on a TTY a
Expand Down Expand Up @@ -679,9 +717,10 @@ function main {
fi
fi
fi
printf 1>&2 '\r\033[K %s %s%s (%s elapsed)' \
"${frames[spin % 10]}" "$why" "$est" \
"$(format_short_duration $((now - wait_start)))"
printf 1>&2 '\r\033[K %s%s%s %s%s%s %s(%s elapsed)%s' \
"$C_ID" "${frames[spin % 10]}" "$C_RST" \
"$C_DIM" "${why}${est}" "$C_RST" \
"$C_DIM" "$(format_short_duration $((now - wait_start)))" "$C_RST"
spin=$((spin + 1))
sleep 0.5
else
Expand All @@ -693,7 +732,7 @@ function main {
batchhost=$(squeue --jobs "${JOB_ID}" --noheader --Format batchhost | xargs)

echo 1>&2 ''
echo 1>&2 'Connecting to sinteractive session, please wait...'
echo 1>&2 "${C_DIM}Allocated ${C_RST}${C_ID}${batchhost}${C_RST}${C_DIM} — bringing up the session...${C_RST}"

# A RUNNING job only means the allocation exists; the batch script still
# has to bring up the tmux server. Poll for the session (a single SSH that
Expand Down Expand Up @@ -728,11 +767,11 @@ function main {
fi
local attach_target="${session_name:-${JOB_ID}}"
echo 1>&2 ''
echo 1>&2 "Session ${JOB_ID} is ready on ${batchhost}."
echo 1>&2 "${C_OK}${C_BOLD}✓${C_RST} ${C_BOLD}Session ${C_RST}${C_ID}${JOB_ID}${C_RST}${C_BOLD} is ready on ${C_RST}${C_ID}${batchhost}${C_RST}${C_BOLD}.${C_RST}"
echo 1>&2 ''
echo 1>&2 " Attach: sinteractive --attach ${attach_target}"
echo 1>&2 " Status: sinteractive --status ${attach_target}"
echo 1>&2 " Cancel: sinteractive --cancel ${attach_target}"
echo 1>&2 " ${C_KEY}Attach:${C_RST} sinteractive --attach ${attach_target}"
echo 1>&2 " ${C_KEY}Status:${C_RST} sinteractive --status ${attach_target}"
echo 1>&2 " ${C_KEY}Cancel:${C_RST} sinteractive --cancel ${attach_target}"
return
fi

Expand All @@ -747,18 +786,14 @@ function main {

if [[ "${post_state}" == 'RUNNING' ]]; then
echo 1>&2 ''
echo 1>&2 "Disconnected from sinteractive session."
echo 1>&2 "Your job ${JOB_ID} is still running on ${batchhost}."
echo 1>&2 "${C_WARN}${C_BOLD}⠿${C_RST} ${C_BOLD}Detached.${C_RST} ${C_DIM}Job${C_RST} ${C_ID}${JOB_ID}${C_RST} ${C_DIM}is still running on${C_RST} ${C_ID}${batchhost}${C_RST}${C_DIM}.${C_RST}"
echo 1>&2 ''
local attach_target="${JOB_ID}"
if [[ -n "${session_name}" ]]; then
attach_target="${session_name}"
fi
echo 1>&2 "To reconnect:"
echo 1>&2 " sinteractive --attach ${attach_target}"
echo 1>&2 ''
echo 1>&2 "To cancel the job:"
echo 1>&2 " scancel ${JOB_ID}"
echo 1>&2 " ${C_KEY}Reconnect:${C_RST} sinteractive --attach ${attach_target}"
echo 1>&2 " ${C_KEY}Cancel:${C_RST} scancel ${JOB_ID}"

# Show other running sinteractive sessions. Pipe-delimited -o output
# (not --Format columns) so long comments can't run into the next field
Expand All @@ -770,14 +805,14 @@ function main {

if [[ -n "$other_running" ]]; then
echo 1>&2 ''
echo 1>&2 'Other running sinteractive sessions:'
printf 1>&2 ' %-10s %-14s %-10s %-10s\n' 'JOBID' 'NODE' 'ELAPSED' 'TIMELIMIT'
echo 1>&2 "${C_DIM}Other running sinteractive sessions:${C_RST}"
printf 1>&2 " ${C_DIM}%-10s %-14s %-10s %-10s${C_RST}\\n" 'JOBID' 'NODE' 'ELAPSED' 'TIMELIMIT'
while IFS='|' read -r jobid comment node elapsed limit; do
printf 1>&2 ' %-10s %-14s %-10s %-10s\n' "$jobid" "$node" "$elapsed" "$limit"
printf 1>&2 " ${C_ID}%-10s${C_RST} %-14s %-10s %-10s\\n" "$jobid" "$node" "$elapsed" "$limit"
done <<<"$other_running"
fi
else
echo 1>&2 "Session ended. Job ${JOB_ID} is no longer running."
echo 1>&2 "${C_DIM}Session ended. Job${C_RST} ${C_ID}${JOB_ID}${C_RST} ${C_DIM}is no longer running.${C_RST}"

# Show any remaining sinteractive sessions. Pipe-delimited -o output
# (not --Format columns) so long comments can't run into the next field
Expand All @@ -788,13 +823,13 @@ function main {

if [[ -n "$remaining" ]]; then
echo 1>&2 ''
echo 1>&2 'You have other sinteractive sessions still running:'
printf 1>&2 ' %-10s %-14s %-10s %-10s\n' 'JOBID' 'NODE' 'ELAPSED' 'TIMELIMIT'
echo 1>&2 "${C_WARN}You have other sinteractive sessions still running:${C_RST}"
printf 1>&2 " ${C_DIM}%-10s %-14s %-10s %-10s${C_RST}\\n" 'JOBID' 'NODE' 'ELAPSED' 'TIMELIMIT'
while IFS='|' read -r jobid comment node elapsed limit; do
printf 1>&2 ' %-10s %-14s %-10s %-10s\n' "$jobid" "$node" "$elapsed" "$limit"
printf 1>&2 " ${C_ID}%-10s${C_RST} %-14s %-10s %-10s\\n" "$jobid" "$node" "$elapsed" "$limit"
done <<<"$remaining"
echo 1>&2 ''
echo 1>&2 'To free slots, cancel sessions you no longer need:'
echo 1>&2 "${C_DIM}To free slots, cancel sessions you no longer need:${C_RST}"
while IFS='|' read -r jobid _rest; do
echo 1>&2 " scancel $jobid"
done <<<"$remaining"
Expand Down Expand Up @@ -1907,7 +1942,7 @@ function fit_maintenance_window {
local maint_when
maint_when=$(date -d "@$start_epoch" "+%a %b %-d %H:%M")
echo 1>&2 ""
echo 1>&2 "Error: maintenance starts too soon to open a session."
echo 1>&2 "${C_ERR}${C_BOLD}Error:${C_RST}${C_ERR} maintenance starts too soon to open a session.${C_RST}"
echo 1>&2 ""
echo 1>&2 " Reservation: ${name}"
echo 1>&2 " Starts: ${maint_when} (in $(format_short_duration $gap_secs))"
Expand Down Expand Up @@ -2053,7 +2088,7 @@ function cancel_session {

local desc="session ${jobid}"
[[ -n "$name" ]] && desc+=" (${name})"
echo 1>&2 "Cancelled ${desc}${node:+ on ${node}}."
echo 1>&2 "${C_OK}✓${C_RST} Cancelled ${desc}${node:+ on ${C_ID}${node}${C_RST}}."
exit 0
}

Expand Down
Loading