#!/usr/bin/env perl
$Data::Dumper::Sortkeys
= 1;
if
(-e
"$RealBin/../dist.ini"
) {
say
STDERR
"[dev mode] Using local lib"
if
(
$ENV
{
"DEBUG"
});
use
lib
"$RealBin/../lib"
;
}
my
$current_slurm_jobid
=
$ENV
{SLURM_JOBID} // -1;
my
$unix_username
=
$ENV
{USER};
my
$user_home_dir
=
$ENV
{HOME};
my
$opt_user
=
$unix_username
;
my
$opt_status
=
'.+'
;
my
$opt_running_bool
= 0;
my
$opt_pending_bool
= 0;
my
$opt_delete_bool
= 0;
my
$opt_verbose_bool
= 0;
my
$opt_queue
=
'.+'
;
my
$opt_name
=
'.+'
;
my
$opt_tab
= 0;
GetOptions(
'u|user=s'
=> \
$opt_user
,
'n|name=s'
=> \
$opt_name
,
's|status=s'
=> \
$opt_status
,
'r|running'
=> \
$opt_running_bool
,
'd|delete'
=> \
$opt_delete_bool
,
't|tab'
=> \
$opt_tab
,
'verbose'
=> \
$opt_verbose_bool
,
'version'
=>
sub
{
say
"lsjobs v"
,
$NBI::Slurm::VERSION
;
exit
},
'help'
=>
sub
{ usage() },
);
if
(not NBI::Slurm::has_squeue()) {
say
STDERR RED,
"Error:"
, RESET,
" squeue not found in PATH. Are you in the cluster?"
;
exit
1;
}
my
$jobs
= getjobs();
my
@ids
= ();
for
my
$positional
(
@ARGV
) {
if
(
$positional
=~ /^(\d+)$/) {
push
(
@ids
, $1);
}
else
{
if
(
$opt_name
eq
'.+'
) {
$opt_name
=
$positional
;
}
else
{
say
STDERR
"Error: unknown positional argument: $positional"
;
usage();
}
}
}
if
(
$opt_user
eq
'ALL'
or
$opt_user
eq
'all'
) {
$opt_user
=
'.+'
;
}
if
(
$opt_verbose_bool
) {
say
STDERR
"User: $opt_user"
;
say
STDERR
"Jobs: "
,
scalar
(
keys
%{
$jobs
});
}
my
$selected_jobs
= {};
my
$selected_arrays
= [[
'JobID'
,
'User'
,
'Queue'
,
'Name'
,
'State'
,
'Time'
,
'TotalTime'
,
'NodeList'
,
'CPUS'
,
'Memory'
,
'Reason'
],
[
'-----'
,
'----'
,
'-----'
,
'----'
,
'-----'
,
'----'
,
'---------'
,
'--------'
,
'----'
,
'------'
,
'------'
]];
if
(
$opt_tab
) {
$selected_arrays
->[0]->[0] =
"#"
.
$selected_arrays
->[0]->[0];
$selected_arrays
= [
$selected_arrays
->[0] ];
}
for
my
$job
(
sort
keys
%{
$jobs
}) {
if
(
$jobs
->{
$job
}->{USER} !~ /^
$opt_user
$/) {
next
;
}
if
(
$jobs
->{
$job
}->{PARTITION} !~ /
$opt_queue
/) {
next
;
}
if
(
$jobs
->{
$job
}->{NAME} !~ /
$opt_name
/) {
next
;
}
if
(
$opt_pending_bool
and
$jobs
->{
$job
}->{STATE} ne
'PENDING'
) {
next
;
}
if
(
$opt_running_bool
and
$jobs
->{
$job
}->{STATE} ne
'RUNNING'
) {
next
;
}
if
(
scalar
@ids
> 0 and not
grep
{
$_
eq
$job
}
@ids
) {
next
;
}
my
$array
= [
$jobs
->{
$job
}->{JOBID} =~/_/ ?
substr
(
$jobs
->{
$job
}->{JOBID}, 0,
index
(
$jobs
->{
$job
}->{JOBID},
'_'
)).
"#"
:
$jobs
->{
$job
}->{JOBID},
$jobs
->{
$job
}->{USER},
$jobs
->{
$job
}->{PARTITION},
$jobs
->{
$job
}->{NAME},
$opt_tab
?
$jobs
->{
$job
}->{STATE} : state_string(
$jobs
->{
$job
}->{STATE}),
$jobs
->{
$job
}->{TIME},
$jobs
->{
$job
}->{TIME_LIMIT},
$jobs
->{
$job
}->{NODELIST},
$jobs
->{
$job
}->{
"CPUS"
},
$jobs
->{
$job
}->{
"MIN_MEMORY"
},
$opt_tab
?
$jobs
->{
$job
}->{
"REASON"
} : reason_string(
$jobs
->{
$job
}->{
"REASON"
})
];
push
(@{
$selected_arrays
},
$array
);
}
if
(
$opt_tab
) {
for
my
$array
(@{
$selected_arrays
}) {
say
join
(
"\t"
, @{
$array
});
}
}
else
{
render_table(@{
$selected_arrays
});
print
RESET,
"\n"
;
}
if
(
$opt_verbose_bool
and
scalar
@{
$selected_arrays
} == 3) {
my
$job
= extractJobByID(
$jobs
,
$selected_arrays
->[2]->[0]);
for
my
$key
(
sort
keys
%{
$job
}) {
if
(
$job
->{
$key
} =~ /^$/ or
$job
->{
$key
} =~ /^(\(null\)|\*)$/) {
next
;
}
if
(
$key
=~/(S_C_T|USER|ACCOUNT)/) {
next
;
}
say
YELLOW,
sprintf
(
"%-20s"
,
$key
), RESET,
$job
->{
$key
};
}
}
my
@selected_ids
= joblist_to_ids(@{
$selected_arrays
});
if
(
$opt_delete_bool
and (
scalar
@selected_ids
)) {
say
RED
"\nDELETE JOBS:"
, RESET;
if
(prompt(
"Delete "
.
scalar
(
@selected_ids
) .
" jobs?"
,
'n'
) =~ /^(y|Y|yes|YES)$/) {
my
$command
=
"scancel "
.
join
(
" "
,
@selected_ids
);
system
(
$command
);
if
($? == -1) {
say
RED,
"ERROR"
, RESET
": Failed to delete: $!\n"
;
}
}
}
elsif
(
$opt_delete_bool
) {
say
STDERR
"No jobs selected for deletion"
;
}
sub
state_string {
my
$s
=
shift
;
$s
=
substr
(
$s
, 0, 1);
my
$c
= RESET;
if
(
$s
=~/^R/) {
$c
= GREEN . ON_BLACK;
}
elsif
(
$s
=~/^P/) {
$c
= YELLOW . ON_BLACK;
}
else
{
$c
= RED . ON_BLACK;
}
return
$c
.
$s
. WHITE . ON_BLACK;
}
sub
reason_string {
my
$s
=
shift
;
my
$c
= RESET;
if
(
$s
=~/^None/) {
$c
= BLUE . ON_BLACK;
}
elsif
(
$s
=~/^Priority/) {
$c
= YELLOW . ON_BLACK;
}
elsif
(
$s
=~/^Bad/) {
$c
= WHITE . ON_RED;
}
else
{
$c
= RED . ON_BLACK;
}
return
$c
.
$s
. RESET . ON_BLACK;
}
sub
joblist_to_ids {
my
@rows
=
@_
;
my
@ids
= ();
for
my
$row
(
@rows
) {
next
if
(
$row
->[0] !~ /^\d+$/);
push
@ids
,
$row
->[0];
}
return
@ids
;
}
sub
short_job {
my
$line_width
= get_terminal_width();
my
$job
=
shift
;
my
$jobid
=
$job
->{JOBID};
my
$name
=
$job
->{NAME};
my
$state
=
$job
->{STATE};
my
$user
=
$job
->{USER};
my
$queue
=
$job
->{PARTITION};
my
$time
=
$job
->{TIME};
my
$line
=
sprintf
(
"%-10s %-10s %-10s %-10s %-10s %-10s"
,
$jobid
,
$name
,
$state
,
$user
,
$queue
,
$time
);
return
$line
;
}
sub
render_table {
my
@rows
=
@_
;
my
$n_cols
=
scalar
(@{
$rows
[0]});
my
$line_width
= get_terminal_width() -
$n_cols
- 1;
my
@max_widths
= ();
for
my
$col
(0..
$n_cols
-1) {
my
$max_width
= 0;
for
my
$row
(
@rows
) {
my
$width
= ascii_len(
$row
->[
$col
]);
$max_width
=
$width
if
(
$width
>
$max_width
);
}
push
(
@max_widths
,
$max_width
);
}
for
my
$row
(
@rows
) {
my
$line
= WHITE . ON_BLACK;
for
my
$col
(0..
$n_cols
-1) {
my
$width
=
$max_widths
[
$col
];
my
$cell
=
$row
->[
$col
];
my
$stripped
=
$cell
;
$stripped
=~ s/\e\[[0-9;]
*m
//g;
my
$tmpline
.=
sprintf
(
"|%-${width}s "
,
$stripped
);
my
$index
=
index
(
$tmpline
,
$stripped
);
substr
(
$tmpline
,
$index
,
length
(
$stripped
),
$cell
);
$line
.=
$tmpline
;
}
say
$line
,
"|"
;
}
print
RESET;
}
sub
ascii_len {
my
$string
=
shift
;
$string
=~ s/\e\[[0-9;]
*m
//g;
return
length
(
$string
);
}
sub
extractJobByID {
my
(
$jobs
,
$id
) =
@_
;
my
$job
= {};
for
my
$jobid
(
keys
%{
$jobs
}) {
if
(
$jobid
eq
$id
) {
$job
=
$jobs
->{
$jobid
};
last
;
}
}
return
$job
;
}
sub
getjobs {
my
$jobs
= {};
my
$cmd
=
q(squeue --format='%all')
;
my
@output
= `
$cmd
`;
my
$c
= 0;
my
@header
= ();
for
my
$line
(
@output
) {
chomp
$line
;
my
@fields
=
split
(/\|/,
$line
);
$c
++;
if
(
$c
== 1 ) {
for
my
$field
(
@fields
) {
push
(
@header
, stripchars(
$field
));
}
}
else
{
my
$job
= {};
if
(
scalar
(
@fields
) !=
scalar
(
@header
)) {
say
STDERR
"Error: number of fields in header and line do not match"
;
say
STDERR
"Header: "
,
scalar
(
@header
);
say
STDERR
"Line: "
,
scalar
(
@fields
);
say
STDERR
"Line: $line"
;
exit
;
}
for
my
$i
(0..
$#header
) {
$job
->{
"$header[$i]"
} =
$fields
[
$i
]
if
(not
defined
$job
->{
"$header[$i]"
});
}
$jobs
->{
$job
->{JOBID}} =
$job
;
}
}
return
$jobs
;
}
sub
get_terminal_width {
my
$terminal_width
= `tput cols`;
chomp
(
$terminal_width
);
return
$terminal_width
> 20 ?
$terminal_width
: 80;
}
sub
stripchars {
my
$string
=
shift
;
$string
=~ s/[^A-Za-z0-9]/_/g;
return
$string
;
}
sub
prompt {
my
(
$message
,
$default
) =
@_
;
my
$prompt
=
"$message [$default]: "
;
print
$prompt
;
my
$answer
= <STDIN>;
chomp
$answer
;
$answer
=
$default
if
(
$answer
eq
''
);
return
$answer
;
}
sub
usage {
say
<<END;
Usage: lsjobs [options] [jobid ... | pattern ]
----------------------------------------------
Options:
-u, --user <username> Show only jobs from this user [default: $unix_username]
Type 'all' to show all users
-n, --name <pattern> Show only jobs with this name [default: .+]
-s, --status <pattern> Show only jobs with this status [default: .+]
-r, --running Show only running jobs
-p, --pending Show only pending jobs
-t, --tab Output in simple TSV format (pipe to vd for interactive table)
-d, --delete Delete the selected jobs
--verbose Show verbose output
END
exit
;
}